303 lines
9.7 KiB
Go
303 lines
9.7 KiB
Go
package policysvc
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/user/internal/domain/common"
|
|
"galaxy/user/internal/domain/policy"
|
|
"galaxy/user/internal/ports"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestApplySanctionServiceExecutePublishesEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Unix(1_775_240_000, 0).UTC()
|
|
userID := common.UserID("user-123")
|
|
sanctionStore := newFakeSanctionStore()
|
|
limitStore := newFakeLimitStore()
|
|
publisher := &recordingPolicyPublisher{}
|
|
|
|
lifecyclePublisher := &fakeLifecyclePublisher{}
|
|
service, err := NewApplySanctionServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
lifecyclePublisher,
|
|
)
|
|
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(-time.Minute).Format(time.RFC3339Nano),
|
|
ExpiresAt: now.Add(time.Hour).Format(time.RFC3339Nano),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, publisher.sanctionEvents, 1)
|
|
require.Equal(t, ports.SanctionChangedOperationApplied, publisher.sanctionEvents[0].Operation)
|
|
require.Equal(t, common.Source("admin_internal_api"), publisher.sanctionEvents[0].Source)
|
|
require.Empty(t, lifecyclePublisher.events,
|
|
"login_block must not emit a user.lifecycle.permanent_blocked event")
|
|
}
|
|
|
|
func TestApplySanctionServiceExecutePermanentBlockPublishesLifecycleEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Unix(1_775_240_000, 0).UTC()
|
|
userID := common.UserID("user-123")
|
|
sanctionStore := newFakeSanctionStore()
|
|
limitStore := newFakeLimitStore()
|
|
publisher := &recordingPolicyPublisher{}
|
|
lifecyclePublisher := &fakeLifecyclePublisher{}
|
|
|
|
service, err := NewApplySanctionServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
lifecyclePublisher,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
appliedAt := now.Add(-time.Minute)
|
|
_, err = service.Execute(context.Background(), ApplySanctionInput{
|
|
UserID: userID.String(),
|
|
SanctionCode: string(policy.SanctionCodePermanentBlock),
|
|
Scope: "platform",
|
|
ReasonCode: "terminal_policy_violation",
|
|
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
|
AppliedAt: appliedAt.Format(time.RFC3339Nano),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, publisher.sanctionEvents, 1)
|
|
require.Len(t, lifecyclePublisher.events, 1)
|
|
emitted := lifecyclePublisher.events[0]
|
|
require.Equal(t, ports.UserLifecyclePermanentBlockedEventType, emitted.EventType)
|
|
require.Equal(t, userID, emitted.UserID)
|
|
require.True(t, emitted.OccurredAt.Equal(appliedAt.UTC()))
|
|
require.Equal(t, common.Source("admin_internal_api"), emitted.Source)
|
|
require.Equal(t, common.ReasonCode("terminal_policy_violation"), emitted.ReasonCode)
|
|
require.Equal(t, common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, emitted.Actor)
|
|
}
|
|
|
|
func TestRemoveSanctionServicePermanentBlockDoesNotEmitLifecycleEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Unix(1_775_240_000, 0).UTC()
|
|
userID := common.UserID("user-123")
|
|
sanctionStore := newFakeSanctionStore()
|
|
limitStore := newFakeLimitStore()
|
|
publisher := &recordingPolicyPublisher{}
|
|
lifecyclePublisher := &fakeLifecyclePublisher{}
|
|
|
|
// First, apply permanent_block so a subsequent remove has an active record
|
|
// to target.
|
|
applyService, err := NewApplySanctionServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{sanctionRecordID: policy.SanctionRecordID("sanction-1")},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
lifecyclePublisher,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = applyService.Execute(context.Background(), ApplySanctionInput{
|
|
UserID: userID.String(),
|
|
SanctionCode: string(policy.SanctionCodePermanentBlock),
|
|
Scope: "platform",
|
|
ReasonCode: "terminal_policy_violation",
|
|
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
|
AppliedAt: now.Add(-time.Hour).Format(time.RFC3339Nano),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, lifecyclePublisher.events, 1)
|
|
|
|
removeService, err := NewRemoveSanctionServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = removeService.Execute(context.Background(), RemoveSanctionInput{
|
|
UserID: userID.String(),
|
|
SanctionCode: string(policy.SanctionCodePermanentBlock),
|
|
ReasonCode: "appeal_granted",
|
|
Actor: ActorInput{Type: "admin", ID: "admin-2"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, lifecyclePublisher.events, 1,
|
|
"remove-sanction must not emit an additional lifecycle event")
|
|
}
|
|
|
|
type fakeLifecyclePublisher struct {
|
|
events []ports.UserLifecycleEvent
|
|
}
|
|
|
|
func (publisher *fakeLifecyclePublisher) PublishUserLifecycleEvent(_ context.Context, event ports.UserLifecycleEvent) error {
|
|
if err := event.Validate(); err != nil {
|
|
return err
|
|
}
|
|
publisher.events = append(publisher.events, event)
|
|
return nil
|
|
}
|
|
|
|
var _ ports.UserLifecyclePublisher = (*fakeLifecyclePublisher)(nil)
|
|
|
|
func TestRemoveSanctionServiceExecuteMissingDoesNotPublishEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Unix(1_775_240_000, 0).UTC()
|
|
userID := common.UserID("user-123")
|
|
sanctionStore := newFakeSanctionStore()
|
|
limitStore := newFakeLimitStore()
|
|
publisher := &recordingPolicyPublisher{}
|
|
|
|
service, err := NewRemoveSanctionServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, 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, publisher.sanctionEvents)
|
|
}
|
|
|
|
func TestSetLimitServiceExecutePublishesEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Unix(1_775_240_000, 0).UTC()
|
|
userID := common.UserID("user-123")
|
|
sanctionStore := newFakeSanctionStore()
|
|
limitStore := newFakeLimitStore()
|
|
publisher := &recordingPolicyPublisher{}
|
|
|
|
service, err := NewSetLimitServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{limitRecordID: policy.LimitRecordID("limit-1")},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
)
|
|
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-1"},
|
|
AppliedAt: now.Add(-time.Minute).Format(time.RFC3339Nano),
|
|
ExpiresAt: now.Add(time.Hour).Format(time.RFC3339Nano),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, publisher.limitEvents, 1)
|
|
require.Equal(t, ports.LimitChangedOperationSet, publisher.limitEvents[0].Operation)
|
|
require.NotNil(t, publisher.limitEvents[0].Value)
|
|
require.Equal(t, 5, *publisher.limitEvents[0].Value)
|
|
}
|
|
|
|
func TestRemoveLimitServiceExecuteMissingDoesNotPublishEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
now := time.Unix(1_775_240_000, 0).UTC()
|
|
userID := common.UserID("user-123")
|
|
sanctionStore := newFakeSanctionStore()
|
|
limitStore := newFakeLimitStore()
|
|
publisher := &recordingPolicyPublisher{}
|
|
|
|
service, err := NewRemoveLimitServiceWithObservability(
|
|
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
|
sanctionStore,
|
|
limitStore,
|
|
&fakePolicyLifecycleStore{sanctions: sanctionStore, limits: limitStore},
|
|
fixedClock{now: now},
|
|
fixedIDGenerator{},
|
|
nil,
|
|
nil,
|
|
publisher,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, 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, publisher.limitEvents)
|
|
}
|
|
|
|
type recordingPolicyPublisher struct {
|
|
sanctionEvents []ports.SanctionChangedEvent
|
|
limitEvents []ports.LimitChangedEvent
|
|
}
|
|
|
|
func (publisher *recordingPolicyPublisher) PublishSanctionChanged(_ context.Context, event ports.SanctionChangedEvent) error {
|
|
if err := event.Validate(); err != nil {
|
|
return err
|
|
}
|
|
publisher.sanctionEvents = append(publisher.sanctionEvents, event)
|
|
return nil
|
|
}
|
|
|
|
func (publisher *recordingPolicyPublisher) PublishLimitChanged(_ context.Context, event ports.LimitChangedEvent) error {
|
|
if err := event.Validate(); err != nil {
|
|
return err
|
|
}
|
|
publisher.limitEvents = append(publisher.limitEvents, event)
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
_ ports.SanctionChangedPublisher = (*recordingPolicyPublisher)(nil)
|
|
_ ports.LimitChangedPublisher = (*recordingPolicyPublisher)(nil)
|
|
)
|