orama/pkg/gateway/handlers/auth/handlers_test.go
2026-02-13 16:18:22 +02:00

720 lines
24 KiB
Go

package auth
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
authsvc "github.com/DeBrosOfficial/network/pkg/gateway/auth"
"github.com/DeBrosOfficial/network/pkg/logging"
"go.uber.org/zap"
)
// ---------------------------------------------------------------------------
// Mock implementations
// ---------------------------------------------------------------------------
// mockDatabaseClient implements DatabaseClient with configurable query results.
type mockDatabaseClient struct {
queryResult *QueryResult
queryErr error
}
func (m *mockDatabaseClient) Query(_ context.Context, _ string, _ ...interface{}) (*QueryResult, error) {
return m.queryResult, m.queryErr
}
// mockNetworkClient implements NetworkClient and returns a mockDatabaseClient.
type mockNetworkClient struct {
db *mockDatabaseClient
}
func (m *mockNetworkClient) Database() DatabaseClient {
return m.db
}
// mockClusterProvisioner implements ClusterProvisioner as a no-op.
type mockClusterProvisioner struct{}
func (m *mockClusterProvisioner) CheckNamespaceCluster(_ context.Context, _ string) (string, string, bool, error) {
return "", "", false, nil
}
func (m *mockClusterProvisioner) ProvisionNamespaceCluster(_ context.Context, _ int, _, _ string) (string, string, error) {
return "", "", nil
}
func (m *mockClusterProvisioner) GetClusterStatusByID(_ context.Context, _ string) (interface{}, error) {
return nil, nil
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// testLogger returns a silent *logging.ColoredLogger suitable for tests.
func testLogger() *logging.ColoredLogger {
nop := zap.NewNop()
return &logging.ColoredLogger{Logger: nop}
}
// noopInternalAuth is a no-op internal auth context function.
func noopInternalAuth(ctx context.Context) context.Context { return ctx }
// decodeBody is a test helper that decodes a JSON response body into a map.
func decodeBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]interface{} {
t.Helper()
var m map[string]interface{}
if err := json.NewDecoder(rec.Body).Decode(&m); err != nil {
t.Fatalf("failed to decode response body: %v", err)
}
return m
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
func TestNewHandlers(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
if h == nil {
t.Fatal("NewHandlers returned nil")
}
}
func TestSetClusterProvisioner(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
// Should not panic.
h.SetClusterProvisioner(&mockClusterProvisioner{})
}
// --- ChallengeHandler tests -----------------------------------------------
func TestChallengeHandler_MissingWallet(t *testing.T) {
// authService is nil, but the handler checks it first and returns 503.
// To reach the wallet validation we need a non-nil authService.
// Since authsvc.Service is a concrete struct, we create a zero-value one
// (it will never be reached for this test path).
// However, the handler checks `h.authService == nil` before everything else.
// So we must supply a non-nil *authsvc.Service. We can create one with
// an empty signing key (NewService returns error for empty PEM only if
// the PEM is non-empty but unparseable). An empty PEM is fine.
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
body, _ := json.Marshal(ChallengeRequest{Wallet: ""})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/challenge", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ChallengeHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "wallet is required" {
t.Fatalf("expected error 'wallet is required', got %v", m["error"])
}
}
func TestChallengeHandler_InvalidMethod(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/challenge", nil)
rec := httptest.NewRecorder()
h.ChallengeHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "method not allowed" {
t.Fatalf("expected error 'method not allowed', got %v", m["error"])
}
}
func TestChallengeHandler_NilAuthService(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
body, _ := json.Marshal(ChallengeRequest{Wallet: "0xABC"})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/challenge", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ChallengeHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
}
}
// --- WhoamiHandler tests --------------------------------------------------
func TestWhoamiHandler_NoAuth(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/whoami", nil)
rec := httptest.NewRecorder()
h.WhoamiHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
m := decodeBody(t, rec)
// When no auth context is set, "authenticated" should be false.
if auth, ok := m["authenticated"].(bool); !ok || auth {
t.Fatalf("expected authenticated=false, got %v", m["authenticated"])
}
if method, ok := m["method"].(string); !ok || method != "api_key" {
t.Fatalf("expected method='api_key', got %v", m["method"])
}
if ns, ok := m["namespace"].(string); !ok || ns != "default" {
t.Fatalf("expected namespace='default', got %v", m["namespace"])
}
}
func TestWhoamiHandler_WithAPIKey(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/whoami", nil)
ctx := req.Context()
ctx = context.WithValue(ctx, CtxKeyAPIKey, "ak_test123:default")
ctx = context.WithValue(ctx, CtxKeyNamespaceOverride, "default")
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
h.WhoamiHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
m := decodeBody(t, rec)
if auth, ok := m["authenticated"].(bool); !ok || !auth {
t.Fatalf("expected authenticated=true, got %v", m["authenticated"])
}
if method, ok := m["method"].(string); !ok || method != "api_key" {
t.Fatalf("expected method='api_key', got %v", m["method"])
}
if key, ok := m["api_key"].(string); !ok || key != "ak_test123:default" {
t.Fatalf("expected api_key='ak_test123:default', got %v", m["api_key"])
}
if ns, ok := m["namespace"].(string); !ok || ns != "default" {
t.Fatalf("expected namespace='default', got %v", m["namespace"])
}
}
func TestWhoamiHandler_WithJWT(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
claims := &authsvc.JWTClaims{
Iss: "orama-gateway",
Sub: "0xWALLET",
Aud: "gateway",
Iat: 1000,
Nbf: 1000,
Exp: 9999,
Namespace: "myns",
}
req := httptest.NewRequest(http.MethodGet, "/v1/auth/whoami", nil)
ctx := context.WithValue(req.Context(), CtxKeyJWT, claims)
ctx = context.WithValue(ctx, CtxKeyNamespaceOverride, "myns")
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
h.WhoamiHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
m := decodeBody(t, rec)
if auth, ok := m["authenticated"].(bool); !ok || !auth {
t.Fatalf("expected authenticated=true, got %v", m["authenticated"])
}
if method, ok := m["method"].(string); !ok || method != "jwt" {
t.Fatalf("expected method='jwt', got %v", m["method"])
}
if sub, ok := m["subject"].(string); !ok || sub != "0xWALLET" {
t.Fatalf("expected subject='0xWALLET', got %v", m["subject"])
}
if ns, ok := m["namespace"].(string); !ok || ns != "myns" {
t.Fatalf("expected namespace='myns', got %v", m["namespace"])
}
}
// --- LogoutHandler tests --------------------------------------------------
func TestLogoutHandler_MissingRefreshToken(t *testing.T) {
// The LogoutHandler does NOT validate refresh_token as required the same
// way RefreshHandler does. Looking at the source, it checks:
// if req.All && no JWT subject -> 401
// then passes req.RefreshToken to authService.RevokeToken
// With All=false and empty RefreshToken, RevokeToken returns "nothing to revoke".
// But before that, authService == nil returns 503.
//
// To test the validation path, we need authService != nil, and All=false
// with empty RefreshToken. The handler will call authService.RevokeToken
// which returns an error because we have a real service but no DB.
// However, the key point is that the handler itself doesn't short-circuit
// on empty token -- that's left to RevokeToken. So we must accept whatever
// error code the handler returns via the authService error path.
//
// Since we can't easily mock authService (it's a concrete struct),
// we test with nil authService to verify the 503 early return.
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
body, _ := json.Marshal(LogoutRequest{RefreshToken: ""})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/logout", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.LogoutHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
}
}
func TestLogoutHandler_InvalidMethod(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/logout", nil)
rec := httptest.NewRecorder()
h.LogoutHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestLogoutHandler_AllTrueNoJWT(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
body, _ := json.Marshal(LogoutRequest{All: true})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/logout", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.LogoutHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "jwt required for all=true" {
t.Fatalf("expected error 'jwt required for all=true', got %v", m["error"])
}
}
// --- RefreshHandler tests -------------------------------------------------
func TestRefreshHandler_MissingRefreshToken(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
body, _ := json.Marshal(RefreshRequest{})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.RefreshHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "refresh_token is required" {
t.Fatalf("expected error 'refresh_token is required', got %v", m["error"])
}
}
func TestRefreshHandler_InvalidMethod(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/refresh", nil)
rec := httptest.NewRecorder()
h.RefreshHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestRefreshHandler_NilAuthService(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
body, _ := json.Marshal(RefreshRequest{RefreshToken: "some-token"})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.RefreshHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
}
}
// --- APIKeyToJWTHandler tests ---------------------------------------------
func TestAPIKeyToJWTHandler_MissingKey(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/token", nil)
rec := httptest.NewRecorder()
h.APIKeyToJWTHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "missing API key" {
t.Fatalf("expected error 'missing API key', got %v", m["error"])
}
}
func TestAPIKeyToJWTHandler_InvalidMethod(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/token", nil)
rec := httptest.NewRecorder()
h.APIKeyToJWTHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestAPIKeyToJWTHandler_NilAuthService(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/token", nil)
req.Header.Set("X-API-Key", "ak_test:default")
rec := httptest.NewRecorder()
h.APIKeyToJWTHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
}
}
// --- RegisterHandler tests ------------------------------------------------
func TestRegisterHandler_MissingFields(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
tests := []struct {
name string
req RegisterRequest
}{
{"missing wallet", RegisterRequest{Nonce: "n", Signature: "s"}},
{"missing nonce", RegisterRequest{Wallet: "0x123", Signature: "s"}},
{"missing signature", RegisterRequest{Wallet: "0x123", Nonce: "n"}},
{"all empty", RegisterRequest{}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
body, _ := json.Marshal(tc.req)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.RegisterHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "wallet, nonce and signature are required" {
t.Fatalf("expected error 'wallet, nonce and signature are required', got %v", m["error"])
}
})
}
}
func TestRegisterHandler_InvalidMethod(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/register", nil)
rec := httptest.NewRecorder()
h.RegisterHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rec.Code)
}
}
func TestRegisterHandler_NilAuthService(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
body, _ := json.Marshal(RegisterRequest{Wallet: "0x123", Nonce: "n", Signature: "s"})
req := httptest.NewRequest(http.MethodPost, "/v1/auth/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.RegisterHandler(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected status %d, got %d", http.StatusServiceUnavailable, rec.Code)
}
}
// --- markNonceUsed (tested indirectly via nil safety) ----------------------
func TestMarkNonceUsed_NilNetClient(t *testing.T) {
// markNonceUsed is unexported but returns early when h.netClient == nil.
// We verify it does not panic by constructing a Handlers with nil netClient
// and invoking it through the struct directly (same-package test).
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
// This should not panic.
h.markNonceUsed(context.Background(), 1, "0xwallet", "nonce123")
}
// --- resolveNamespace (tested indirectly via nil safety) --------------------
func TestResolveNamespace_NilAuthService(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
_, err := h.resolveNamespace(context.Background(), "default")
if err == nil {
t.Fatal("expected error when authService is nil, got nil")
}
}
// --- extractAPIKey tests ---------------------------------------------------
func TestExtractAPIKey_XAPIKeyHeader(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-API-Key", "ak_test123:ns")
got := extractAPIKey(req)
if got != "ak_test123:ns" {
t.Fatalf("expected 'ak_test123:ns', got '%s'", got)
}
}
func TestExtractAPIKey_BearerNonJWT(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer ak_mykey")
got := extractAPIKey(req)
if got != "ak_mykey" {
t.Fatalf("expected 'ak_mykey', got '%s'", got)
}
}
func TestExtractAPIKey_BearerJWTSkipped(t *testing.T) {
// A JWT-looking token (two dots) should be skipped by extractAPIKey.
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer header.payload.signature")
got := extractAPIKey(req)
if got != "" {
t.Fatalf("expected empty string for JWT bearer, got '%s'", got)
}
}
func TestExtractAPIKey_ApiKeyScheme(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "ApiKey ak_scheme_key")
got := extractAPIKey(req)
if got != "ak_scheme_key" {
t.Fatalf("expected 'ak_scheme_key', got '%s'", got)
}
}
func TestExtractAPIKey_QueryParam(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/?api_key=ak_query", nil)
got := extractAPIKey(req)
if got != "ak_query" {
t.Fatalf("expected 'ak_query', got '%s'", got)
}
}
func TestExtractAPIKey_TokenQueryParam(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/?token=ak_tokenval", nil)
got := extractAPIKey(req)
if got != "ak_tokenval" {
t.Fatalf("expected 'ak_tokenval', got '%s'", got)
}
}
func TestExtractAPIKey_NoKey(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
got := extractAPIKey(req)
if got != "" {
t.Fatalf("expected empty string, got '%s'", got)
}
}
func TestExtractAPIKey_AuthorizationNoSchemeNonJWT(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "ak_raw_token")
got := extractAPIKey(req)
if got != "ak_raw_token" {
t.Fatalf("expected 'ak_raw_token', got '%s'", got)
}
}
func TestExtractAPIKey_AuthorizationNoSchemeJWTSkipped(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "a.b.c")
got := extractAPIKey(req)
if got != "" {
t.Fatalf("expected empty string for JWT-like auth, got '%s'", got)
}
}
// --- ChallengeHandler invalid JSON ----------------------------------------
func TestChallengeHandler_InvalidJSON(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/challenge", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ChallengeHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
m := decodeBody(t, rec)
if errMsg, ok := m["error"].(string); !ok || errMsg != "invalid json body" {
t.Fatalf("expected error 'invalid json body', got %v", m["error"])
}
}
// --- WhoamiHandler with namespace override --------------------------------
func TestWhoamiHandler_NamespaceOverride(t *testing.T) {
h := NewHandlers(testLogger(), nil, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodGet, "/v1/auth/whoami", nil)
ctx := context.WithValue(req.Context(), CtxKeyNamespaceOverride, "custom-ns")
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
h.WhoamiHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
}
m := decodeBody(t, rec)
if ns, ok := m["namespace"].(string); !ok || ns != "custom-ns" {
t.Fatalf("expected namespace='custom-ns', got %v", m["namespace"])
}
}
// --- LogoutHandler invalid JSON -------------------------------------------
func TestLogoutHandler_InvalidJSON(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/logout", bytes.NewReader([]byte("bad json")))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.LogoutHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
}
// --- RefreshHandler invalid JSON ------------------------------------------
func TestRefreshHandler_InvalidJSON(t *testing.T) {
svc, err := authsvc.NewService(testLogger(), nil, "", "default")
if err != nil {
t.Fatalf("failed to create auth service: %v", err)
}
h := NewHandlers(testLogger(), svc, nil, "default", noopInternalAuth)
req := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh", bytes.NewReader([]byte("bad json")))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.RefreshHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, rec.Code)
}
}