feat: authsession service

This commit is contained in:
Ilia Denisov
2026-04-08 16:23:07 +02:00
committed by GitHub
parent 28f04916af
commit 86a68ed9d0
174 changed files with 31732 additions and 112 deletions
@@ -0,0 +1,286 @@
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="