feat: user service
This commit is contained in:
@@ -0,0 +1,705 @@
|
||||
package policysvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/shared"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplySanctionServiceExecuteBuildsActiveRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
limitStore := newFakeLimitStore()
|
||||
|
||||
service, err := NewApplySanctionService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), ApplySanctionInput{
|
||||
UserID: userID.String(),
|
||||
SanctionCode: string(policy.SanctionCodeLoginBlock),
|
||||
Scope: "auth",
|
||||
ReasonCode: "policy_blocked",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
AppliedAt: now.Add(-time.Minute).Format(time.RFC3339Nano),
|
||||
ExpiresAt: now.Add(time.Hour).Format(time.RFC3339Nano),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userID.String(), result.UserID)
|
||||
require.Len(t, result.ActiveSanctions, 1)
|
||||
require.Equal(t, string(policy.SanctionCodeLoginBlock), result.ActiveSanctions[0].SanctionCode)
|
||||
|
||||
records, err := sanctionStore.ListByUserID(context.Background(), userID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, records, 1)
|
||||
require.Equal(t, policy.SanctionRecordID("sanction-1"), records[0].RecordID)
|
||||
}
|
||||
|
||||
func TestApplySanctionServiceExecuteRejectsExpiredSanction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
limitStore := newFakeLimitStore()
|
||||
|
||||
service, err := NewApplySanctionService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), ApplySanctionInput{
|
||||
UserID: userID.String(),
|
||||
SanctionCode: string(policy.SanctionCodeLoginBlock),
|
||||
Scope: "auth",
|
||||
ReasonCode: "policy_blocked",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
AppliedAt: now.Add(-2 * time.Hour).Format(time.RFC3339Nano),
|
||||
ExpiresAt: now.Add(-time.Minute).Format(time.RFC3339Nano),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err))
|
||||
}
|
||||
|
||||
func TestApplySanctionServiceExecuteReturnsConflictWhenActiveSanctionExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
existing := policy.SanctionRecord{
|
||||
RecordID: policy.SanctionRecordID("sanction-existing"),
|
||||
UserID: userID,
|
||||
SanctionCode: policy.SanctionCodeLoginBlock,
|
||||
Scope: common.Scope("auth"),
|
||||
ReasonCode: common.ReasonCode("policy_blocked"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")},
|
||||
AppliedAt: now.Add(-time.Hour),
|
||||
}
|
||||
require.NoError(t, sanctionStore.Create(context.Background(), existing))
|
||||
|
||||
service, err := NewApplySanctionService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
newFakeLimitStore(),
|
||||
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: newFakeLimitStore()},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), ApplySanctionInput{
|
||||
UserID: userID.String(),
|
||||
SanctionCode: string(policy.SanctionCodeLoginBlock),
|
||||
Scope: "auth",
|
||||
ReasonCode: "policy_blocked",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-2"},
|
||||
AppliedAt: now.Add(-time.Minute).Format(time.RFC3339Nano),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeConflict, shared.CodeOf(err))
|
||||
}
|
||||
|
||||
func TestApplySanctionServiceExecuteReturnsNotFoundForUnknownUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
service, err := NewApplySanctionService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{}},
|
||||
newFakeSanctionStore(),
|
||||
newFakeLimitStore(),
|
||||
&fakePolicyLifecycleStore{sanctions: newFakeSanctionStore(), limits: newFakeLimitStore()},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), ApplySanctionInput{
|
||||
UserID: "user-missing",
|
||||
SanctionCode: string(policy.SanctionCodeLoginBlock),
|
||||
Scope: "auth",
|
||||
ReasonCode: "policy_blocked",
|
||||
Actor: ActorInput{Type: "admin"},
|
||||
AppliedAt: now.Format(time.RFC3339Nano),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeSubjectNotFound, shared.CodeOf(err))
|
||||
}
|
||||
|
||||
func TestRemoveSanctionServiceExecuteIsIdempotentWhenMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
limitStore := newFakeLimitStore()
|
||||
|
||||
service, err := NewRemoveSanctionService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), RemoveSanctionInput{
|
||||
UserID: userID.String(),
|
||||
SanctionCode: string(policy.SanctionCodeLoginBlock),
|
||||
ReasonCode: "manual_remove",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userID.String(), result.UserID)
|
||||
require.Empty(t, result.ActiveSanctions)
|
||||
}
|
||||
|
||||
func TestRemoveSanctionServiceExecuteTreatsConcurrentRemovalAsSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
limitStore := newFakeLimitStore()
|
||||
record := policy.SanctionRecord{
|
||||
RecordID: policy.SanctionRecordID("sanction-1"),
|
||||
UserID: userID,
|
||||
SanctionCode: policy.SanctionCodeLoginBlock,
|
||||
Scope: common.Scope("auth"),
|
||||
ReasonCode: common.ReasonCode("policy_blocked"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")},
|
||||
AppliedAt: now.Add(-time.Hour),
|
||||
}
|
||||
require.NoError(t, sanctionStore.Create(context.Background(), record))
|
||||
|
||||
lifecycle := &fakePolicyLifecycleStore{
|
||||
sanctions: sanctionStore,
|
||||
limits: limitStore,
|
||||
removeSanctionHook: func(input ports.RemoveSanctionInput) error {
|
||||
updated := input.ExpectedActiveRecord
|
||||
removedAt := now.Add(-time.Minute)
|
||||
updated.RemovedAt = &removedAt
|
||||
updated.RemovedBy = common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-2")}
|
||||
updated.RemovedReasonCode = common.ReasonCode("manual_remove")
|
||||
if err := sanctionStore.Update(context.Background(), updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ports.ErrConflict
|
||||
},
|
||||
}
|
||||
|
||||
service, err := NewRemoveSanctionService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
limitStore,
|
||||
lifecycle,
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), RemoveSanctionInput{
|
||||
UserID: userID.String(),
|
||||
SanctionCode: string(policy.SanctionCodeLoginBlock),
|
||||
ReasonCode: "manual_remove",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, result.ActiveSanctions)
|
||||
}
|
||||
|
||||
func TestSetLimitServiceExecuteReplacesActiveLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
limitStore := newFakeLimitStore()
|
||||
current := policy.LimitRecord{
|
||||
RecordID: policy.LimitRecordID("limit-existing"),
|
||||
UserID: userID,
|
||||
LimitCode: policy.LimitCodeMaxOwnedPrivateGames,
|
||||
Value: 3,
|
||||
ReasonCode: common.ReasonCode("manual_override"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")},
|
||||
AppliedAt: now.Add(-time.Hour),
|
||||
}
|
||||
require.NoError(t, limitStore.Create(context.Background(), current))
|
||||
|
||||
service, err := NewSetLimitService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{limitRecordID: policy.LimitRecordID("limit-new")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), SetLimitInput{
|
||||
UserID: userID.String(),
|
||||
LimitCode: string(policy.LimitCodeMaxOwnedPrivateGames),
|
||||
Value: 5,
|
||||
ReasonCode: "manual_override",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-2"},
|
||||
AppliedAt: now.Format(time.RFC3339Nano),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.ActiveLimits, 1)
|
||||
require.Equal(t, 5, result.ActiveLimits[0].Value)
|
||||
|
||||
storedCurrent, err := limitStore.GetByRecordID(context.Background(), current.RecordID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, storedCurrent.RemovedAt)
|
||||
require.True(t, storedCurrent.RemovedAt.Equal(now))
|
||||
}
|
||||
|
||||
func TestSetLimitServiceExecuteRejectsRetroactiveReplacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
limitStore := newFakeLimitStore()
|
||||
current := policy.LimitRecord{
|
||||
RecordID: policy.LimitRecordID("limit-existing"),
|
||||
UserID: userID,
|
||||
LimitCode: policy.LimitCodeMaxOwnedPrivateGames,
|
||||
Value: 3,
|
||||
ReasonCode: common.ReasonCode("manual_override"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")},
|
||||
AppliedAt: now.Add(-time.Hour),
|
||||
}
|
||||
require.NoError(t, limitStore.Create(context.Background(), current))
|
||||
|
||||
service, err := NewSetLimitService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
newFakeSanctionStore(),
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: newFakeSanctionStore(), limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{limitRecordID: policy.LimitRecordID("limit-new")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), SetLimitInput{
|
||||
UserID: userID.String(),
|
||||
LimitCode: string(policy.LimitCodeMaxOwnedPrivateGames),
|
||||
Value: 5,
|
||||
ReasonCode: "manual_override",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-2"},
|
||||
AppliedAt: now.Add(-2 * time.Hour).Format(time.RFC3339Nano),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err))
|
||||
}
|
||||
|
||||
func TestSetLimitServiceExecuteRejectsRetiredLimitCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
|
||||
tests := []string{
|
||||
string(policy.LimitCodeMaxActivePrivateGames),
|
||||
string(policy.LimitCodeMaxPendingPrivateJoinRequests),
|
||||
string(policy.LimitCodeMaxPendingPrivateInvitesSent),
|
||||
}
|
||||
|
||||
for _, limitCode := range tests {
|
||||
limitCode := limitCode
|
||||
t.Run(limitCode, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, err := NewSetLimitService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
newFakeSanctionStore(),
|
||||
newFakeLimitStore(),
|
||||
&fakePolicyLifecycleStore{sanctions: newFakeSanctionStore(), limits: newFakeLimitStore()},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{limitRecordID: policy.LimitRecordID("limit-new")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), SetLimitInput{
|
||||
UserID: userID.String(),
|
||||
LimitCode: limitCode,
|
||||
Value: 5,
|
||||
ReasonCode: "manual_override",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-2"},
|
||||
AppliedAt: now.Format(time.RFC3339Nano),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLimitServiceExecuteIgnoresRetiredRecordsDuringReload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
limitStore := newFakeLimitStore()
|
||||
require.NoError(t, limitStore.Create(context.Background(), policy.LimitRecord{
|
||||
RecordID: policy.LimitRecordID("limit-legacy"),
|
||||
UserID: userID,
|
||||
LimitCode: policy.LimitCodeMaxActivePrivateGames,
|
||||
Value: 9,
|
||||
ReasonCode: common.ReasonCode("legacy_override"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")},
|
||||
AppliedAt: now.Add(-time.Hour),
|
||||
}))
|
||||
|
||||
service, err := NewSetLimitService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
newFakeSanctionStore(),
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: newFakeSanctionStore(), limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{limitRecordID: policy.LimitRecordID("limit-new")},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), SetLimitInput{
|
||||
UserID: userID.String(),
|
||||
LimitCode: string(policy.LimitCodeMaxOwnedPrivateGames),
|
||||
Value: 5,
|
||||
ReasonCode: "manual_override",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-2"},
|
||||
AppliedAt: now.Format(time.RFC3339Nano),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.ActiveLimits, 1)
|
||||
require.Equal(t, string(policy.LimitCodeMaxOwnedPrivateGames), result.ActiveLimits[0].LimitCode)
|
||||
}
|
||||
|
||||
func TestRemoveLimitServiceExecuteIsIdempotentWhenMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
sanctionStore := newFakeSanctionStore()
|
||||
limitStore := newFakeLimitStore()
|
||||
|
||||
service, err := NewRemoveLimitService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
limitStore,
|
||||
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), RemoveLimitInput{
|
||||
UserID: userID.String(),
|
||||
LimitCode: string(policy.LimitCodeMaxOwnedPrivateGames),
|
||||
ReasonCode: "manual_remove",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, result.ActiveLimits)
|
||||
}
|
||||
|
||||
func TestRemoveLimitServiceExecuteRejectsRetiredLimitCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
|
||||
service, err := NewRemoveLimitService(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
newFakeSanctionStore(),
|
||||
newFakeLimitStore(),
|
||||
&fakePolicyLifecycleStore{sanctions: newFakeSanctionStore(), limits: newFakeLimitStore()},
|
||||
fixedClock{now: now},
|
||||
fixedIDGenerator{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), RemoveLimitInput{
|
||||
UserID: userID.String(),
|
||||
LimitCode: string(policy.LimitCodeMaxPendingPrivateJoinRequests),
|
||||
ReasonCode: "manual_remove",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err))
|
||||
}
|
||||
|
||||
type fakeAccountStore struct {
|
||||
existsByUserID map[common.UserID]bool
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) Create(context.Context, ports.CreateAccountInput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) GetByUserID(context.Context, common.UserID) (account.UserAccount, error) {
|
||||
return account.UserAccount{}, ports.ErrNotFound
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) GetByEmail(context.Context, common.Email) (account.UserAccount, error) {
|
||||
return account.UserAccount{}, ports.ErrNotFound
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) GetByRaceName(context.Context, common.RaceName) (account.UserAccount, error) {
|
||||
return account.UserAccount{}, ports.ErrNotFound
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) ExistsByUserID(_ context.Context, userID common.UserID) (bool, error) {
|
||||
return store.existsByUserID[userID], nil
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) RenameRaceName(context.Context, ports.RenameRaceNameInput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) Update(context.Context, account.UserAccount) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeSanctionStore struct {
|
||||
byUserID map[common.UserID][]policy.SanctionRecord
|
||||
byRecordID map[policy.SanctionRecordID]policy.SanctionRecord
|
||||
}
|
||||
|
||||
func newFakeSanctionStore() *fakeSanctionStore {
|
||||
return &fakeSanctionStore{
|
||||
byUserID: make(map[common.UserID][]policy.SanctionRecord),
|
||||
byRecordID: make(map[policy.SanctionRecordID]policy.SanctionRecord),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *fakeSanctionStore) Create(_ context.Context, record policy.SanctionRecord) error {
|
||||
if err := record.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := store.byRecordID[record.RecordID]; exists {
|
||||
return ports.ErrConflict
|
||||
}
|
||||
store.byRecordID[record.RecordID] = record
|
||||
store.byUserID[record.UserID] = append(store.byUserID[record.UserID], record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *fakeSanctionStore) GetByRecordID(_ context.Context, recordID policy.SanctionRecordID) (policy.SanctionRecord, error) {
|
||||
record, ok := store.byRecordID[recordID]
|
||||
if !ok {
|
||||
return policy.SanctionRecord{}, ports.ErrNotFound
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (store *fakeSanctionStore) ListByUserID(_ context.Context, userID common.UserID) ([]policy.SanctionRecord, error) {
|
||||
records := store.byUserID[userID]
|
||||
cloned := make([]policy.SanctionRecord, len(records))
|
||||
copy(cloned, records)
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
func (store *fakeSanctionStore) Update(_ context.Context, record policy.SanctionRecord) error {
|
||||
if err := record.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := store.byRecordID[record.RecordID]; !exists {
|
||||
return ports.ErrNotFound
|
||||
}
|
||||
store.byRecordID[record.RecordID] = record
|
||||
records := store.byUserID[record.UserID]
|
||||
for index := range records {
|
||||
if records[index].RecordID == record.RecordID {
|
||||
records[index] = record
|
||||
store.byUserID[record.UserID] = records
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ports.ErrNotFound
|
||||
}
|
||||
|
||||
type fakeLimitStore struct {
|
||||
byUserID map[common.UserID][]policy.LimitRecord
|
||||
byRecordID map[policy.LimitRecordID]policy.LimitRecord
|
||||
}
|
||||
|
||||
func newFakeLimitStore() *fakeLimitStore {
|
||||
return &fakeLimitStore{
|
||||
byUserID: make(map[common.UserID][]policy.LimitRecord),
|
||||
byRecordID: make(map[policy.LimitRecordID]policy.LimitRecord),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *fakeLimitStore) Create(_ context.Context, record policy.LimitRecord) error {
|
||||
if err := record.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := store.byRecordID[record.RecordID]; exists {
|
||||
return ports.ErrConflict
|
||||
}
|
||||
store.byRecordID[record.RecordID] = record
|
||||
store.byUserID[record.UserID] = append(store.byUserID[record.UserID], record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *fakeLimitStore) GetByRecordID(_ context.Context, recordID policy.LimitRecordID) (policy.LimitRecord, error) {
|
||||
record, ok := store.byRecordID[recordID]
|
||||
if !ok {
|
||||
return policy.LimitRecord{}, ports.ErrNotFound
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (store *fakeLimitStore) ListByUserID(_ context.Context, userID common.UserID) ([]policy.LimitRecord, error) {
|
||||
records := store.byUserID[userID]
|
||||
cloned := make([]policy.LimitRecord, len(records))
|
||||
copy(cloned, records)
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
func (store *fakeLimitStore) Update(_ context.Context, record policy.LimitRecord) error {
|
||||
if err := record.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := store.byRecordID[record.RecordID]; !exists {
|
||||
return ports.ErrNotFound
|
||||
}
|
||||
store.byRecordID[record.RecordID] = record
|
||||
records := store.byUserID[record.UserID]
|
||||
for index := range records {
|
||||
if records[index].RecordID == record.RecordID {
|
||||
records[index] = record
|
||||
store.byUserID[record.UserID] = records
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ports.ErrNotFound
|
||||
}
|
||||
|
||||
type fakePolicyLifecycleStore struct {
|
||||
sanctions *fakeSanctionStore
|
||||
limits *fakeLimitStore
|
||||
|
||||
applySanctionHook func(input ports.ApplySanctionInput) error
|
||||
removeSanctionHook func(input ports.RemoveSanctionInput) error
|
||||
setLimitHook func(input ports.SetLimitInput) error
|
||||
removeLimitHook func(input ports.RemoveLimitInput) error
|
||||
}
|
||||
|
||||
func (store *fakePolicyLifecycleStore) ApplySanction(ctx context.Context, input ports.ApplySanctionInput) error {
|
||||
if store.applySanctionHook != nil {
|
||||
return store.applySanctionHook(input)
|
||||
}
|
||||
|
||||
records, err := store.sanctions.ListByUserID(ctx, input.NewRecord.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
active, err := policy.ActiveSanctionsAt(records, input.NewRecord.AppliedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, record := range active {
|
||||
if record.SanctionCode == input.NewRecord.SanctionCode {
|
||||
return ports.ErrConflict
|
||||
}
|
||||
}
|
||||
|
||||
return store.sanctions.Create(ctx, input.NewRecord)
|
||||
}
|
||||
|
||||
func (store *fakePolicyLifecycleStore) RemoveSanction(ctx context.Context, input ports.RemoveSanctionInput) error {
|
||||
if store.removeSanctionHook != nil {
|
||||
return store.removeSanctionHook(input)
|
||||
}
|
||||
|
||||
return store.sanctions.Update(ctx, input.UpdatedRecord)
|
||||
}
|
||||
|
||||
func (store *fakePolicyLifecycleStore) SetLimit(ctx context.Context, input ports.SetLimitInput) error {
|
||||
if store.setLimitHook != nil {
|
||||
return store.setLimitHook(input)
|
||||
}
|
||||
|
||||
if input.ExpectedActiveRecord != nil {
|
||||
if err := store.limits.Update(ctx, *input.UpdatedActiveRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return store.limits.Create(ctx, input.NewRecord)
|
||||
}
|
||||
|
||||
func (store *fakePolicyLifecycleStore) RemoveLimit(ctx context.Context, input ports.RemoveLimitInput) error {
|
||||
if store.removeLimitHook != nil {
|
||||
return store.removeLimitHook(input)
|
||||
}
|
||||
|
||||
return store.limits.Update(ctx, input.UpdatedRecord)
|
||||
}
|
||||
|
||||
type fixedClock struct {
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (clock fixedClock) Now() time.Time {
|
||||
return clock.now
|
||||
}
|
||||
|
||||
type fixedIDGenerator struct {
|
||||
sanctionRecordID policy.SanctionRecordID
|
||||
limitRecordID policy.LimitRecordID
|
||||
}
|
||||
|
||||
func (generator fixedIDGenerator) NewUserID() (common.UserID, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (generator fixedIDGenerator) NewInitialRaceName() (common.RaceName, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (generator fixedIDGenerator) NewEntitlementRecordID() (entitlement.EntitlementRecordID, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (generator fixedIDGenerator) NewSanctionRecordID() (policy.SanctionRecordID, error) {
|
||||
return generator.sanctionRecordID, nil
|
||||
}
|
||||
|
||||
func (generator fixedIDGenerator) NewLimitRecordID() (policy.LimitRecordID, error) {
|
||||
return generator.limitRecordID, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ ports.UserAccountStore = fakeAccountStore{}
|
||||
_ ports.SanctionStore = (*fakeSanctionStore)(nil)
|
||||
_ ports.LimitStore = (*fakeLimitStore)(nil)
|
||||
_ ports.PolicyLifecycleStore = (*fakePolicyLifecycleStore)(nil)
|
||||
_ ports.Clock = fixedClock{}
|
||||
_ ports.IDGenerator = fixedIDGenerator{}
|
||||
)
|
||||
Reference in New Issue
Block a user