feat: user service
This commit is contained in:
@@ -0,0 +1,504 @@
|
||||
// Package adminusers implements the trusted administrative user-read surface
|
||||
// owned by User Service.
|
||||
package adminusers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/user/internal/domain/common"
|
||||
"galaxy/user/internal/domain/entitlement"
|
||||
"galaxy/user/internal/domain/policy"
|
||||
"galaxy/user/internal/ports"
|
||||
"galaxy/user/internal/service/accountview"
|
||||
"galaxy/user/internal/service/shared"
|
||||
)
|
||||
|
||||
// LookupResult stores one exact trusted admin user lookup result.
|
||||
type LookupResult struct {
|
||||
// User stores the shared account aggregate of the resolved user.
|
||||
User accountview.AccountView `json:"user"`
|
||||
}
|
||||
|
||||
// GetUserByIDInput stores one exact trusted lookup by stable user identifier.
|
||||
type GetUserByIDInput struct {
|
||||
// UserID stores the stable regular-user identifier to resolve.
|
||||
UserID string
|
||||
}
|
||||
|
||||
// GetUserByEmailInput stores one exact trusted lookup by normalized e-mail.
|
||||
type GetUserByEmailInput struct {
|
||||
// Email stores the normalized login/contact e-mail to resolve.
|
||||
Email string
|
||||
}
|
||||
|
||||
// GetUserByRaceNameInput stores one exact trusted lookup by exact stored race
|
||||
// name.
|
||||
type GetUserByRaceNameInput struct {
|
||||
// RaceName stores the exact current race name to resolve.
|
||||
RaceName string
|
||||
}
|
||||
|
||||
// ListUsersInput stores one trusted administrative user-list request.
|
||||
type ListUsersInput struct {
|
||||
// PageSize stores the requested maximum number of returned users. The zero
|
||||
// value selects the frozen default page size.
|
||||
PageSize int
|
||||
|
||||
// PageToken stores the optional opaque continuation cursor.
|
||||
PageToken string
|
||||
|
||||
// PaidState stores the optional coarse free-versus-paid filter.
|
||||
PaidState string
|
||||
|
||||
// PaidExpiresBefore stores the optional strict finite paid-expiry upper
|
||||
// bound.
|
||||
PaidExpiresBefore *time.Time
|
||||
|
||||
// PaidExpiresAfter stores the optional strict finite paid-expiry lower
|
||||
// bound.
|
||||
PaidExpiresAfter *time.Time
|
||||
|
||||
// DeclaredCountry stores the optional current declared-country filter.
|
||||
DeclaredCountry string
|
||||
|
||||
// SanctionCode stores the optional active-sanction filter.
|
||||
SanctionCode string
|
||||
|
||||
// LimitCode stores the optional active user-specific limit filter.
|
||||
LimitCode string
|
||||
|
||||
// CanLogin stores the optional derived login-eligibility filter.
|
||||
CanLogin *bool
|
||||
|
||||
// CanCreatePrivateGame stores the optional derived private-game-create
|
||||
// eligibility filter.
|
||||
CanCreatePrivateGame *bool
|
||||
|
||||
// CanJoinGame stores the optional derived game-join eligibility filter.
|
||||
CanJoinGame *bool
|
||||
}
|
||||
|
||||
// ListUsersResult stores one trusted administrative page of user aggregates.
|
||||
type ListUsersResult struct {
|
||||
// Items stores the returned user aggregates in deterministic order.
|
||||
Items []accountview.AccountView `json:"items"`
|
||||
|
||||
// NextPageToken stores the optional continuation cursor for the next page.
|
||||
NextPageToken string `json:"next_page_token,omitempty"`
|
||||
}
|
||||
|
||||
type entitlementReader interface {
|
||||
GetByUserID(ctx context.Context, userID common.UserID) (entitlement.CurrentSnapshot, error)
|
||||
}
|
||||
|
||||
type readSupport struct {
|
||||
accounts ports.UserAccountStore
|
||||
loader *accountview.Loader
|
||||
}
|
||||
|
||||
func newReadSupport(
|
||||
accounts ports.UserAccountStore,
|
||||
entitlements entitlementReader,
|
||||
sanctions ports.SanctionStore,
|
||||
limits ports.LimitStore,
|
||||
clock ports.Clock,
|
||||
) (readSupport, error) {
|
||||
loader, err := accountview.NewLoader(accounts, entitlements, sanctions, limits, clock)
|
||||
if err != nil {
|
||||
return readSupport{}, fmt.Errorf("account view loader: %w", err)
|
||||
}
|
||||
|
||||
return readSupport{
|
||||
accounts: accounts,
|
||||
loader: loader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ByIDGetter executes exact trusted lookups by stable user identifier.
|
||||
type ByIDGetter struct {
|
||||
support readSupport
|
||||
}
|
||||
|
||||
// NewByIDGetter constructs one exact admin lookup by user id.
|
||||
func NewByIDGetter(
|
||||
accounts ports.UserAccountStore,
|
||||
entitlements entitlementReader,
|
||||
sanctions ports.SanctionStore,
|
||||
limits ports.LimitStore,
|
||||
clock ports.Clock,
|
||||
) (*ByIDGetter, error) {
|
||||
support, err := newReadSupport(accounts, entitlements, sanctions, limits, clock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("admin users by-id getter: %w", err)
|
||||
}
|
||||
|
||||
return &ByIDGetter{support: support}, nil
|
||||
}
|
||||
|
||||
// Execute resolves one exact user by stable user identifier.
|
||||
func (service *ByIDGetter) Execute(ctx context.Context, input GetUserByIDInput) (LookupResult, error) {
|
||||
if ctx == nil {
|
||||
return LookupResult{}, shared.InvalidRequest("context must not be nil")
|
||||
}
|
||||
|
||||
userID, err := shared.ParseUserID(input.UserID)
|
||||
if err != nil {
|
||||
return LookupResult{}, err
|
||||
}
|
||||
|
||||
aggregate, err := service.support.loader.Load(ctx, userID)
|
||||
if err != nil {
|
||||
return LookupResult{}, err
|
||||
}
|
||||
|
||||
return LookupResult{User: aggregate.View()}, nil
|
||||
}
|
||||
|
||||
// ByEmailGetter executes exact trusted lookups by normalized e-mail.
|
||||
type ByEmailGetter struct {
|
||||
support readSupport
|
||||
}
|
||||
|
||||
// NewByEmailGetter constructs one exact admin lookup by normalized e-mail.
|
||||
func NewByEmailGetter(
|
||||
accounts ports.UserAccountStore,
|
||||
entitlements entitlementReader,
|
||||
sanctions ports.SanctionStore,
|
||||
limits ports.LimitStore,
|
||||
clock ports.Clock,
|
||||
) (*ByEmailGetter, error) {
|
||||
support, err := newReadSupport(accounts, entitlements, sanctions, limits, clock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("admin users by-email getter: %w", err)
|
||||
}
|
||||
|
||||
return &ByEmailGetter{support: support}, nil
|
||||
}
|
||||
|
||||
// Execute resolves one exact user by normalized e-mail.
|
||||
func (service *ByEmailGetter) Execute(ctx context.Context, input GetUserByEmailInput) (LookupResult, error) {
|
||||
if ctx == nil {
|
||||
return LookupResult{}, shared.InvalidRequest("context must not be nil")
|
||||
}
|
||||
|
||||
email, err := shared.ParseEmail(input.Email)
|
||||
if err != nil {
|
||||
return LookupResult{}, err
|
||||
}
|
||||
|
||||
record, err := service.support.accounts.GetByEmail(ctx, email)
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, ports.ErrNotFound):
|
||||
return LookupResult{}, shared.SubjectNotFound()
|
||||
default:
|
||||
return LookupResult{}, shared.ServiceUnavailable(err)
|
||||
}
|
||||
|
||||
aggregate, err := service.support.loader.Load(ctx, record.UserID)
|
||||
if err != nil {
|
||||
return LookupResult{}, err
|
||||
}
|
||||
|
||||
return LookupResult{User: aggregate.View()}, nil
|
||||
}
|
||||
|
||||
// ByRaceNameGetter executes exact trusted lookups by exact stored race name.
|
||||
type ByRaceNameGetter struct {
|
||||
support readSupport
|
||||
}
|
||||
|
||||
// NewByRaceNameGetter constructs one exact admin lookup by exact stored race
|
||||
// name.
|
||||
func NewByRaceNameGetter(
|
||||
accounts ports.UserAccountStore,
|
||||
entitlements entitlementReader,
|
||||
sanctions ports.SanctionStore,
|
||||
limits ports.LimitStore,
|
||||
clock ports.Clock,
|
||||
) (*ByRaceNameGetter, error) {
|
||||
support, err := newReadSupport(accounts, entitlements, sanctions, limits, clock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("admin users by-race-name getter: %w", err)
|
||||
}
|
||||
|
||||
return &ByRaceNameGetter{support: support}, nil
|
||||
}
|
||||
|
||||
// Execute resolves one exact user by exact stored race name.
|
||||
func (service *ByRaceNameGetter) Execute(ctx context.Context, input GetUserByRaceNameInput) (LookupResult, error) {
|
||||
if ctx == nil {
|
||||
return LookupResult{}, shared.InvalidRequest("context must not be nil")
|
||||
}
|
||||
|
||||
raceName, err := shared.ParseRaceName(input.RaceName)
|
||||
if err != nil {
|
||||
return LookupResult{}, err
|
||||
}
|
||||
|
||||
record, err := service.support.accounts.GetByRaceName(ctx, raceName)
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, ports.ErrNotFound):
|
||||
return LookupResult{}, shared.SubjectNotFound()
|
||||
default:
|
||||
return LookupResult{}, shared.ServiceUnavailable(err)
|
||||
}
|
||||
|
||||
aggregate, err := service.support.loader.Load(ctx, record.UserID)
|
||||
if err != nil {
|
||||
return LookupResult{}, err
|
||||
}
|
||||
|
||||
return LookupResult{User: aggregate.View()}, nil
|
||||
}
|
||||
|
||||
// Lister executes the trusted administrative filtered user listing.
|
||||
type Lister struct {
|
||||
support readSupport
|
||||
listStore ports.UserListStore
|
||||
}
|
||||
|
||||
// NewLister constructs one trusted administrative filtered user lister.
|
||||
func NewLister(
|
||||
accounts ports.UserAccountStore,
|
||||
entitlements entitlementReader,
|
||||
sanctions ports.SanctionStore,
|
||||
limits ports.LimitStore,
|
||||
clock ports.Clock,
|
||||
listStore ports.UserListStore,
|
||||
) (*Lister, error) {
|
||||
if listStore == nil {
|
||||
return nil, fmt.Errorf("admin users lister: user list store must not be nil")
|
||||
}
|
||||
|
||||
support, err := newReadSupport(accounts, entitlements, sanctions, limits, clock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("admin users lister: %w", err)
|
||||
}
|
||||
|
||||
return &Lister{
|
||||
support: support,
|
||||
listStore: listStore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute lists users in deterministic newest-first order and combines all
|
||||
// supplied filters with logical AND semantics.
|
||||
func (service *Lister) Execute(ctx context.Context, input ListUsersInput) (ListUsersResult, error) {
|
||||
if ctx == nil {
|
||||
return ListUsersResult{}, shared.InvalidRequest("context must not be nil")
|
||||
}
|
||||
if strings.TrimSpace(input.PageToken) != input.PageToken {
|
||||
return ListUsersResult{}, shared.InvalidRequest("page_token must not contain surrounding whitespace")
|
||||
}
|
||||
|
||||
pageSize, err := normalizePageSize(input.PageSize)
|
||||
if err != nil {
|
||||
return ListUsersResult{}, err
|
||||
}
|
||||
filters, err := parseListFilters(input)
|
||||
if err != nil {
|
||||
return ListUsersResult{}, err
|
||||
}
|
||||
|
||||
result := ListUsersResult{
|
||||
Items: make([]accountview.AccountView, 0, pageSize),
|
||||
}
|
||||
currentToken := input.PageToken
|
||||
|
||||
for len(result.Items) < pageSize {
|
||||
candidatePage, err := service.listStore.ListUserIDs(ctx, ports.ListUsersInput{
|
||||
PageSize: 1,
|
||||
PageToken: currentToken,
|
||||
Filters: filters,
|
||||
})
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, ports.ErrInvalidPageToken):
|
||||
return ListUsersResult{}, shared.InvalidRequest("page_token is invalid or does not match current filters")
|
||||
default:
|
||||
return ListUsersResult{}, shared.ServiceUnavailable(err)
|
||||
}
|
||||
if len(candidatePage.UserIDs) == 0 {
|
||||
result.NextPageToken = ""
|
||||
return result, nil
|
||||
}
|
||||
|
||||
nextToken := candidatePage.NextPageToken
|
||||
candidateID := candidatePage.UserIDs[0]
|
||||
|
||||
aggregate, err := service.support.loader.Load(ctx, candidateID)
|
||||
if err != nil {
|
||||
return ListUsersResult{}, err
|
||||
}
|
||||
if matchesFilters(aggregate, filters) {
|
||||
result.Items = append(result.Items, aggregate.View())
|
||||
result.NextPageToken = nextToken
|
||||
}
|
||||
|
||||
if nextToken == "" {
|
||||
result.NextPageToken = ""
|
||||
return result, nil
|
||||
}
|
||||
|
||||
currentToken = nextToken
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizePageSize(value int) (int, error) {
|
||||
switch {
|
||||
case value == 0:
|
||||
return ports.DefaultUserListPageSize, nil
|
||||
case value < 0:
|
||||
return 0, shared.InvalidRequest("page_size must be between 1 and 200")
|
||||
case value > ports.MaxUserListPageSize:
|
||||
return 0, shared.InvalidRequest("page_size must be between 1 and 200")
|
||||
default:
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseListFilters(input ListUsersInput) (ports.UserListFilters, error) {
|
||||
paidState, err := parsePaidState(input.PaidState)
|
||||
if err != nil {
|
||||
return ports.UserListFilters{}, err
|
||||
}
|
||||
declaredCountry, err := parseCountryCode(input.DeclaredCountry)
|
||||
if err != nil {
|
||||
return ports.UserListFilters{}, err
|
||||
}
|
||||
sanctionCode, err := parseSanctionCode(input.SanctionCode)
|
||||
if err != nil {
|
||||
return ports.UserListFilters{}, err
|
||||
}
|
||||
limitCode, err := parseLimitCode(input.LimitCode)
|
||||
if err != nil {
|
||||
return ports.UserListFilters{}, err
|
||||
}
|
||||
|
||||
filters := ports.UserListFilters{
|
||||
PaidState: paidState,
|
||||
PaidExpiresBefore: input.PaidExpiresBefore,
|
||||
PaidExpiresAfter: input.PaidExpiresAfter,
|
||||
DeclaredCountry: declaredCountry,
|
||||
SanctionCode: sanctionCode,
|
||||
LimitCode: limitCode,
|
||||
CanLogin: input.CanLogin,
|
||||
CanCreatePrivateGame: input.CanCreatePrivateGame,
|
||||
CanJoinGame: input.CanJoinGame,
|
||||
}
|
||||
if err := filters.Validate(); err != nil {
|
||||
return ports.UserListFilters{}, shared.InvalidRequest(err.Error())
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
func parsePaidState(value string) (entitlement.PaidState, error) {
|
||||
state := entitlement.PaidState(shared.NormalizeString(value))
|
||||
if !state.IsKnown() {
|
||||
return "", shared.InvalidRequest(fmt.Sprintf("paid_state %q is unsupported", state))
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func parseCountryCode(value string) (common.CountryCode, error) {
|
||||
code := common.CountryCode(shared.NormalizeString(value))
|
||||
if code.IsZero() {
|
||||
return "", nil
|
||||
}
|
||||
if err := code.Validate(); err != nil {
|
||||
return "", shared.InvalidRequest(fmt.Sprintf("declared_country: %s", err.Error()))
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func parseSanctionCode(value string) (policy.SanctionCode, error) {
|
||||
code := policy.SanctionCode(shared.NormalizeString(value))
|
||||
if code == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !code.IsKnown() {
|
||||
return "", shared.InvalidRequest(fmt.Sprintf("sanction_code %q is unsupported", code))
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func parseLimitCode(value string) (policy.LimitCode, error) {
|
||||
code := policy.LimitCode(shared.NormalizeString(value))
|
||||
if code == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !code.IsKnown() {
|
||||
return "", shared.InvalidRequest(fmt.Sprintf("limit_code %q is unsupported", code))
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func matchesFilters(aggregate accountview.Aggregate, filters ports.UserListFilters) bool {
|
||||
switch filters.PaidState {
|
||||
case entitlement.PaidStateFree:
|
||||
if aggregate.EntitlementSnapshot.IsPaid {
|
||||
return false
|
||||
}
|
||||
case entitlement.PaidStatePaid:
|
||||
if !aggregate.EntitlementSnapshot.IsPaid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if filters.PaidExpiresBefore != nil {
|
||||
if !aggregate.EntitlementSnapshot.HasFiniteExpiry() || !aggregate.EntitlementSnapshot.EndsAt.Before(filters.PaidExpiresBefore.UTC()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if filters.PaidExpiresAfter != nil {
|
||||
if !aggregate.EntitlementSnapshot.HasFiniteExpiry() || !aggregate.EntitlementSnapshot.EndsAt.After(filters.PaidExpiresAfter.UTC()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !filters.DeclaredCountry.IsZero() && aggregate.AccountRecord.DeclaredCountry != filters.DeclaredCountry {
|
||||
return false
|
||||
}
|
||||
if filters.SanctionCode != "" && !aggregate.HasActiveSanction(filters.SanctionCode) {
|
||||
return false
|
||||
}
|
||||
if filters.LimitCode != "" && !aggregate.HasActiveLimit(filters.LimitCode) {
|
||||
return false
|
||||
}
|
||||
|
||||
canLogin, canCreatePrivateGame, canJoinGame := deriveFilterEligibility(aggregate)
|
||||
if filters.CanLogin != nil && canLogin != *filters.CanLogin {
|
||||
return false
|
||||
}
|
||||
if filters.CanCreatePrivateGame != nil && canCreatePrivateGame != *filters.CanCreatePrivateGame {
|
||||
return false
|
||||
}
|
||||
if filters.CanJoinGame != nil && canJoinGame != *filters.CanJoinGame {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func deriveFilterEligibility(aggregate accountview.Aggregate) (bool, bool, bool) {
|
||||
canLogin := !aggregate.HasActiveSanction(policy.SanctionCodeLoginBlock)
|
||||
canCreatePrivateGame := canLogin &&
|
||||
aggregate.EntitlementSnapshot.IsPaid &&
|
||||
!aggregate.HasActiveSanction(policy.SanctionCodePrivateGameCreateBlock)
|
||||
canJoinGame := canLogin &&
|
||||
!aggregate.HasActiveSanction(policy.SanctionCodeGameJoinBlock)
|
||||
|
||||
return canLogin, canCreatePrivateGame, canJoinGame
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
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", "Pilot Nova", 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 TestByRaceNameGetterExecuteReturnsAggregate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_500, 0).UTC()
|
||||
service, err := NewByRaceNameGetter(
|
||||
newFakeAdminAccountStore(validAdminUserAccount("user-123", "pilot@example.com", "Pilot Nova", 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(), GetUserByRaceNameInput{RaceName: " Pilot Nova "})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user-123", result.User.UserID)
|
||||
require.Equal(t, "Pilot Nova", result.User.RaceName)
|
||||
}
|
||||
|
||||
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", "User 300", now),
|
||||
validAdminUserAccount("user-200", "u200@example.com", "User 200", now),
|
||||
validAdminUserAccount("user-100", "u100@example.com", "User 100", 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", "User 300", now),
|
||||
validAdminUserAccount("user-200", "u200@example.com", "User 200", now),
|
||||
validAdminUserAccount("user-100", "u100@example.com", "User 100", 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", "Pilot Nova", 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) NewInitialRaceName() (common.RaceName, error) {
|
||||
return "", errors.New("unexpected NewInitialRaceName 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
|
||||
byRaceName map[common.RaceName]common.UserID
|
||||
updateErr error
|
||||
renameErr 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)),
|
||||
byRaceName: make(map[common.RaceName]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.byRaceName[record.RaceName] = 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) GetByRaceName(_ context.Context, raceName common.RaceName) (account.UserAccount, error) {
|
||||
userID, ok := store.byRaceName[raceName]
|
||||
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) RenameRaceName(context.Context, ports.RenameRaceNameInput) error {
|
||||
return store.renameErr
|
||||
}
|
||||
|
||||
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, raceName string, now time.Time) account.UserAccount {
|
||||
return account.UserAccount{
|
||||
UserID: common.UserID(userID),
|
||||
Email: common.Email(email),
|
||||
RaceName: common.RaceName(raceName),
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user