package userstore import ( "context" "errors" "testing" "time" "galaxy/user/internal/domain/account" "galaxy/user/internal/domain/authblock" "galaxy/user/internal/domain/common" "galaxy/user/internal/domain/entitlement" "galaxy/user/internal/domain/policy" "galaxy/user/internal/ports" "github.com/stretchr/testify/require" ) // All time values are aligned to microseconds because PostgreSQL's // timestamptz only stores microsecond precision; using nanoseconds here // would cause round-trip mismatches. var fixtureCreatedAt = time.Unix(1_775_240_000, 0).UTC() func validAccount() account.UserAccount { return account.UserAccount{ UserID: common.UserID("user-pilot-001"), Email: common.Email("pilot@example.com"), UserName: common.UserName("player-aaaaaaaa"), DisplayName: common.DisplayName("NovaPrime"), PreferredLanguage: common.LanguageTag("en"), TimeZone: common.TimeZoneName("Europe/Kaliningrad"), CreatedAt: fixtureCreatedAt, UpdatedAt: fixtureCreatedAt, } } func validFreeSnapshot(userID common.UserID, at time.Time) entitlement.CurrentSnapshot { return entitlement.CurrentSnapshot{ UserID: userID, PlanCode: entitlement.PlanCodeFree, IsPaid: false, StartsAt: at.UTC(), Source: common.Source("auth_signup"), Actor: common.ActorRef{Type: common.ActorType("auth")}, ReasonCode: common.ReasonCode("initial_free_entitlement"), UpdatedAt: at.UTC(), } } func validFreePeriod(userID common.UserID, recordID entitlement.EntitlementRecordID, at time.Time) entitlement.PeriodRecord { return entitlement.PeriodRecord{ RecordID: recordID, UserID: userID, PlanCode: entitlement.PlanCodeFree, Source: common.Source("auth_signup"), Actor: common.ActorRef{Type: common.ActorType("auth")}, ReasonCode: common.ReasonCode("initial_free_entitlement"), StartsAt: at.UTC(), CreatedAt: at.UTC(), } } func paidPeriod(userID common.UserID, recordID entitlement.EntitlementRecordID, startsAt, endsAt time.Time) entitlement.PeriodRecord { end := endsAt.UTC() return entitlement.PeriodRecord{ RecordID: recordID, UserID: userID, PlanCode: entitlement.PlanCodePaidMonthly, Source: common.Source("admin"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, ReasonCode: common.ReasonCode("manual_grant"), StartsAt: startsAt.UTC(), EndsAt: &end, CreatedAt: startsAt.UTC(), } } func paidSnapshot(userID common.UserID, startsAt, endsAt, updatedAt time.Time) entitlement.CurrentSnapshot { end := endsAt.UTC() return entitlement.CurrentSnapshot{ UserID: userID, PlanCode: entitlement.PlanCodePaidMonthly, IsPaid: true, StartsAt: startsAt.UTC(), EndsAt: &end, Source: common.Source("admin"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, ReasonCode: common.ReasonCode("manual_grant"), UpdatedAt: updatedAt.UTC(), } } func validSanction(userID common.UserID, code policy.SanctionCode, appliedAt time.Time) policy.SanctionRecord { return policy.SanctionRecord{ RecordID: policy.SanctionRecordID("sanction-" + string(code) + "-1"), UserID: userID, SanctionCode: code, Scope: common.Scope("platform"), ReasonCode: common.ReasonCode("manual_block"), Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, AppliedAt: appliedAt.UTC(), } } func validLimit(userID common.UserID, code policy.LimitCode, value int, appliedAt time.Time) policy.LimitRecord { return policy.LimitRecord{ RecordID: policy.LimitRecordID("limit-" + string(code) + "-1"), 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.UTC(), } } func TestAccountCreateAndLookups(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) got, err := store.GetByUserID(ctx, record.UserID) require.NoError(t, err) require.Equal(t, record, got) got, err = store.GetByEmail(ctx, record.Email) require.NoError(t, err) require.Equal(t, record, got) got, err = store.GetByUserName(ctx, record.UserName) require.NoError(t, err) require.Equal(t, record, got) exists, err := store.ExistsByUserID(ctx, record.UserID) require.NoError(t, err) require.True(t, exists) } func TestAccountCreateConflictsAreClassified(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) // Same UserID -> generic conflict. require.True(t, errors.Is(store.Create(ctx, ports.CreateAccountInput{Account: record}), ports.ErrConflict)) // Same UserName, different UserID/email -> ErrUserNameConflict (which // also satisfies errors.Is(ErrConflict)). clone := validAccount() clone.UserID = common.UserID("user-pilot-002") clone.Email = common.Email("pilot2@example.com") err := store.Create(ctx, ports.CreateAccountInput{Account: clone}) require.True(t, errors.Is(err, ports.ErrUserNameConflict)) require.True(t, errors.Is(err, ports.ErrConflict)) // Same email, different UserID/user_name -> generic conflict. clone = validAccount() clone.UserID = common.UserID("user-pilot-003") clone.UserName = common.UserName("player-bbbbbbbb") err = store.Create(ctx, ports.CreateAccountInput{Account: clone}) require.True(t, errors.Is(err, ports.ErrConflict)) require.False(t, errors.Is(err, ports.ErrUserNameConflict)) } func TestAccountUpdateRespectsImmutableFieldsAndSoftDelete(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) updated := record updated.DisplayName = common.DisplayName("HelloWorld") updated.DeclaredCountry = common.CountryCode("DE") updated.UpdatedAt = record.UpdatedAt.Add(time.Minute) require.NoError(t, store.Update(ctx, updated)) got, err := store.GetByUserID(ctx, record.UserID) require.NoError(t, err) require.Equal(t, updated, got) // Mutating user_name must surface as ErrConflict. mutating := updated mutating.UserName = common.UserName("player-xxxxxxxx") require.True(t, errors.Is(store.Update(ctx, mutating), ports.ErrConflict)) // Soft-delete via Update sets DeletedAt; ExistsByUserID flips to false. deletedAt := updated.UpdatedAt.Add(time.Minute) soft := updated soft.DeletedAt = &deletedAt soft.UpdatedAt = deletedAt require.NoError(t, store.Update(ctx, soft)) exists, err := store.ExistsByUserID(ctx, record.UserID) require.NoError(t, err) require.False(t, exists) } func TestBlockedEmailUpsertAndGet(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := authblock.BlockedEmailSubject{ Email: common.Email("blocked@example.com"), ReasonCode: common.ReasonCode("policy_blocked"), BlockedAt: fixtureCreatedAt, } require.NoError(t, store.PutBlockedEmail(ctx, record)) got, err := store.GetBlockedEmail(ctx, record.Email) require.NoError(t, err) require.Equal(t, record, got) // Upsert replaces existing. updated := record updated.ReasonCode = common.ReasonCode("admin_blocked") updated.BlockedAt = record.BlockedAt.Add(time.Hour) updated.Actor = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} require.NoError(t, store.PutBlockedEmail(ctx, updated)) got, err = store.GetBlockedEmail(ctx, record.Email) require.NoError(t, err) require.Equal(t, updated, got) } func TestResolveByEmailReturnsCreatableExistingBlockedAndDeleted(t *testing.T) { store := newTestStore(t) ctx := context.Background() creatable, err := store.ResolveByEmail(ctx, common.Email("nobody@example.com")) require.NoError(t, err) require.Equal(t, ports.AuthResolutionKindCreatable, creatable.Kind) require.NoError(t, store.PutBlockedEmail(ctx, authblock.BlockedEmailSubject{ Email: common.Email("blocked@example.com"), ReasonCode: common.ReasonCode("policy_blocked"), BlockedAt: fixtureCreatedAt, })) blocked, err := store.ResolveByEmail(ctx, common.Email("blocked@example.com")) require.NoError(t, err) require.Equal(t, ports.AuthResolutionKindBlocked, blocked.Kind) require.Equal(t, common.ReasonCode("policy_blocked"), blocked.BlockReasonCode) record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) existing, err := store.ResolveByEmail(ctx, record.Email) require.NoError(t, err) require.Equal(t, ports.AuthResolutionKindExisting, existing.Kind) require.Equal(t, record.UserID, existing.UserID) // Soft-delete the account; the email lookup must now resolve to blocked. deletedAt := record.UpdatedAt.Add(time.Minute) soft := record soft.DeletedAt = &deletedAt soft.UpdatedAt = deletedAt require.NoError(t, store.Update(ctx, soft)) deletedResult, err := store.ResolveByEmail(ctx, record.Email) require.NoError(t, err) require.Equal(t, ports.AuthResolutionKindBlocked, deletedResult.Kind) require.Equal(t, deletedAccountBlockReasonCode, deletedResult.BlockReasonCode) } func TestEnsureByEmailCoversAllOutcomes(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() snapshot := validFreeSnapshot(record.UserID, record.CreatedAt) period := validFreePeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-initial"), record.CreatedAt) created, err := store.EnsureByEmail(ctx, ports.EnsureByEmailInput{ Email: record.Email, Account: record, Entitlement: snapshot, EntitlementRecord: period, }) require.NoError(t, err) require.Equal(t, ports.EnsureByEmailOutcomeCreated, created.Outcome) require.Equal(t, record.UserID, created.UserID) // Second call with the same email returns existing. The Account input // describes the would-be-created record if no account existed yet; its // email must match the request email per ports.EnsureByEmailInput.Validate. existingCandidate := validSecondAccount() existingCandidate.Email = record.Email existing, err := store.EnsureByEmail(ctx, ports.EnsureByEmailInput{ Email: record.Email, Account: existingCandidate, Entitlement: validFreeSnapshot(existingCandidate.UserID, record.CreatedAt), EntitlementRecord: validFreePeriod(existingCandidate.UserID, entitlement.EntitlementRecordID("entitlement-second"), record.CreatedAt), }) require.NoError(t, err) require.Equal(t, ports.EnsureByEmailOutcomeExisting, existing.Outcome) require.Equal(t, record.UserID, existing.UserID) // Blocked email path. require.NoError(t, store.PutBlockedEmail(ctx, authblock.BlockedEmailSubject{ Email: common.Email("blocked@example.com"), ReasonCode: common.ReasonCode("policy_blocked"), BlockedAt: fixtureCreatedAt, })) blockedAccount := validSecondAccount() blockedAccount.Email = common.Email("blocked@example.com") blockedSnapshot := validFreeSnapshot(blockedAccount.UserID, record.CreatedAt) blockedPeriod := validFreePeriod(blockedAccount.UserID, entitlement.EntitlementRecordID("entitlement-blocked"), record.CreatedAt) blocked, err := store.EnsureByEmail(ctx, ports.EnsureByEmailInput{ Email: blockedAccount.Email, Account: blockedAccount, Entitlement: blockedSnapshot, EntitlementRecord: blockedPeriod, }) require.NoError(t, err) require.Equal(t, ports.EnsureByEmailOutcomeBlocked, blocked.Outcome) require.Equal(t, common.ReasonCode("policy_blocked"), blocked.BlockReasonCode) // Soft-deleted account → blocked(account_deleted). deletedAt := record.UpdatedAt.Add(time.Hour) soft := record soft.DeletedAt = &deletedAt soft.UpdatedAt = deletedAt require.NoError(t, store.Update(ctx, soft)) deletedCandidate := validSecondAccount() deletedCandidate.Email = record.Email deletedCandidate.UserID = common.UserID("user-third") deletedCandidate.UserName = common.UserName("player-cccccccc") deletedResult, err := store.EnsureByEmail(ctx, ports.EnsureByEmailInput{ Email: record.Email, Account: deletedCandidate, Entitlement: validFreeSnapshot(deletedCandidate.UserID, record.CreatedAt), EntitlementRecord: validFreePeriod(deletedCandidate.UserID, entitlement.EntitlementRecordID("entitlement-second-2"), record.CreatedAt), }) require.NoError(t, err) require.Equal(t, ports.EnsureByEmailOutcomeBlocked, deletedResult.Outcome) require.Equal(t, deletedAccountBlockReasonCode, deletedResult.BlockReasonCode) } func validSecondAccount() account.UserAccount { return account.UserAccount{ UserID: common.UserID("user-second"), Email: common.Email("second@example.com"), UserName: common.UserName("player-bbbbbbbb"), PreferredLanguage: common.LanguageTag("en"), TimeZone: common.TimeZoneName("UTC"), CreatedAt: fixtureCreatedAt, UpdatedAt: fixtureCreatedAt, } } func TestBlockByUserIDAndBlockByEmail(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) res, err := store.BlockByUserID(ctx, ports.BlockByUserIDInput{ UserID: record.UserID, ReasonCode: common.ReasonCode("manual_block"), BlockedAt: fixtureCreatedAt.Add(time.Hour), }) require.NoError(t, err) require.Equal(t, ports.AuthBlockOutcomeBlocked, res.Outcome) require.Equal(t, record.UserID, res.UserID) // Replay returns AlreadyBlocked. res, err = store.BlockByUserID(ctx, ports.BlockByUserIDInput{ UserID: record.UserID, ReasonCode: common.ReasonCode("manual_block"), BlockedAt: fixtureCreatedAt.Add(2 * time.Hour), }) require.NoError(t, err) require.Equal(t, ports.AuthBlockOutcomeAlreadyBlocked, res.Outcome) require.Equal(t, record.UserID, res.UserID) // Block by email for a non-existing address records the block with // nil resolved_user_id. res, err = store.BlockByEmail(ctx, ports.BlockByEmailInput{ Email: common.Email("ghost@example.com"), ReasonCode: common.ReasonCode("policy_blocked"), BlockedAt: fixtureCreatedAt.Add(time.Hour), }) require.NoError(t, err) require.Equal(t, ports.AuthBlockOutcomeBlocked, res.Outcome) require.True(t, res.UserID.IsZero()) } func TestEntitlementSnapshotPutAndGet(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) snapshot := validFreeSnapshot(record.UserID, record.CreatedAt) require.NoError(t, store.PutEntitlement(ctx, snapshot)) got, err := store.GetEntitlementByUserID(ctx, record.UserID) require.NoError(t, err) require.Equal(t, snapshot, got) // Upsert replaces. paid := paidSnapshot(record.UserID, record.CreatedAt, record.CreatedAt.Add(30*24*time.Hour), record.CreatedAt.Add(time.Minute)) require.NoError(t, store.PutEntitlement(ctx, paid)) got, err = store.GetEntitlementByUserID(ctx, record.UserID) require.NoError(t, err) require.Equal(t, paid, got) } func TestEntitlementHistoryCRUDAndList(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) first := validFreePeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-1"), record.CreatedAt) second := paidPeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-2"), record.CreatedAt.Add(time.Hour), record.CreatedAt.Add(48*time.Hour)) require.NoError(t, store.CreateEntitlementRecord(ctx, first)) require.NoError(t, store.CreateEntitlementRecord(ctx, second)) require.True(t, errors.Is(store.CreateEntitlementRecord(ctx, first), ports.ErrConflict)) got, err := store.GetEntitlementRecordByID(ctx, first.RecordID) require.NoError(t, err) require.Equal(t, first, got) list, err := store.ListEntitlementRecordsByUserID(ctx, record.UserID) require.NoError(t, err) require.Len(t, list, 2) require.Equal(t, first.RecordID, list[0].RecordID) require.Equal(t, second.RecordID, list[1].RecordID) closedAt := record.CreatedAt.Add(2 * time.Hour) updated := first updated.ClosedAt = &closedAt updated.ClosedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} updated.ClosedReasonCode = common.ReasonCode("superseded") require.NoError(t, store.UpdateEntitlementRecord(ctx, updated)) got, err = store.GetEntitlementRecordByID(ctx, updated.RecordID) require.NoError(t, err) require.Equal(t, updated, got) } func TestEntitlementLifecycleGrantExtendRevokeRepair(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) freeSnap := validFreeSnapshot(record.UserID, record.CreatedAt) freeRecord := validFreePeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-free-1"), record.CreatedAt) require.NoError(t, store.PutEntitlement(ctx, freeSnap)) require.NoError(t, store.CreateEntitlementRecord(ctx, freeRecord)) closedAt := record.CreatedAt.Add(time.Hour) closedFree := freeRecord closedFree.ClosedAt = &closedAt closedFree.ClosedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} closedFree.ClosedReasonCode = common.ReasonCode("superseded") paidStart := closedAt paidEnd := paidStart.Add(30 * 24 * time.Hour) paid := paidPeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-paid-1"), paidStart, paidEnd) paidSnap := paidSnapshot(record.UserID, paidStart, paidEnd, paidStart) require.NoError(t, store.GrantEntitlement(ctx, ports.GrantEntitlementInput{ ExpectedCurrentSnapshot: freeSnap, ExpectedCurrentRecord: freeRecord, UpdatedCurrentRecord: closedFree, NewRecord: paid, NewSnapshot: paidSnap, })) got, err := store.GetEntitlementByUserID(ctx, record.UserID) require.NoError(t, err) require.Equal(t, paidSnap, got) // Extend with a new paid segment. extendStart := paidEnd extendEnd := extendStart.Add(30 * 24 * time.Hour) extendRecord := paidPeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-paid-2"), extendStart, extendEnd) extendSnap := paidSnapshot(record.UserID, paidStart, extendEnd, extendStart) require.NoError(t, store.ExtendEntitlement(ctx, ports.ExtendEntitlementInput{ ExpectedCurrentSnapshot: paidSnap, NewRecord: extendRecord, NewSnapshot: extendSnap, })) // Revoke -> back to free. revokeAt := extendStart.Add(time.Hour) revokedPaid := extendRecord revokedPaid.ClosedAt = &revokeAt revokedPaid.ClosedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} revokedPaid.ClosedReasonCode = common.ReasonCode("revoked") freeAgain := validFreePeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-free-2"), revokeAt) freeAgainSnap := validFreeSnapshot(record.UserID, revokeAt) require.NoError(t, store.RevokeEntitlement(ctx, ports.RevokeEntitlementInput{ ExpectedCurrentSnapshot: extendSnap, ExpectedCurrentRecord: extendRecord, UpdatedCurrentRecord: revokedPaid, NewRecord: freeAgain, NewSnapshot: freeAgainSnap, })) got, err = store.GetEntitlementByUserID(ctx, record.UserID) require.NoError(t, err) require.Equal(t, freeAgainSnap, got) } func TestEntitlementLifecycleConflictsOnSnapshotMismatch(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) freeSnap := validFreeSnapshot(record.UserID, record.CreatedAt) require.NoError(t, store.PutEntitlement(ctx, freeSnap)) stale := freeSnap stale.UpdatedAt = freeSnap.UpdatedAt.Add(-time.Hour) freeRecord := validFreePeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-free-1"), record.CreatedAt) require.NoError(t, store.CreateEntitlementRecord(ctx, freeRecord)) closedAt := record.CreatedAt.Add(time.Hour) closedFree := freeRecord closedFree.ClosedAt = &closedAt closedFree.ClosedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} closedFree.ClosedReasonCode = common.ReasonCode("superseded") paid := paidPeriod(record.UserID, entitlement.EntitlementRecordID("entitlement-paid-1"), closedAt, closedAt.Add(time.Hour)) paidSnap := paidSnapshot(record.UserID, closedAt, closedAt.Add(time.Hour), closedAt) err := store.GrantEntitlement(ctx, ports.GrantEntitlementInput{ ExpectedCurrentSnapshot: stale, ExpectedCurrentRecord: freeRecord, UpdatedCurrentRecord: closedFree, NewRecord: paid, NewSnapshot: paidSnap, }) require.True(t, errors.Is(err, ports.ErrConflict)) } func TestPolicyApplyRemoveSanctionAndLimit(t *testing.T) { store := newTestStore(t) ctx := context.Background() record := validAccount() require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: record})) sanction := validSanction(record.UserID, policy.SanctionCodeLoginBlock, fixtureCreatedAt.Add(time.Minute)) require.NoError(t, store.ApplySanction(ctx, ports.ApplySanctionInput{NewRecord: sanction})) got, err := store.GetSanctionByRecordID(ctx, sanction.RecordID) require.NoError(t, err) require.Equal(t, sanction, got) // Re-applying the same sanction code without removing first must return // ErrConflict because (user_id, sanction_code) is unique on // sanction_active. dup := sanction dup.RecordID = policy.SanctionRecordID("sanction-login_block-2") require.True(t, errors.Is(store.ApplySanction(ctx, ports.ApplySanctionInput{NewRecord: dup}), ports.ErrConflict)) removedAt := sanction.AppliedAt.Add(time.Hour) updated := sanction updated.RemovedAt = &removedAt updated.RemovedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} updated.RemovedReasonCode = common.ReasonCode("manual_unblock") require.NoError(t, store.RemoveSanction(ctx, ports.RemoveSanctionInput{ ExpectedActiveRecord: sanction, UpdatedRecord: updated, })) got, err = store.GetSanctionByRecordID(ctx, sanction.RecordID) require.NoError(t, err) require.Equal(t, updated, got) // Now SetLimit on a fresh code; replay must conflict. limit := validLimit(record.UserID, policy.LimitCodeMaxOwnedPrivateGames, 5, fixtureCreatedAt.Add(2*time.Minute)) require.NoError(t, store.SetLimit(ctx, ports.SetLimitInput{NewRecord: limit})) dupLimit := limit dupLimit.RecordID = policy.LimitRecordID("limit-max_owned_private_games-2") require.True(t, errors.Is(store.SetLimit(ctx, ports.SetLimitInput{NewRecord: dupLimit}), ports.ErrConflict)) // SetLimit with ExpectedActiveRecord -> replaces in the active slot. expected := limit expected.RemovedAt = nil expected.RemovedBy = common.ActorRef{} expected.RemovedReasonCode = "" supersededTime := limit.AppliedAt.Add(time.Hour) supersededLimit := limit supersededLimit.RemovedAt = &supersededTime supersededLimit.RemovedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")} supersededLimit.RemovedReasonCode = common.ReasonCode("superseded") newLimit := validLimit(record.UserID, policy.LimitCodeMaxOwnedPrivateGames, 7, supersededTime) newLimit.RecordID = policy.LimitRecordID("limit-max_owned_private_games-3") require.NoError(t, store.SetLimit(ctx, ports.SetLimitInput{ ExpectedActiveRecord: &expected, UpdatedActiveRecord: &supersededLimit, NewRecord: newLimit, })) gotLimit, err := store.GetLimitByRecordID(ctx, newLimit.RecordID) require.NoError(t, err) require.Equal(t, newLimit, gotLimit) } func TestUserListPaginatesNewestFirstAndDetectsFilterMismatch(t *testing.T) { store := newTestStore(t) ctx := context.Background() base := fixtureCreatedAt for index, suffix := range []string{"a", "b", "c", "d", "e"} { acc := validAccount() acc.UserID = common.UserID("user-list-" + suffix) acc.Email = common.Email("list-" + suffix + "@example.com") acc.UserName = common.UserName("player-list" + suffix + "xx") acc.CreatedAt = base.Add(time.Duration(index) * time.Minute) acc.UpdatedAt = acc.CreatedAt require.NoError(t, store.Create(ctx, ports.CreateAccountInput{Account: acc})) } page1, err := store.ListUserIDs(ctx, ports.ListUsersInput{PageSize: 2}) require.NoError(t, err) require.Len(t, page1.UserIDs, 2) require.Equal(t, common.UserID("user-list-e"), page1.UserIDs[0]) require.Equal(t, common.UserID("user-list-d"), page1.UserIDs[1]) require.NotEmpty(t, page1.NextPageToken) page2, err := store.ListUserIDs(ctx, ports.ListUsersInput{ PageSize: 2, PageToken: page1.NextPageToken, }) require.NoError(t, err) require.Len(t, page2.UserIDs, 2) require.Equal(t, common.UserID("user-list-c"), page2.UserIDs[0]) require.Equal(t, common.UserID("user-list-b"), page2.UserIDs[1]) // Mismatched filters must reject the previously-issued token. mismatched, err := store.ListUserIDs(ctx, ports.ListUsersInput{ PageSize: 2, PageToken: page1.NextPageToken, Filters: ports.UserListFilters{PaidState: entitlement.PaidStatePaid}, }) require.True(t, errors.Is(err, ports.ErrInvalidPageToken), "got result %#v err %v", mismatched, err) }