287 lines
11 KiB
Go
287 lines
11 KiB
Go
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="
|