feat: authsession service
This commit is contained in:
@@ -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="
|
||||
Reference in New Issue
Block a user