Files
galaxy-game/authsession/internal/api/internalhttp/handler_test.go
T
2026-04-08 16:23:07 +02:00

785 lines
30 KiB
Go

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")
}