feat: game lobby service
This commit is contained in:
@@ -8,11 +8,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/user/internal/domain/account"
|
||||
"galaxy/user/internal/domain/common"
|
||||
"galaxy/user/internal/domain/entitlement"
|
||||
"galaxy/user/internal/domain/policy"
|
||||
"galaxy/user/internal/ports"
|
||||
"galaxy/user/internal/service/accountdeletion"
|
||||
"galaxy/user/internal/service/adminusers"
|
||||
"galaxy/user/internal/service/authdirectory"
|
||||
"galaxy/user/internal/service/entitlementsvc"
|
||||
@@ -56,9 +56,9 @@ func TestAuthFacingHandlersSuccessCases(t *testing.T) {
|
||||
}),
|
||||
UpdateMyProfile: updateMyProfileFunc(func(_ context.Context, input selfservice.UpdateMyProfileInput) (selfservice.UpdateMyProfileResult, error) {
|
||||
require.Equal(t, "user-123", input.UserID)
|
||||
require.Equal(t, "Nova Prime", input.RaceName)
|
||||
require.Equal(t, "NovaPrime", input.DisplayName)
|
||||
accountView := sampleAccountView()
|
||||
accountView.RaceName = input.RaceName
|
||||
accountView.DisplayName = input.DisplayName
|
||||
return selfservice.UpdateMyProfileResult{Account: accountView}, nil
|
||||
}),
|
||||
UpdateMySettings: updateMySettingsFunc(func(_ context.Context, input selfservice.UpdateMySettingsInput) (selfservice.UpdateMySettingsResult, error) {
|
||||
@@ -211,6 +211,16 @@ func TestAuthFacingHandlersSuccessCases(t *testing.T) {
|
||||
require.Equal(t, "manual_remove", input.ReasonCode)
|
||||
return policysvc.LimitCommandResult{UserID: "user-123", ActiveLimits: []policysvc.ActiveLimitView{}}, nil
|
||||
}),
|
||||
DeleteUser: deleteUserFunc(func(_ context.Context, input accountdeletion.Input) (accountdeletion.Result, error) {
|
||||
require.Equal(t, "user-123", input.UserID)
|
||||
require.Equal(t, "user_right_to_be_forgotten", input.ReasonCode)
|
||||
require.Equal(t, "admin", input.Actor.Type)
|
||||
require.Equal(t, "admin-1", input.Actor.ID)
|
||||
return accountdeletion.Result{
|
||||
UserID: "user-123",
|
||||
DeletedAt: time.Date(2026, time.April, 24, 12, 0, 0, 0, time.UTC),
|
||||
}, nil
|
||||
}),
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
@@ -265,15 +275,15 @@ func TestAuthFacingHandlersSuccessCases(t *testing.T) {
|
||||
method: http.MethodGet,
|
||||
path: "/api/v1/internal/users/user-123/account",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"account":{"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"}}`,
|
||||
wantBody: `{"account":{"user_id":"user-123","email":"pilot@example.com","user_name":"player-abcdefgh","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: "update my profile",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/internal/users/user-123/profile",
|
||||
body: `{"race_name":"Nova Prime"}`,
|
||||
body: `{"display_name":"NovaPrime"}`,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"account":{"user_id":"user-123","email":"pilot@example.com","race_name":"Nova Prime","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"}}`,
|
||||
wantBody: `{"account":{"user_id":"user-123","email":"pilot@example.com","user_name":"player-abcdefgh","display_name":"NovaPrime","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: "update my settings",
|
||||
@@ -281,14 +291,14 @@ func TestAuthFacingHandlersSuccessCases(t *testing.T) {
|
||||
path: "/api/v1/internal/users/user-123/settings",
|
||||
body: `{"preferred_language":"en-US","time_zone":"UTC"}`,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"account":{"user_id":"user-123","email":"pilot@example.com","race_name":"Pilot Nova","preferred_language":"en-US","time_zone":"UTC","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"}}`,
|
||||
wantBody: `{"account":{"user_id":"user-123","email":"pilot@example.com","user_name":"player-abcdefgh","preferred_language":"en-US","time_zone":"UTC","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 eligibility",
|
||||
method: http.MethodGet,
|
||||
path: "/api/v1/internal/users/user-123/eligibility",
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"exists":true,"user_id":"user-123","entitlement":{"plan_code":"paid_monthly","is_paid":true,"source":"billing","actor":{"type":"billing","id":"invoice-1"},"reason_code":"renewal","starts_at":"2026-04-09T10:00:00Z","ends_at":"2026-05-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[{"sanction_code":"private_game_create_block","scope":"lobby","reason_code":"manual_block","actor":{"type":"admin","id":"admin-1"},"applied_at":"2026-04-09T10:00:00Z","expires_at":"2026-05-09T10:00:00Z"}],"effective_limits":[{"limit_code":"max_owned_private_games","value":3},{"limit_code":"max_pending_public_applications","value":10},{"limit_code":"max_active_game_memberships","value":10}],"markers":{"can_login":true,"can_create_private_game":false,"can_manage_private_game":true,"can_join_game":true,"can_update_profile":true}}`,
|
||||
wantBody: `{"exists":true,"user_id":"user-123","entitlement":{"plan_code":"paid_monthly","is_paid":true,"source":"billing","actor":{"type":"billing","id":"invoice-1"},"reason_code":"renewal","starts_at":"2026-04-09T10:00:00Z","ends_at":"2026-05-09T10:00:00Z","updated_at":"2026-04-09T10:00:00Z"},"active_sanctions":[{"sanction_code":"private_game_create_block","scope":"lobby","reason_code":"manual_block","actor":{"type":"admin","id":"admin-1"},"applied_at":"2026-04-09T10:00:00Z","expires_at":"2026-05-09T10:00:00Z"}],"effective_limits":[{"limit_code":"max_owned_private_games","value":3},{"limit_code":"max_pending_public_applications","value":10},{"limit_code":"max_active_game_memberships","value":10},{"limit_code":"max_registered_race_names","value":2}],"markers":{"can_login":true,"can_create_private_game":false,"can_manage_private_game":true,"can_join_game":true,"can_update_profile":true}}`,
|
||||
},
|
||||
{
|
||||
name: "get user eligibility not found snapshot",
|
||||
@@ -369,6 +379,14 @@ func TestAuthFacingHandlersSuccessCases(t *testing.T) {
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"user_id":"user-123","active_limits":[]}`,
|
||||
},
|
||||
{
|
||||
name: "delete user",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/internal/users/user-123/delete",
|
||||
body: `{"reason_code":"user_right_to_be_forgotten","actor":{"type":"admin","id":"admin-1"}}`,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `{"user_id":"user-123","deleted_at":"2026-04-24T12:00:00Z"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -659,7 +677,7 @@ func TestSelfServiceHandlersRejectUnknownFieldsAndProjectErrors(t *testing.T) {
|
||||
name: "update my profile conflict",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/internal/users/user-123/profile",
|
||||
body: `{"race_name":"Taken Name"}`,
|
||||
body: `{"display_name":"TakenName"}`,
|
||||
wantStatus: http.StatusConflict,
|
||||
wantBody: `{"error":{"code":"conflict","message":"request conflicts with current state"}}`,
|
||||
},
|
||||
@@ -667,7 +685,7 @@ func TestSelfServiceHandlersRejectUnknownFieldsAndProjectErrors(t *testing.T) {
|
||||
name: "update my profile rejects email field",
|
||||
method: http.MethodPost,
|
||||
path: "/api/v1/internal/users/user-123/profile",
|
||||
body: `{"race_name":"Nova Prime","email":"pilot@example.com"}`,
|
||||
body: `{"display_name":"NovaPrime","email":"pilot@example.com"}`,
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantBody: `{"error":{"code":"invalid_request","message":"request body contains unknown field \"email\""}}`,
|
||||
},
|
||||
@@ -776,9 +794,9 @@ func TestEnsureByEmailHandlerRejectsSemanticRegistrationContext(t *testing.T) {
|
||||
|
||||
ensurer, err := authdirectory.NewEnsurer(handlerTestStore{}, handlerTestClock{now: time.Unix(1_775_240_000, 0).UTC()}, handlerTestIDGenerator{
|
||||
userID: common.UserID("user-created"),
|
||||
raceName: common.RaceName("player-test123"),
|
||||
userName: common.UserName("player-test123"),
|
||||
entitlementRecordID: entitlement.EntitlementRecordID("entitlement-created"),
|
||||
}, handlerTestRaceNamePolicy{})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := mustNewHandler(t, Dependencies{
|
||||
@@ -937,8 +955,8 @@ func mustNewHandler(t *testing.T, deps Dependencies) http.Handler {
|
||||
return adminusers.LookupResult{}, nil
|
||||
})
|
||||
}
|
||||
if deps.GetUserByRaceName == nil {
|
||||
deps.GetUserByRaceName = getUserByRaceNameFunc(func(context.Context, adminusers.GetUserByRaceNameInput) (adminusers.LookupResult, error) {
|
||||
if deps.GetUserByUserName == nil {
|
||||
deps.GetUserByUserName = getUserByUserNameFunc(func(context.Context, adminusers.GetUserByUserNameInput) (adminusers.LookupResult, error) {
|
||||
return adminusers.LookupResult{}, nil
|
||||
})
|
||||
}
|
||||
@@ -967,6 +985,11 @@ func mustNewHandler(t *testing.T, deps Dependencies) http.Handler {
|
||||
return policysvc.LimitCommandResult{}, nil
|
||||
})
|
||||
}
|
||||
if deps.DeleteUser == nil {
|
||||
deps.DeleteUser = deleteUserFunc(func(context.Context, accountdeletion.Input) (accountdeletion.Result, error) {
|
||||
return accountdeletion.Result{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
handler, err := newHandlerWithConfig(Config{
|
||||
Addr: "127.0.0.1:0",
|
||||
@@ -1046,9 +1069,9 @@ func (fn getUserByEmailFunc) Execute(ctx context.Context, input adminusers.GetUs
|
||||
return fn(ctx, input)
|
||||
}
|
||||
|
||||
type getUserByRaceNameFunc func(context.Context, adminusers.GetUserByRaceNameInput) (adminusers.LookupResult, error)
|
||||
type getUserByUserNameFunc func(context.Context, adminusers.GetUserByUserNameInput) (adminusers.LookupResult, error)
|
||||
|
||||
func (fn getUserByRaceNameFunc) Execute(ctx context.Context, input adminusers.GetUserByRaceNameInput) (adminusers.LookupResult, error) {
|
||||
func (fn getUserByUserNameFunc) Execute(ctx context.Context, input adminusers.GetUserByUserNameInput) (adminusers.LookupResult, error) {
|
||||
return fn(ctx, input)
|
||||
}
|
||||
|
||||
@@ -1112,6 +1135,12 @@ func (fn removeLimitFunc) Execute(ctx context.Context, input policysvc.RemoveLim
|
||||
return fn(ctx, input)
|
||||
}
|
||||
|
||||
type deleteUserFunc func(context.Context, accountdeletion.Input) (accountdeletion.Result, error)
|
||||
|
||||
func (fn deleteUserFunc) Execute(ctx context.Context, input accountdeletion.Input) (accountdeletion.Result, error) {
|
||||
return fn(ctx, input)
|
||||
}
|
||||
|
||||
type handlerTestStore struct{}
|
||||
|
||||
func (handlerTestStore) ResolveByEmail(context.Context, common.Email) (ports.ResolveByEmailResult, error) {
|
||||
@@ -1144,7 +1173,7 @@ func (clock handlerTestClock) Now() time.Time {
|
||||
|
||||
type handlerTestIDGenerator struct {
|
||||
userID common.UserID
|
||||
raceName common.RaceName
|
||||
userName common.UserName
|
||||
entitlementRecordID entitlement.EntitlementRecordID
|
||||
sanctionRecordID policy.SanctionRecordID
|
||||
limitRecordID policy.LimitRecordID
|
||||
@@ -1154,8 +1183,8 @@ func (generator handlerTestIDGenerator) NewUserID() (common.UserID, error) {
|
||||
return generator.userID, nil
|
||||
}
|
||||
|
||||
func (generator handlerTestIDGenerator) NewInitialRaceName() (common.RaceName, error) {
|
||||
return generator.raceName, nil
|
||||
func (generator handlerTestIDGenerator) NewUserName() (common.UserName, error) {
|
||||
return generator.userName, nil
|
||||
}
|
||||
|
||||
func (generator handlerTestIDGenerator) NewEntitlementRecordID() (entitlement.EntitlementRecordID, error) {
|
||||
@@ -1170,18 +1199,13 @@ func (generator handlerTestIDGenerator) NewLimitRecordID() (policy.LimitRecordID
|
||||
return generator.limitRecordID, nil
|
||||
}
|
||||
|
||||
type handlerTestRaceNamePolicy struct{}
|
||||
|
||||
func (handlerTestRaceNamePolicy) CanonicalKey(raceName common.RaceName) (account.RaceNameCanonicalKey, error) {
|
||||
return account.RaceNameCanonicalKey("key:" + raceName.String()), nil
|
||||
}
|
||||
|
||||
func sampleAccountView() selfservice.AccountView {
|
||||
timestamp := time.Date(2026, time.April, 9, 10, 0, 0, 0, time.UTC)
|
||||
return selfservice.AccountView{
|
||||
UserID: "user-123",
|
||||
Email: "pilot@example.com",
|
||||
RaceName: "Pilot Nova",
|
||||
UserName: "player-abcdefgh",
|
||||
PreferredLanguage: "en",
|
||||
TimeZone: "Europe/Kaliningrad",
|
||||
DeclaredCountry: "DE",
|
||||
@@ -1240,6 +1264,7 @@ func sampleEligibilityView(exists bool) lobbyeligibility.GetUserEligibilityResul
|
||||
{LimitCode: "max_owned_private_games", Value: 3},
|
||||
{LimitCode: "max_pending_public_applications", Value: 10},
|
||||
{LimitCode: "max_active_game_memberships", Value: 10},
|
||||
{LimitCode: "max_registered_race_names", Value: 2},
|
||||
},
|
||||
Markers: lobbyeligibility.EligibilityMarkersView{
|
||||
CanLogin: true,
|
||||
@@ -1260,5 +1285,4 @@ var (
|
||||
_ ports.AuthDirectoryStore = handlerTestStore{}
|
||||
_ ports.Clock = handlerTestClock{}
|
||||
_ ports.IDGenerator = handlerTestIDGenerator{}
|
||||
_ ports.RaceNamePolicy = handlerTestRaceNamePolicy{}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user