package adminusers import ( "context" "errors" "fmt" "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/entitlementsvc" "galaxy/user/internal/service/shared" "github.com/stretchr/testify/require" ) func TestByIDGetterExecuteReturnsAggregate(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_500, 0).UTC() service, err := NewByIDGetter( newFakeAdminAccountStore(validAdminUserAccount("user-123", "pilot@example.com", "player-abcdefgh", now)), &fakeAdminEntitlementSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ common.UserID("user-123"): validAdminFreeSnapshot(common.UserID("user-123"), now), }, }, fakeAdminSanctionStore{ byUserID: map[common.UserID][]policy.SanctionRecord{ common.UserID("user-123"): { validAdminActiveSanction(common.UserID("user-123"), policy.SanctionCodeLoginBlock, now.Add(-time.Hour)), expiredAdminSanction(common.UserID("user-123"), policy.SanctionCodeGameJoinBlock, now.Add(-2*time.Hour)), }, }, }, fakeAdminLimitStore{ byUserID: map[common.UserID][]policy.LimitRecord{ common.UserID("user-123"): { validAdminActiveLimit(common.UserID("user-123"), policy.LimitCodeMaxOwnedPrivateGames, 3, now.Add(-time.Hour)), }, }, }, adminFixedClock{now: now}, ) require.NoError(t, err) result, err := service.Execute(context.Background(), GetUserByIDInput{UserID: " user-123 "}) require.NoError(t, err) require.Equal(t, "user-123", result.User.UserID) require.Equal(t, "pilot@example.com", result.User.Email) require.Len(t, result.User.ActiveSanctions, 1) require.Equal(t, string(policy.SanctionCodeLoginBlock), result.User.ActiveSanctions[0].SanctionCode) require.Len(t, result.User.ActiveLimits, 1) require.Equal(t, string(policy.LimitCodeMaxOwnedPrivateGames), result.User.ActiveLimits[0].LimitCode) } func TestByEmailGetterExecuteUnknownUserReturnsNotFound(t *testing.T) { t.Parallel() service, err := NewByEmailGetter( newFakeAdminAccountStore(), &fakeAdminEntitlementSnapshotStore{}, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: time.Unix(1_775_240_500, 0).UTC()}, ) require.NoError(t, err) _, err = service.Execute(context.Background(), GetUserByEmailInput{Email: "missing@example.com"}) require.Error(t, err) require.Equal(t, shared.ErrorCodeSubjectNotFound, shared.CodeOf(err)) } func TestByUserNameGetterExecuteReturnsAggregate(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_500, 0).UTC() service, err := NewByUserNameGetter( newFakeAdminAccountStore(validAdminUserAccount("user-123", "pilot@example.com", "player-abcdefgh", now)), &fakeAdminEntitlementSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ common.UserID("user-123"): validAdminFreeSnapshot(common.UserID("user-123"), now), }, }, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: now}, ) require.NoError(t, err) result, err := service.Execute(context.Background(), GetUserByUserNameInput{UserName: " player-abcdefgh "}) require.NoError(t, err) require.Equal(t, "user-123", result.User.UserID) require.Equal(t, "player-abcdefgh", result.User.UserName) } func TestListerExecuteAppliesCombinedFiltersWithLogicalAND(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_500, 0).UTC() firstExpiry := now.Add(48 * time.Hour) secondExpiry := now.Add(72 * time.Hour) before := now.Add(96 * time.Hour) after := now.Add(24 * time.Hour) canLogin := false canCreatePrivateGame := false canJoinGame := false accountStore := newFakeAdminAccountStore( validAdminUserAccount("user-300", "u300@example.com", "player-user300a", now), validAdminUserAccount("user-200", "u200@example.com", "player-user200a", now), validAdminUserAccount("user-100", "u100@example.com", "player-user100a", now), ) snapshotStore := &fakeAdminEntitlementSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ common.UserID("user-300"): validAdminPaidSnapshot(common.UserID("user-300"), now, firstExpiry), common.UserID("user-200"): validAdminPaidSnapshot(common.UserID("user-200"), now, secondExpiry), common.UserID("user-100"): validAdminPaidSnapshot(common.UserID("user-100"), now, secondExpiry), }, } sanctionStore := fakeAdminSanctionStore{ byUserID: map[common.UserID][]policy.SanctionRecord{ common.UserID("user-300"): { validAdminActiveSanction(common.UserID("user-300"), policy.SanctionCodeLoginBlock, now.Add(-time.Hour)), }, common.UserID("user-200"): { validAdminActiveSanction(common.UserID("user-200"), policy.SanctionCodeLoginBlock, now.Add(-time.Hour)), }, common.UserID("user-100"): { validAdminActiveSanction(common.UserID("user-100"), policy.SanctionCodeLoginBlock, now.Add(-time.Hour)), }, }, } limitStore := fakeAdminLimitStore{ byUserID: map[common.UserID][]policy.LimitRecord{ common.UserID("user-300"): { validAdminActiveLimit(common.UserID("user-300"), policy.LimitCodeMaxOwnedPrivateGames, 3, now.Add(-time.Hour)), }, common.UserID("user-100"): { validAdminActiveLimit(common.UserID("user-100"), policy.LimitCodeMaxOwnedPrivateGames, 3, now.Add(-time.Hour)), }, }, } listStore := &fakeAdminListStore{ pages: map[string]ports.ListUsersResult{ "": { UserIDs: []common.UserID{common.UserID("user-300")}, NextPageToken: "cursor-1", }, "cursor-1": { UserIDs: []common.UserID{common.UserID("user-200")}, NextPageToken: "cursor-2", }, "cursor-2": { UserIDs: []common.UserID{common.UserID("user-100")}, NextPageToken: "", }, }, } service, err := NewLister(accountStore, snapshotStore, sanctionStore, limitStore, adminFixedClock{now: now}, listStore) require.NoError(t, err) result, err := service.Execute(context.Background(), ListUsersInput{ PageSize: 2, PaidState: "paid", PaidExpiresBefore: &before, PaidExpiresAfter: &after, DeclaredCountry: "DE", SanctionCode: "login_block", LimitCode: "max_owned_private_games", CanLogin: &canLogin, CanCreatePrivateGame: &canCreatePrivateGame, CanJoinGame: &canJoinGame, }) require.NoError(t, err) require.Len(t, result.Items, 2) require.Equal(t, "user-300", result.Items[0].UserID) require.Equal(t, "user-100", result.Items[1].UserID) require.Equal(t, "", result.NextPageToken) require.Len(t, listStore.calls, 3) for _, call := range listStore.calls { require.Equal(t, 1, call.PageSize) require.Equal(t, entitlement.PaidStatePaid, call.Filters.PaidState) require.Equal(t, common.CountryCode("DE"), call.Filters.DeclaredCountry) require.Equal(t, policy.SanctionCodeLoginBlock, call.Filters.SanctionCode) require.Equal(t, policy.LimitCodeMaxOwnedPrivateGames, call.Filters.LimitCode) } } func TestListerExecuteDefaultAndMaximumPageSize(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_500, 0).UTC() accountStore := newFakeAdminAccountStore( validAdminUserAccount("user-300", "u300@example.com", "player-user300a", now), validAdminUserAccount("user-200", "u200@example.com", "player-user200a", now), validAdminUserAccount("user-100", "u100@example.com", "player-user100a", now), ) snapshotStore := &fakeAdminEntitlementSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ common.UserID("user-300"): validAdminFreeSnapshot(common.UserID("user-300"), now), common.UserID("user-200"): validAdminFreeSnapshot(common.UserID("user-200"), now), common.UserID("user-100"): validAdminFreeSnapshot(common.UserID("user-100"), now), }, } t.Run("default page size", func(t *testing.T) { t.Parallel() listStore := &fakeAdminListStore{ pages: map[string]ports.ListUsersResult{ "": { UserIDs: []common.UserID{common.UserID("user-300")}, NextPageToken: "cursor-1", }, "cursor-1": { UserIDs: []common.UserID{common.UserID("user-200")}, NextPageToken: "cursor-2", }, "cursor-2": { UserIDs: []common.UserID{common.UserID("user-100")}, NextPageToken: "", }, }, } service, err := NewLister(accountStore, snapshotStore, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: now}, listStore) require.NoError(t, err) result, err := service.Execute(context.Background(), ListUsersInput{}) require.NoError(t, err) require.Len(t, result.Items, 3) }) t.Run("maximum page size", func(t *testing.T) { t.Parallel() listStore := &fakeAdminListStore{ pages: map[string]ports.ListUsersResult{ "": { UserIDs: []common.UserID{common.UserID("user-300")}, NextPageToken: "", }, }, } service, err := NewLister(accountStore, snapshotStore, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: now}, listStore) require.NoError(t, err) result, err := service.Execute(context.Background(), ListUsersInput{PageSize: ports.MaxUserListPageSize}) require.NoError(t, err) require.Len(t, result.Items, 1) }) t.Run("above maximum is rejected", func(t *testing.T) { t.Parallel() service, err := NewLister(accountStore, snapshotStore, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: now}, &fakeAdminListStore{}) require.NoError(t, err) _, err = service.Execute(context.Background(), ListUsersInput{PageSize: ports.MaxUserListPageSize + 1}) require.Error(t, err) require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err)) require.Equal(t, "page_size must be between 1 and 200", err.Error()) }) } func TestListerExecuteInvalidPageTokenReturnsInvalidRequest(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_500, 0).UTC() service, err := NewLister( newFakeAdminAccountStore(validAdminUserAccount("user-123", "pilot@example.com", "player-abcdefgh", now)), &fakeAdminEntitlementSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ common.UserID("user-123"): validAdminFreeSnapshot(common.UserID("user-123"), now), }, }, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: now}, &fakeAdminListStore{err: fmt.Errorf("wrapped: %w", ports.ErrInvalidPageToken)}, ) require.NoError(t, err) _, err = service.Execute(context.Background(), ListUsersInput{PageToken: "bad-token"}) require.Error(t, err) require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err)) require.Equal(t, "page_token is invalid or does not match current filters", err.Error()) } func TestListerExecuteRepairsExpiredPaidSnapshotBeforeFiltering(t *testing.T) { t.Parallel() now := time.Unix(1_775_240_500, 0).UTC() expiredAt := now.Add(-time.Hour) accountStore := newFakeAdminAccountStore(validAdminUserAccount("user-123", "pilot@example.com", "Pilot Nova", now)) snapshotStore := &fakeAdminEntitlementSnapshotStore{ byUserID: map[common.UserID]entitlement.CurrentSnapshot{ common.UserID("user-123"): { UserID: common.UserID("user-123"), PlanCode: entitlement.PlanCodePaidMonthly, IsPaid: true, StartsAt: now.Add(-30 * 24 * time.Hour), EndsAt: adminTimePointer(expiredAt), Source: common.Source("admin"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, ReasonCode: common.ReasonCode("manual_grant"), UpdatedAt: expiredAt, }, }, } reader, err := entitlementsvc.NewReader( snapshotStore, &fakeAdminEntitlementLifecycleStore{snapshotStore: snapshotStore}, adminFixedClock{now: now}, adminReaderIDGenerator{recordID: entitlement.EntitlementRecordID("entitlement-repair-free-record")}, ) require.NoError(t, err) listStore := &fakeAdminListStore{ pages: map[string]ports.ListUsersResult{ "": { UserIDs: []common.UserID{common.UserID("user-123")}, NextPageToken: "", }, }, } service, err := NewLister(accountStore, reader, fakeAdminSanctionStore{}, fakeAdminLimitStore{}, adminFixedClock{now: now}, listStore) require.NoError(t, err) result, err := service.Execute(context.Background(), ListUsersInput{PaidState: "free"}) require.NoError(t, err) require.Len(t, result.Items, 1) require.Equal(t, "free", result.Items[0].Entitlement.PlanCode) require.False(t, result.Items[0].Entitlement.IsPaid) storedSnapshot, err := snapshotStore.GetByUserID(context.Background(), common.UserID("user-123")) require.NoError(t, err) require.Equal(t, entitlement.PlanCodeFree, storedSnapshot.PlanCode) require.False(t, storedSnapshot.IsPaid) require.Equal(t, expiredAt, storedSnapshot.StartsAt) } type adminFixedClock struct { now time.Time } func (clock adminFixedClock) Now() time.Time { return clock.now } type adminReaderIDGenerator struct { recordID entitlement.EntitlementRecordID } func (generator adminReaderIDGenerator) NewUserID() (common.UserID, error) { return "", errors.New("unexpected NewUserID call") } func (generator adminReaderIDGenerator) NewUserName() (common.UserName, error) { return "", errors.New("unexpected NewUserName call") } func (generator adminReaderIDGenerator) NewEntitlementRecordID() (entitlement.EntitlementRecordID, error) { return generator.recordID, nil } func (generator adminReaderIDGenerator) NewSanctionRecordID() (policy.SanctionRecordID, error) { return "", errors.New("unexpected NewSanctionRecordID call") } func (generator adminReaderIDGenerator) NewLimitRecordID() (policy.LimitRecordID, error) { return "", errors.New("unexpected NewLimitRecordID call") } type fakeAdminAccountStore struct { byUserID map[common.UserID]account.UserAccount byEmail map[common.Email]common.UserID byUserName map[common.UserName]common.UserID updateErr error createErr error existsByID map[common.UserID]bool } func newFakeAdminAccountStore(records ...account.UserAccount) *fakeAdminAccountStore { store := &fakeAdminAccountStore{ byUserID: make(map[common.UserID]account.UserAccount, len(records)), byEmail: make(map[common.Email]common.UserID, len(records)), byUserName: make(map[common.UserName]common.UserID, len(records)), existsByID: make(map[common.UserID]bool, len(records)), } for _, record := range records { store.byUserID[record.UserID] = record store.byEmail[record.Email] = record.UserID store.byUserName[record.UserName] = record.UserID store.existsByID[record.UserID] = true } return store } func (store *fakeAdminAccountStore) Create(context.Context, ports.CreateAccountInput) error { return store.createErr } func (store *fakeAdminAccountStore) GetByUserID(_ context.Context, userID common.UserID) (account.UserAccount, error) { record, ok := store.byUserID[userID] if !ok { return account.UserAccount{}, ports.ErrNotFound } return record, nil } func (store *fakeAdminAccountStore) GetByEmail(_ context.Context, email common.Email) (account.UserAccount, error) { userID, ok := store.byEmail[email] if !ok { return account.UserAccount{}, ports.ErrNotFound } return store.byUserID[userID], nil } func (store *fakeAdminAccountStore) GetByUserName(_ context.Context, userName common.UserName) (account.UserAccount, error) { userID, ok := store.byUserName[userName] if !ok { return account.UserAccount{}, ports.ErrNotFound } return store.byUserID[userID], nil } func (store *fakeAdminAccountStore) ExistsByUserID(_ context.Context, userID common.UserID) (bool, error) { return store.existsByID[userID], nil } func (store *fakeAdminAccountStore) Update(context.Context, account.UserAccount) error { return store.updateErr } type fakeAdminEntitlementSnapshotStore struct { byUserID map[common.UserID]entitlement.CurrentSnapshot } func (store *fakeAdminEntitlementSnapshotStore) GetByUserID(_ context.Context, userID common.UserID) (entitlement.CurrentSnapshot, error) { record, ok := store.byUserID[userID] if !ok { return entitlement.CurrentSnapshot{}, ports.ErrNotFound } return record, nil } func (store *fakeAdminEntitlementSnapshotStore) Put(_ context.Context, record entitlement.CurrentSnapshot) error { if store.byUserID == nil { store.byUserID = make(map[common.UserID]entitlement.CurrentSnapshot) } store.byUserID[record.UserID] = record return nil } type fakeAdminEntitlementLifecycleStore struct { snapshotStore *fakeAdminEntitlementSnapshotStore } func (store *fakeAdminEntitlementLifecycleStore) Grant(context.Context, ports.GrantEntitlementInput) error { return errors.New("unexpected Grant call") } func (store *fakeAdminEntitlementLifecycleStore) Extend(context.Context, ports.ExtendEntitlementInput) error { return errors.New("unexpected Extend call") } func (store *fakeAdminEntitlementLifecycleStore) Revoke(context.Context, ports.RevokeEntitlementInput) error { return errors.New("unexpected Revoke call") } func (store *fakeAdminEntitlementLifecycleStore) RepairExpired(ctx context.Context, input ports.RepairExpiredEntitlementInput) error { return store.snapshotStore.Put(ctx, input.NewSnapshot) } type fakeAdminSanctionStore struct { byUserID map[common.UserID][]policy.SanctionRecord } func (store fakeAdminSanctionStore) Create(context.Context, policy.SanctionRecord) error { return nil } func (store fakeAdminSanctionStore) GetByRecordID(context.Context, policy.SanctionRecordID) (policy.SanctionRecord, error) { return policy.SanctionRecord{}, ports.ErrNotFound } func (store fakeAdminSanctionStore) ListByUserID(_ context.Context, userID common.UserID) ([]policy.SanctionRecord, error) { return append([]policy.SanctionRecord(nil), store.byUserID[userID]...), nil } func (store fakeAdminSanctionStore) Update(context.Context, policy.SanctionRecord) error { return nil } type fakeAdminLimitStore struct { byUserID map[common.UserID][]policy.LimitRecord } func (store fakeAdminLimitStore) Create(context.Context, policy.LimitRecord) error { return nil } func (store fakeAdminLimitStore) GetByRecordID(context.Context, policy.LimitRecordID) (policy.LimitRecord, error) { return policy.LimitRecord{}, ports.ErrNotFound } func (store fakeAdminLimitStore) ListByUserID(_ context.Context, userID common.UserID) ([]policy.LimitRecord, error) { return append([]policy.LimitRecord(nil), store.byUserID[userID]...), nil } func (store fakeAdminLimitStore) Update(context.Context, policy.LimitRecord) error { return nil } type fakeAdminListStore struct { pages map[string]ports.ListUsersResult err error calls []ports.ListUsersInput } func (store *fakeAdminListStore) ListUserIDs(_ context.Context, input ports.ListUsersInput) (ports.ListUsersResult, error) { store.calls = append(store.calls, input) if store.err != nil { return ports.ListUsersResult{}, store.err } result, ok := store.pages[input.PageToken] if !ok { return ports.ListUsersResult{}, nil } return result, nil } func validAdminUserAccount(userID string, email string, userName string, now time.Time) account.UserAccount { return account.UserAccount{ UserID: common.UserID(userID), Email: common.Email(email), UserName: common.UserName(userName), PreferredLanguage: common.LanguageTag("en"), TimeZone: common.TimeZoneName("Europe/Kaliningrad"), DeclaredCountry: common.CountryCode("DE"), CreatedAt: now, UpdatedAt: now, } } func validAdminFreeSnapshot(userID common.UserID, now time.Time) entitlement.CurrentSnapshot { return entitlement.CurrentSnapshot{ UserID: userID, PlanCode: entitlement.PlanCodeFree, IsPaid: false, StartsAt: now, Source: common.Source("auth_registration"), Actor: common.ActorRef{Type: common.ActorType("service"), ID: common.ActorID("user-service")}, ReasonCode: common.ReasonCode("initial_free_entitlement"), UpdatedAt: now, } } func validAdminPaidSnapshot(userID common.UserID, now time.Time, endsAt time.Time) entitlement.CurrentSnapshot { return entitlement.CurrentSnapshot{ UserID: userID, PlanCode: entitlement.PlanCodePaidMonthly, IsPaid: true, StartsAt: now.Add(-24 * time.Hour), EndsAt: adminTimePointer(endsAt), Source: common.Source("admin"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, ReasonCode: common.ReasonCode("manual_grant"), UpdatedAt: now, } } func validAdminActiveSanction(userID common.UserID, code policy.SanctionCode, appliedAt time.Time) policy.SanctionRecord { return policy.SanctionRecord{ RecordID: policy.SanctionRecordID("sanction-" + string(code) + "-" + userID.String()), UserID: userID, SanctionCode: code, Scope: common.Scope("auth"), ReasonCode: common.ReasonCode("manual_block"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, AppliedAt: appliedAt, } } func expiredAdminSanction(userID common.UserID, code policy.SanctionCode, appliedAt time.Time) policy.SanctionRecord { record := validAdminActiveSanction(userID, code, appliedAt) record.ExpiresAt = adminTimePointer(appliedAt.Add(30 * time.Minute)) return record } func validAdminActiveLimit(userID common.UserID, code policy.LimitCode, value int, appliedAt time.Time) policy.LimitRecord { return policy.LimitRecord{ RecordID: policy.LimitRecordID("limit-" + string(code) + "-" + userID.String()), UserID: userID, LimitCode: code, Value: value, ReasonCode: common.ReasonCode("manual_override"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, AppliedAt: appliedAt, } } func adminTimePointer(value time.Time) *time.Time { copied := value.UTC() return &copied }