703 lines
22 KiB
Go
703 lines
22 KiB
Go
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) GetByUserName(context.Context, common.UserName) (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) 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) NewUserName() (common.UserName, 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{}
|
|
)
|