package internalhttp import ( "bytes" "context" "net/http" "net/http/httptest" "testing" "time" "galaxy/user/internal/service/accountview" "galaxy/user/internal/service/adminusers" "galaxy/user/internal/service/shared" "github.com/stretchr/testify/require" ) func TestAdminReadHandlersSuccessCases(t *testing.T) { t.Parallel() handler := mustNewHandler(t, Dependencies{ GetUserByID: getUserByIDFunc(func(_ context.Context, input adminusers.GetUserByIDInput) (adminusers.LookupResult, error) { require.Equal(t, "user-123", input.UserID) return adminusers.LookupResult{User: sampleAccountView()}, nil }), GetUserByEmail: getUserByEmailFunc(func(_ context.Context, input adminusers.GetUserByEmailInput) (adminusers.LookupResult, error) { require.Equal(t, "pilot@example.com", input.Email) return adminusers.LookupResult{User: sampleAccountView()}, nil }), GetUserByRaceName: getUserByRaceNameFunc(func(_ context.Context, input adminusers.GetUserByRaceNameInput) (adminusers.LookupResult, error) { require.Equal(t, "Pilot Nova", input.RaceName) return adminusers.LookupResult{User: sampleAccountView()}, nil }), ListUsers: listUsersFunc(func(_ context.Context, input adminusers.ListUsersInput) (adminusers.ListUsersResult, error) { require.Equal(t, 2, input.PageSize) require.Equal(t, "cursor-1", input.PageToken) require.Equal(t, "paid", input.PaidState) require.Equal(t, "DE", input.DeclaredCountry) require.Equal(t, "login_block", input.SanctionCode) require.Equal(t, "max_owned_private_games", input.LimitCode) require.NotNil(t, input.PaidExpiresBefore) require.NotNil(t, input.PaidExpiresAfter) require.NotNil(t, input.CanLogin) require.NotNil(t, input.CanCreatePrivateGame) require.NotNil(t, input.CanJoinGame) require.False(t, *input.CanLogin) require.True(t, *input.CanCreatePrivateGame) require.True(t, *input.CanJoinGame) require.Equal(t, time.Date(2026, time.April, 10, 12, 0, 0, 0, time.UTC), input.PaidExpiresBefore.UTC()) require.Equal(t, time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC), input.PaidExpiresAfter.UTC()) other := sampleAccountView() other.UserID = "user-234" other.Email = "second@example.com" other.RaceName = "Second Pilot" return adminusers.ListUsersResult{ Items: []accountview.AccountView{sampleAccountView(), other}, NextPageToken: "cursor-2", }, nil }), }) tests := []struct { name string method string path string body string wantStatus int wantBody string }{ { name: "get user by id", method: http.MethodGet, path: "/api/v1/internal/users/user-123", wantStatus: http.StatusOK, wantBody: `{"user":{"user_id":"user-123","email":"pilot@example.com","race_name":"Pilot Nova","preferred_language":"en","time_zone":"Europe/Kaliningrad","declared_country":"DE","entitlement":{"plan_code":"free","is_paid":false,"source":"auth_registration","actor":{"type":"service","id":"user-service"},"reason_code":"initial_free_entitlement","starts_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[],"active_limits":[],"created_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"}}`, }, { name: "get user by email", method: http.MethodPost, path: "/api/v1/internal/user-lookups/by-email", body: `{"email":"pilot@example.com"}`, wantStatus: http.StatusOK, wantBody: `{"user":{"user_id":"user-123","email":"pilot@example.com","race_name":"Pilot Nova","preferred_language":"en","time_zone":"Europe/Kaliningrad","declared_country":"DE","entitlement":{"plan_code":"free","is_paid":false,"source":"auth_registration","actor":{"type":"service","id":"user-service"},"reason_code":"initial_free_entitlement","starts_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[],"active_limits":[],"created_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"}}`, }, { name: "get user by race name", method: http.MethodPost, path: "/api/v1/internal/user-lookups/by-race-name", body: `{"race_name":"Pilot Nova"}`, wantStatus: http.StatusOK, wantBody: `{"user":{"user_id":"user-123","email":"pilot@example.com","race_name":"Pilot Nova","preferred_language":"en","time_zone":"Europe/Kaliningrad","declared_country":"DE","entitlement":{"plan_code":"free","is_paid":false,"source":"auth_registration","actor":{"type":"service","id":"user-service"},"reason_code":"initial_free_entitlement","starts_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[],"active_limits":[],"created_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"}}`, }, { name: "list users", method: http.MethodGet, path: "/api/v1/internal/users?page_size=2&page_token=cursor-1&paid_state=paid&paid_expires_before=2026-04-10T12:00:00Z&paid_expires_after=2026-04-01T12:00:00Z&declared_country=DE&sanction_code=login_block&limit_code=max_owned_private_games&can_login=false&can_create_private_game=true&can_join_game=true", wantStatus: http.StatusOK, wantBody: `{"items":[{"user_id":"user-123","email":"pilot@example.com","race_name":"Pilot Nova","preferred_language":"en","time_zone":"Europe/Kaliningrad","declared_country":"DE","entitlement":{"plan_code":"free","is_paid":false,"source":"auth_registration","actor":{"type":"service","id":"user-service"},"reason_code":"initial_free_entitlement","starts_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[],"active_limits":[],"created_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},{"user_id":"user-234","email":"second@example.com","race_name":"Second Pilot","preferred_language":"en","time_zone":"Europe/Kaliningrad","declared_country":"DE","entitlement":{"plan_code":"free","is_paid":false,"source":"auth_registration","actor":{"type":"service","id":"user-service"},"reason_code":"initial_free_entitlement","starts_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[],"active_limits":[],"created_at":"2026-04-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"}],"next_page_token":"cursor-2"}`, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() var body *bytes.Buffer if tt.body != "" { body = bytes.NewBufferString(tt.body) } else { body = &bytes.Buffer{} } request := httptest.NewRequest(tt.method, tt.path, body) if tt.body != "" { request.Header.Set("Content-Type", "application/json") } recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) require.Equal(t, tt.wantStatus, recorder.Code) assertJSONEq(t, recorder.Body.String(), tt.wantBody) }) } } func TestAdminReadHandlersErrorCases(t *testing.T) { t.Parallel() handler := mustNewHandler(t, Dependencies{ GetUserByID: getUserByIDFunc(func(context.Context, adminusers.GetUserByIDInput) (adminusers.LookupResult, error) { return adminusers.LookupResult{}, shared.SubjectNotFound() }), GetUserByEmail: getUserByEmailFunc(func(context.Context, adminusers.GetUserByEmailInput) (adminusers.LookupResult, error) { return adminusers.LookupResult{}, shared.SubjectNotFound() }), GetUserByRaceName: getUserByRaceNameFunc(func(context.Context, adminusers.GetUserByRaceNameInput) (adminusers.LookupResult, error) { return adminusers.LookupResult{}, shared.SubjectNotFound() }), ListUsers: listUsersFunc(func(context.Context, adminusers.ListUsersInput) (adminusers.ListUsersResult, error) { return adminusers.ListUsersResult{}, shared.InvalidRequest("page_token is invalid or does not match current filters") }), }) tests := []struct { name string method string path string body string wantStatus int wantBody string }{ { name: "get user by id not found", method: http.MethodGet, path: "/api/v1/internal/users/user-missing", wantStatus: http.StatusNotFound, wantBody: `{"error":{"code":"subject_not_found","message":"subject not found"}}`, }, { name: "get user by email malformed json", method: http.MethodPost, path: "/api/v1/internal/user-lookups/by-email", body: `{"email":"pilot@example.com","extra":true}`, wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"request body contains unknown field \"extra\""}}`, }, { name: "get user by race name not found", method: http.MethodPost, path: "/api/v1/internal/user-lookups/by-race-name", body: `{"race_name":"Missing Pilot"}`, wantStatus: http.StatusNotFound, wantBody: `{"error":{"code":"subject_not_found","message":"subject not found"}}`, }, { name: "list users invalid page size", method: http.MethodGet, path: "/api/v1/internal/users?page_size=201", wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"page_size must be between 1 and 200"}}`, }, { name: "list users invalid timestamp", method: http.MethodGet, path: "/api/v1/internal/users?paid_expires_before=not-a-time", wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"paid_expires_before must be a valid RFC 3339 timestamp"}}`, }, { name: "list users invalid boolean", method: http.MethodGet, path: "/api/v1/internal/users?can_login=maybe", wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"can_login must be a valid boolean"}}`, }, { name: "list users invalid page token", method: http.MethodGet, path: "/api/v1/internal/users?page_token=cursor-1", wantStatus: http.StatusBadRequest, wantBody: `{"error":{"code":"invalid_request","message":"page_token is invalid or does not match current filters"}}`, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() var body *bytes.Buffer if tt.body != "" { body = bytes.NewBufferString(tt.body) } else { body = &bytes.Buffer{} } request := httptest.NewRequest(tt.method, tt.path, body) if tt.body != "" { request.Header.Set("Content-Type", "application/json") } recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) require.Equal(t, tt.wantStatus, recorder.Code) assertJSONEq(t, recorder.Body.String(), tt.wantBody) }) } }