package internalhttp import ( "bytes" "context" "crypto/ed25519" "encoding/base64" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "time" "galaxy/authsession/internal/adapters/userservice" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/domain/devicesession" "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/testkit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestInternalHTTPEndToEndGetSession(t *testing.T) { t.Parallel() app := newEndToEndApp(t) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-1", "user-1", testClientPublicKey(t, validClientPublicKey), time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)))) server := httptest.NewServer(app.handler) defer server.Close() response := getJSON(t, server.URL+"/api/v1/internal/sessions/device-session-1") assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"session":{"device_session_id":"device-session-1","user_id":"user-1","client_public_key":"`+validClientPublicKey+`","status":"active","created_at":"2026-04-05T12:00:00Z"}}`, response.Body) } func TestInternalHTTPEndToEndListUserSessions(t *testing.T) { t.Parallel() app := newEndToEndApp(t) key := testClientPublicKey(t, validClientPublicKey) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-1", "user-1", key, time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)))) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-2", "user-1", key, time.Date(2026, 4, 5, 12, 1, 0, 0, time.UTC)))) server := httptest.NewServer(app.handler) defer server.Close() response := getJSON(t, server.URL+"/api/v1/internal/users/user-1/sessions") assert.Equal(t, http.StatusOK, response.StatusCode) assert.Contains(t, response.Body, `"device_session_id":"device-session-2"`) assert.Contains(t, response.Body, `"device_session_id":"device-session-1"`) assert.Less(t, bytes.Index([]byte(response.Body), []byte(`"device_session_id":"device-session-2"`)), bytes.Index([]byte(response.Body), []byte(`"device_session_id":"device-session-1"`))) } func TestInternalHTTPEndToEndListUserSessionsUnknownUserReturnsEmptyArray(t *testing.T) { t.Parallel() app := newEndToEndApp(t) server := httptest.NewServer(app.handler) defer server.Close() response := getJSON(t, server.URL+"/api/v1/internal/users/unknown-user/sessions") assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"sessions":[]}`, response.Body) } func TestInternalHTTPEndToEndGetSessionNotFound(t *testing.T) { t.Parallel() app := newEndToEndApp(t) server := httptest.NewServer(app.handler) defer server.Close() response := getJSON(t, server.URL+"/api/v1/internal/sessions/missing-session") assert.Equal(t, http.StatusNotFound, response.StatusCode) assert.JSONEq(t, `{"error":{"code":"session_not_found","message":"session not found"}}`, response.Body) } func TestInternalHTTPEndToEndRevokeDeviceSession(t *testing.T) { t.Parallel() app := newEndToEndApp(t) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-1", "user-1", testClientPublicKey(t, validClientPublicKey), time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)))) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/sessions/device-session-1/revoke", `{"reason_code":"admin_revoke","actor":{"type":"system"}}`) assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"outcome":"revoked","device_session_id":"device-session-1","affected_session_count":1}`, response.Body) } func TestInternalHTTPEndToEndRevokeAllUserSessions(t *testing.T) { t.Parallel() app := newEndToEndApp(t) require.NoError(t, app.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1"))) key := testClientPublicKey(t, validClientPublicKey) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-1", "user-1", key, time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)))) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-2", "user-1", key, time.Date(2026, 4, 5, 12, 1, 0, 0, time.UTC)))) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/users/user-1/sessions/revoke-all", `{"reason_code":"logout_all","actor":{"type":"system"}}`) assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"outcome":"revoked","user_id":"user-1","affected_session_count":2,"affected_device_session_ids":["device-session-2","device-session-1"]}`, response.Body) } func TestInternalHTTPEndToEndRevokeAllUserSessionsNoActiveSessions(t *testing.T) { t.Parallel() app := newEndToEndApp(t) require.NoError(t, app.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1"))) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/users/user-1/sessions/revoke-all", `{"reason_code":"logout_all","actor":{"type":"system"}}`) assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"outcome":"no_active_sessions","user_id":"user-1","affected_session_count":0,"affected_device_session_ids":[]}`, response.Body) } func TestInternalHTTPEndToEndRevokeAllUserSessionsUnknownUser(t *testing.T) { t.Parallel() app := newEndToEndApp(t) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/users/missing-user/sessions/revoke-all", `{"reason_code":"logout_all","actor":{"type":"system"}}`) assert.Equal(t, http.StatusNotFound, response.StatusCode) assert.JSONEq(t, `{"error":{"code":"subject_not_found","message":"subject not found"}}`, response.Body) } func TestInternalHTTPEndToEndBlockUserByEmail(t *testing.T) { t.Parallel() app := newEndToEndApp(t) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/user-blocks", `{"email":"pilot@example.com","reason_code":"policy_blocked","actor":{"type":"admin"}}`) assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"outcome":"blocked","subject_kind":"email","subject_value":"pilot@example.com","affected_session_count":0,"affected_device_session_ids":[]}`, response.Body) } func TestInternalHTTPEndToEndBlockUserByUserID(t *testing.T) { t.Parallel() app := newEndToEndApp(t) require.NoError(t, app.userDirectory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1"))) require.NoError(t, app.sessionStore.Create(context.Background(), activeSession("device-session-1", "user-1", testClientPublicKey(t, validClientPublicKey), time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)))) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/user-blocks", `{"user_id":"user-1","reason_code":"policy_blocked","actor":{"type":"admin"}}`) assert.Equal(t, http.StatusOK, response.StatusCode) assert.JSONEq(t, `{"outcome":"blocked","subject_kind":"user_id","subject_value":"user-1","affected_session_count":1,"affected_device_session_ids":["device-session-1"]}`, response.Body) } func TestInternalHTTPEndToEndBlockUserUnknownUserID(t *testing.T) { t.Parallel() app := newEndToEndApp(t) server := httptest.NewServer(app.handler) defer server.Close() response := postJSON(t, server.URL+"/api/v1/internal/user-blocks", `{"user_id":"missing-user","reason_code":"policy_blocked","actor":{"type":"admin"}}`) assert.Equal(t, http.StatusNotFound, response.StatusCode) assert.JSONEq(t, `{"error":{"code":"subject_not_found","message":"subject not found"}}`, response.Body) } type endToEndApp struct { handler http.Handler sessionStore *testkit.InMemorySessionStore userDirectory *userservice.StubDirectory } func newEndToEndApp(t *testing.T) endToEndApp { t.Helper() sessionStore := &testkit.InMemorySessionStore{} userDirectory := &userservice.StubDirectory{} publisher := &testkit.RecordingProjectionPublisher{} clock := testkit.FixedClock{Time: time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)} getSessionService, err := getsession.New(sessionStore) require.NoError(t, err) listUserSessionsService, err := listusersessions.New(sessionStore) require.NoError(t, err) revokeDeviceSessionService, err := revokedevicesession.New(sessionStore, publisher, clock) require.NoError(t, err) revokeAllUserSessionsService, err := revokeallusersessions.New(sessionStore, userDirectory, publisher, clock) require.NoError(t, err) blockUserService, err := blockuser.New(userDirectory, sessionStore, publisher, clock) require.NoError(t, err) handler := mustNewHandler(t, DefaultConfig(), Dependencies{ GetSession: getSessionService, ListUserSessions: listUserSessionsService, RevokeDeviceSession: revokeDeviceSessionService, RevokeAllUserSessions: revokeAllUserSessionsService, BlockUser: blockUserService, }) return endToEndApp{ handler: handler, sessionStore: sessionStore, userDirectory: userDirectory, } } type httpResponse struct { StatusCode int Body string } func getJSON(t *testing.T, url string) httpResponse { t.Helper() response, err := http.Get(url) require.NoError(t, err) defer response.Body.Close() payload, err := io.ReadAll(response.Body) require.NoError(t, err) return httpResponse{StatusCode: response.StatusCode, Body: string(payload)} } func postJSON(t *testing.T, url string, body string) httpResponse { t.Helper() response, err := http.Post(url, "application/json", bytes.NewBufferString(body)) require.NoError(t, err) defer response.Body.Close() payload, err := io.ReadAll(response.Body) require.NoError(t, err) return httpResponse{StatusCode: response.StatusCode, Body: string(payload)} } func postJSONValue(t *testing.T, url string, value any) httpResponse { t.Helper() body, err := json.Marshal(value) require.NoError(t, err) return postJSON(t, url, string(body)) } func activeSession(id string, userID string, key common.ClientPublicKey, createdAt time.Time) devicesession.Session { return devicesession.Session{ ID: common.DeviceSessionID(id), UserID: common.UserID(userID), ClientPublicKey: key, Status: devicesession.StatusActive, CreatedAt: createdAt, } } func testClientPublicKey(t *testing.T, encoded string) common.ClientPublicKey { t.Helper() decoded, err := base64.StdEncoding.DecodeString(encoded) require.NoError(t, err) key, err := common.NewClientPublicKey(ed25519.PublicKey(decoded)) require.NoError(t, err) return key } const validClientPublicKey = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="