feat: user service
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user