package internalhttp import ( "bytes" "context" "errors" "net/http" "net/http/httptest" "testing" "time" "galaxy/authsession/internal/service/blockuser" "galaxy/authsession/internal/service/getsession" "galaxy/authsession/internal/service/listusersessions" "galaxy/authsession/internal/service/revokeallusersessions" "galaxy/authsession/internal/service/revokedevicesession" "galaxy/authsession/internal/service/shared" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func TestGetSessionHandlerSuccess(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(func(_ context.Context, input getsession.Input) (getsession.Result, error) { assert.Equal(t, getsession.Input{DeviceSessionID: "device-session-123"}, input) return getsession.Result{ Session: validSessionDTO(), }, nil }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/internal/sessions/device-session-123", nil) handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, `{"session":{"device_session_id":"device-session-123","user_id":"user-123","client_public_key":"public-key-material","status":"active","created_at":"2026-04-05T12:00:00Z"}}`, recorder.Body.String()) } func TestListUserSessionsHandlerSuccess(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(func(_ context.Context, input listusersessions.Input) (listusersessions.Result, error) { assert.Equal(t, listusersessions.Input{UserID: "user-123"}, input) first := validSessionDTO() second := validRevokedSessionDTO() second.DeviceSessionID = "device-session-122" return listusersessions.Result{Sessions: []shared.Session{first, second}}, nil }), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/internal/users/user-123/sessions", nil) handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.Contains(t, recorder.Body.String(), `"sessions":[`) assert.Contains(t, recorder.Body.String(), `"device_session_id":"device-session-123"`) assert.Contains(t, recorder.Body.String(), `"device_session_id":"device-session-122"`) } func TestListUserSessionsHandlerUnknownUserReturnsEmptyArray(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(func(_ context.Context, input listusersessions.Input) (listusersessions.Result, error) { assert.Equal(t, listusersessions.Input{UserID: "unknown-user"}, input) return listusersessions.Result{Sessions: []shared.Session{}}, nil }), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/internal/users/unknown-user/sessions", nil) handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, `{"sessions":[]}`, recorder.Body.String()) } func TestRevokeDeviceSessionHandlerAlreadyRevoked(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(func(_ context.Context, input revokedevicesession.Input) (revokedevicesession.Result, error) { assert.Equal(t, revokedevicesession.Input{ DeviceSessionID: "device-session-123", ReasonCode: "admin_revoke", ActorType: "system", }, input) return revokedevicesession.Result{ Outcome: "already_revoked", DeviceSessionID: "device-session-123", AffectedSessionCount: 0, }, nil }), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }) recorder := httptest.NewRecorder() request := httptest.NewRequest( http.MethodPost, "/api/v1/internal/sessions/device-session-123/revoke", bytes.NewBufferString(`{"reason_code":"admin_revoke","actor":{"type":"system"}}`), ) request.Header.Set("Content-Type", "application/json") handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, `{"outcome":"already_revoked","device_session_id":"device-session-123","affected_session_count":0}`, recorder.Body.String()) } func TestRevokeAllUserSessionsHandlerNoActiveSessions(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(func(_ context.Context, input revokeallusersessions.Input) (revokeallusersessions.Result, error) { assert.Equal(t, revokeallusersessions.Input{ UserID: "user-123", ReasonCode: "logout_all", ActorType: "system", }, input) return revokeallusersessions.Result{ Outcome: "no_active_sessions", UserID: "user-123", AffectedSessionCount: 0, AffectedDeviceSessionIDs: []string{}, }, nil }), BlockUser: blockUserFunc(unexpectedBlockUser), }) recorder := httptest.NewRecorder() request := httptest.NewRequest( http.MethodPost, "/api/v1/internal/users/user-123/sessions/revoke-all", bytes.NewBufferString(`{"reason_code":"logout_all","actor":{"type":"system"}}`), ) request.Header.Set("Content-Type", "application/json") handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, `{"outcome":"no_active_sessions","user_id":"user-123","affected_session_count":0,"affected_device_session_ids":[]}`, recorder.Body.String()) } func TestBlockUserHandlerSuccessByEmail(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(func(_ context.Context, input blockuser.Input) (blockuser.Result, error) { assert.Equal(t, blockuser.Input{ Email: "pilot@example.com", ReasonCode: "policy_blocked", ActorType: "admin", }, input) return blockuser.Result{ Outcome: "blocked", SubjectKind: blockuser.SubjectKindEmail, SubjectValue: "pilot@example.com", AffectedSessionCount: 0, AffectedDeviceSessionIDs: []string{}, }, nil }), }) recorder := httptest.NewRecorder() request := httptest.NewRequest( http.MethodPost, "/api/v1/internal/user-blocks", bytes.NewBufferString(`{"email":"pilot@example.com","reason_code":"policy_blocked","actor":{"type":"admin"}}`), ) request.Header.Set("Content-Type", "application/json") handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, `{"outcome":"blocked","subject_kind":"email","subject_value":"pilot@example.com","affected_session_count":0,"affected_device_session_ids":[]}`, recorder.Body.String()) } func TestBlockUserHandlerSuccessByUserID(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(func(_ context.Context, input blockuser.Input) (blockuser.Result, error) { assert.Equal(t, blockuser.Input{ UserID: "user-123", ReasonCode: "policy_blocked", ActorType: "admin", }, input) return blockuser.Result{ Outcome: "already_blocked", SubjectKind: blockuser.SubjectKindUserID, SubjectValue: "user-123", AffectedSessionCount: 0, AffectedDeviceSessionIDs: []string{}, }, nil }), }) recorder := httptest.NewRecorder() request := httptest.NewRequest( http.MethodPost, "/api/v1/internal/user-blocks", bytes.NewBufferString(`{"user_id":"user-123","reason_code":"policy_blocked","actor":{"type":"admin"}}`), ) request.Header.Set("Content-Type", "application/json") handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, `{"outcome":"already_blocked","subject_kind":"user_id","subject_value":"user-123","affected_session_count":0,"affected_device_session_ids":[]}`, recorder.Body.String()) } func TestInternalHandlersRejectInvalidPathParams(t *testing.T) { t.Parallel() tests := []struct { name string method string target string body string wantStatus int wantBody string }{ { name: "get session empty device session id", method: http.MethodGet, target: "/api/v1/internal/sessions/%20", wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"device session id must not be empty"}}`, }, { name: "list sessions empty user id", method: http.MethodGet, target: "/api/v1/internal/users/%20/sessions", wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"user id must not be empty"}}`, }, { name: "revoke all empty user id", method: http.MethodPost, target: "/api/v1/internal/users/%20/sessions/revoke-all", body: `{"reason_code":"logout_all","actor":{"type":"system"}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"user id must not be empty"}}`, }, } handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, shared.InvalidRequest("device session id must not be empty") }), ListUserSessions: listUserSessionsFunc(func(context.Context, listusersessions.Input) (listusersessions.Result, error) { return listusersessions.Result{}, shared.InvalidRequest("user id must not be empty") }), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(func(context.Context, revokeallusersessions.Input) (revokeallusersessions.Result, error) { return revokeallusersessions.Result{}, shared.InvalidRequest("user id must not be empty") }), BlockUser: blockUserFunc(unexpectedBlockUser), }) for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() recorder := httptest.NewRecorder() request := httptest.NewRequest(tt.method, tt.target, bytes.NewBufferString(tt.body)) if tt.body != "" { request.Header.Set("Content-Type", "application/json") } handler.ServeHTTP(recorder, request) assert.Equal(t, tt.wantStatus, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, tt.wantBody, recorder.Body.String()) }) } } func TestInternalMutationHandlersRejectInvalidRequests(t *testing.T) { t.Parallel() tests := []struct { name string method string target string body string wantStatus int wantBody string }{ { name: "revoke device session empty body", method: http.MethodPost, target: "/api/v1/internal/sessions/device-session-123/revoke", body: ``, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"request body must not be empty"}}`, }, { name: "revoke device session malformed json", method: http.MethodPost, target: "/api/v1/internal/sessions/device-session-123/revoke", body: `{"reason_code":`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"request body contains malformed JSON"}}`, }, { name: "revoke device session multiple objects", method: http.MethodPost, target: "/api/v1/internal/sessions/device-session-123/revoke", body: `{"reason_code":"admin_revoke","actor":{"type":"system"}}{"reason_code":"admin_revoke","actor":{"type":"system"}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"request body must contain a single JSON object"}}`, }, { name: "revoke device session unknown field", method: http.MethodPost, target: "/api/v1/internal/sessions/device-session-123/revoke", body: `{"reason_code":"admin_revoke","actor":{"type":"system"},"extra":true}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"request body contains unknown field \"extra\""}}`, }, { name: "revoke device session invalid json type", method: http.MethodPost, target: "/api/v1/internal/sessions/device-session-123/revoke", body: `{"reason_code":123,"actor":{"type":"system"}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"request body contains an invalid value for \"reason_code\""}}`, }, { name: "revoke all missing reason code", method: http.MethodPost, target: "/api/v1/internal/users/user-123/sessions/revoke-all", body: `{"reason_code":" ","actor":{"type":"system"}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"reason_code must not be empty"}}`, }, { name: "block user missing actor type", method: http.MethodPost, target: "/api/v1/internal/user-blocks", body: `{"email":"pilot@example.com","reason_code":"policy_blocked","actor":{"type":" "}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"actor.type must not be empty"}}`, }, { name: "block user missing subject", method: http.MethodPost, target: "/api/v1/internal/user-blocks", body: `{"reason_code":"policy_blocked","actor":{"type":"system"}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"exactly one of user_id or email must be provided"}}`, }, { name: "block user conflicting subjects", method: http.MethodPost, target: "/api/v1/internal/user-blocks", body: `{"user_id":"user-123","email":"pilot@example.com","reason_code":"policy_blocked","actor":{"type":"system"}}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"exactly one of user_id or email must be provided"}}`, }, } handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }) for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() recorder := httptest.NewRecorder() request := httptest.NewRequest(tt.method, tt.target, bytes.NewBufferString(tt.body)) if tt.body != "" { request.Header.Set("Content-Type", "application/json") } handler.ServeHTTP(recorder, request) assert.Equal(t, tt.wantStatus, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, tt.wantBody, recorder.Body.String()) }) } } func TestInternalHandlersMapServiceErrors(t *testing.T) { t.Parallel() tests := []struct { name string method string target string body string deps Dependencies wantStatus int wantBody string }{ { name: "get session not found", method: http.MethodGet, target: "/api/v1/internal/sessions/missing", deps: Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, shared.SessionNotFound() }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }, wantStatus: http.StatusNotFound, wantBody: `{"error":{"code":"session_not_found","message":"session not found"}}`, }, { name: "revoke all subject not found", method: http.MethodPost, target: "/api/v1/internal/users/missing/sessions/revoke-all", body: `{"reason_code":"logout_all","actor":{"type":"system"}}`, deps: Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(func(context.Context, revokeallusersessions.Input) (revokeallusersessions.Result, error) { return revokeallusersessions.Result{}, shared.SubjectNotFound() }), BlockUser: blockUserFunc(unexpectedBlockUser), }, wantStatus: http.StatusNotFound, wantBody: `{"error":{"code":"subject_not_found","message":"subject not found"}}`, }, { name: "service unavailable", method: http.MethodGet, target: "/api/v1/internal/sessions/device-session-123", deps: Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, shared.ServiceUnavailable(errors.New("redis timeout")) }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }, wantStatus: http.StatusServiceUnavailable, wantBody: `{"error":{"code":"service_unavailable","message":"service is unavailable"}}`, }, { name: "internal error", method: http.MethodGet, target: "/api/v1/internal/sessions/device-session-123", deps: Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, shared.InternalError(errors.New("broken invariant")) }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }, wantStatus: http.StatusInternalServerError, wantBody: `{"error":{"code":"internal_error","message":"internal server error"}}`, }, { name: "unexpected error hidden", method: http.MethodGet, target: "/api/v1/internal/sessions/device-session-123", deps: Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, errors.New("boom") }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }, wantStatus: http.StatusInternalServerError, wantBody: `{"error":{"code":"internal_error","message":"internal server error"}}`, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), tt.deps) recorder := httptest.NewRecorder() request := httptest.NewRequest(tt.method, tt.target, bytes.NewBufferString(tt.body)) if tt.body != "" { request.Header.Set("Content-Type", "application/json") } handler.ServeHTTP(recorder, request) assert.Equal(t, tt.wantStatus, recorder.Code) assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type")) assert.JSONEq(t, tt.wantBody, recorder.Body.String()) }) } } func TestInternalHandlerTimeoutMapsToServiceUnavailable(t *testing.T) { t.Parallel() cfg := DefaultConfig() cfg.RequestTimeout = 5 * time.Millisecond handler := mustNewHandler(t, cfg, Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, context.DeadlineExceeded }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }) recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/v1/internal/sessions/device-session-123", nil) handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusServiceUnavailable, recorder.Code) assert.JSONEq(t, `{"error":{"code":"service_unavailable","message":"service is unavailable"}}`, recorder.Body.String()) } func TestInternalHandlersRejectInvalidSuccessPayloads(t *testing.T) { t.Parallel() tests := []struct { name string method string target string body string deps Dependencies }{ { name: "get session malformed response", method: http.MethodGet, target: "/api/v1/internal/sessions/device-session-123", deps: Dependencies{ GetSession: getSessionFunc(func(context.Context, getsession.Input) (getsession.Result, error) { dto := validSessionDTO() dto.DeviceSessionID = "" return getsession.Result{Session: dto}, nil }), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(unexpectedBlockUser), }, }, { name: "revoke all malformed response", method: http.MethodPost, target: "/api/v1/internal/users/user-123/sessions/revoke-all", body: `{"reason_code":"logout_all","actor":{"type":"system"}}`, deps: Dependencies{ GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(func(context.Context, revokeallusersessions.Input) (revokeallusersessions.Result, error) { return revokeallusersessions.Result{ Outcome: "revoked", UserID: "user-123", AffectedSessionCount: 2, AffectedDeviceSessionIDs: []string{"device-session-1"}, }, nil }), BlockUser: blockUserFunc(unexpectedBlockUser), }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() handler := mustNewHandler(t, DefaultConfig(), tt.deps) recorder := httptest.NewRecorder() request := httptest.NewRequest(tt.method, tt.target, bytes.NewBufferString(tt.body)) if tt.body != "" { request.Header.Set("Content-Type", "application/json") } handler.ServeHTTP(recorder, request) assert.Equal(t, http.StatusInternalServerError, recorder.Code) assert.JSONEq(t, `{"error":{"code":"internal_error","message":"internal server error"}}`, recorder.Body.String()) }) } } func TestInternalHandlerLogsDoNotContainSensitiveFields(t *testing.T) { t.Parallel() logger, buffer := newObservedLogger() handler := mustNewHandler(t, DefaultConfig(), Dependencies{ Logger: logger, GetSession: getSessionFunc(unexpectedGetSession), ListUserSessions: listUserSessionsFunc(unexpectedListUserSessions), RevokeDeviceSession: revokeDeviceSessionFunc(unexpectedRevokeDeviceSession), RevokeAllUserSessions: revokeAllUserSessionsFunc(unexpectedRevokeAllUserSessions), BlockUser: blockUserFunc(func(context.Context, blockuser.Input) (blockuser.Result, error) { return blockuser.Result{ Outcome: "blocked", SubjectKind: blockuser.SubjectKindEmail, SubjectValue: "pilot@example.com", AffectedSessionCount: 0, AffectedDeviceSessionIDs: []string{}, }, nil }), }) recorder := httptest.NewRecorder() request := httptest.NewRequest( http.MethodPost, "/api/v1/internal/user-blocks", bytes.NewBufferString(`{"email":"pilot@example.com","reason_code":"policy_blocked","actor":{"type":"admin","id":"admin-1"}}`), ) request.Header.Set("Content-Type", "application/json") handler.ServeHTTP(recorder, request) require.Equal(t, http.StatusOK, recorder.Code) logOutput := buffer.String() assert.NotContains(t, logOutput, "pilot@example.com") assert.NotContains(t, logOutput, "admin-1") assert.NotContains(t, logOutput, "reason_code") } func mustNewHandler(t *testing.T, cfg Config, deps Dependencies) http.Handler { t.Helper() handler, err := newHandlerWithConfig(cfg, deps) require.NoError(t, err) return handler } type getSessionFunc func(ctx context.Context, input getsession.Input) (getsession.Result, error) func (f getSessionFunc) Execute(ctx context.Context, input getsession.Input) (getsession.Result, error) { return f(ctx, input) } type listUserSessionsFunc func(ctx context.Context, input listusersessions.Input) (listusersessions.Result, error) func (f listUserSessionsFunc) Execute(ctx context.Context, input listusersessions.Input) (listusersessions.Result, error) { return f(ctx, input) } type revokeDeviceSessionFunc func(ctx context.Context, input revokedevicesession.Input) (revokedevicesession.Result, error) func (f revokeDeviceSessionFunc) Execute(ctx context.Context, input revokedevicesession.Input) (revokedevicesession.Result, error) { return f(ctx, input) } type revokeAllUserSessionsFunc func(ctx context.Context, input revokeallusersessions.Input) (revokeallusersessions.Result, error) func (f revokeAllUserSessionsFunc) Execute(ctx context.Context, input revokeallusersessions.Input) (revokeallusersessions.Result, error) { return f(ctx, input) } type blockUserFunc func(ctx context.Context, input blockuser.Input) (blockuser.Result, error) func (f blockUserFunc) Execute(ctx context.Context, input blockuser.Input) (blockuser.Result, error) { return f(ctx, input) } func validSessionDTO() shared.Session { return shared.Session{ DeviceSessionID: "device-session-123", UserID: "user-123", ClientPublicKey: "public-key-material", Status: "active", CreatedAt: "2026-04-05T12:00:00Z", } } func validRevokedSessionDTO() shared.Session { dto := validSessionDTO() dto.Status = "revoked" revokedAt := "2026-04-05T12:01:00Z" reasonCode := "admin_revoke" actorType := "admin" actorID := "admin-1" dto.RevokedAt = &revokedAt dto.RevokeReasonCode = &reasonCode dto.RevokeActorType = &actorType dto.RevokeActorID = &actorID return dto } func newObservedLogger() (*zap.Logger, *bytes.Buffer) { buffer := &bytes.Buffer{} encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.TimeKey = "" core := zapcore.NewCore( zapcore.NewJSONEncoder(encoderConfig), zapcore.AddSync(buffer), zap.DebugLevel, ) return zap.New(core), buffer } func unexpectedGetSession(context.Context, getsession.Input) (getsession.Result, error) { return getsession.Result{}, errors.New("unexpected call") } func unexpectedListUserSessions(context.Context, listusersessions.Input) (listusersessions.Result, error) { return listusersessions.Result{}, errors.New("unexpected call") } func unexpectedRevokeDeviceSession(context.Context, revokedevicesession.Input) (revokedevicesession.Result, error) { return revokedevicesession.Result{}, errors.New("unexpected call") } func unexpectedRevokeAllUserSessions(context.Context, revokeallusersessions.Input) (revokeallusersessions.Result, error) { return revokeallusersessions.Result{}, errors.New("unexpected call") } func unexpectedBlockUser(context.Context, blockuser.Input) (blockuser.Result, error) { return blockuser.Result{}, errors.New("unexpected call") }