feat: game lobby service
This commit is contained in:
@@ -21,6 +21,7 @@ func TestApplySanctionServiceExecutePublishesEvent(t *testing.T) {
|
||||
limitStore := newFakeLimitStore()
|
||||
publisher := &recordingPolicyPublisher{}
|
||||
|
||||
lifecyclePublisher := &fakeLifecyclePublisher{}
|
||||
service, err := NewApplySanctionServiceWithObservability(
|
||||
fakeAccountStore{existsByUserID: map[common.UserID]bool{userID: true}},
|
||||
sanctionStore,
|
||||
@@ -31,6 +32,7 @@ func TestApplySanctionServiceExecutePublishesEvent(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
publisher,
|
||||
lifecyclePublisher,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -47,8 +49,130 @@ func TestApplySanctionServiceExecutePublishesEvent(t *testing.T) {
|
||||
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()
|
||||
|
||||
|
||||
@@ -267,10 +267,11 @@ func (support commandSupport) loadActiveLimits(
|
||||
|
||||
// ApplySanctionService executes the explicit trusted sanction-apply command.
|
||||
type ApplySanctionService struct {
|
||||
support commandSupport
|
||||
logger *slog.Logger
|
||||
telemetry *telemetry.Runtime
|
||||
publisher ports.SanctionChangedPublisher
|
||||
support commandSupport
|
||||
logger *slog.Logger
|
||||
telemetry *telemetry.Runtime
|
||||
publisher ports.SanctionChangedPublisher
|
||||
lifecyclePublisher ports.UserLifecyclePublisher
|
||||
}
|
||||
|
||||
// NewApplySanctionService constructs one sanction-apply use case.
|
||||
@@ -282,11 +283,13 @@ func NewApplySanctionService(
|
||||
clock ports.Clock,
|
||||
idGenerator ports.IDGenerator,
|
||||
) (*ApplySanctionService, error) {
|
||||
return NewApplySanctionServiceWithObservability(accounts, sanctions, limits, lifecycle, clock, idGenerator, nil, nil, nil)
|
||||
return NewApplySanctionServiceWithObservability(accounts, sanctions, limits, lifecycle, clock, idGenerator, nil, nil, nil, nil)
|
||||
}
|
||||
|
||||
// NewApplySanctionServiceWithObservability constructs one sanction-apply use
|
||||
// case with optional observability hooks.
|
||||
// case with optional observability hooks. `lifecyclePublisher` is consulted
|
||||
// when the newly applied sanction is `SanctionCodePermanentBlock`: one
|
||||
// `user.lifecycle.permanent_blocked` event is emitted after the commit.
|
||||
func NewApplySanctionServiceWithObservability(
|
||||
accounts ports.UserAccountStore,
|
||||
sanctions ports.SanctionStore,
|
||||
@@ -297,6 +300,7 @@ func NewApplySanctionServiceWithObservability(
|
||||
logger *slog.Logger,
|
||||
telemetryRuntime *telemetry.Runtime,
|
||||
publisher ports.SanctionChangedPublisher,
|
||||
lifecyclePublisher ports.UserLifecyclePublisher,
|
||||
) (*ApplySanctionService, error) {
|
||||
support, err := newCommandSupport(accounts, sanctions, limits, lifecycle, clock, idGenerator)
|
||||
if err != nil {
|
||||
@@ -304,10 +308,11 @@ func NewApplySanctionServiceWithObservability(
|
||||
}
|
||||
|
||||
return &ApplySanctionService{
|
||||
support: support,
|
||||
logger: logger,
|
||||
telemetry: telemetryRuntime,
|
||||
publisher: publisher,
|
||||
support: support,
|
||||
logger: logger,
|
||||
telemetry: telemetryRuntime,
|
||||
publisher: publisher,
|
||||
lifecyclePublisher: lifecyclePublisher,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -389,6 +394,9 @@ func (service *ApplySanctionService) Execute(ctx context.Context, input ApplySan
|
||||
ActiveSanctions: sanctionViews(active),
|
||||
}
|
||||
publishSanctionChanged(ctx, service.publisher, service.telemetry, service.logger, "apply_sanction", ports.SanctionChangedOperationApplied, record)
|
||||
if record.SanctionCode == policy.SanctionCodePermanentBlock {
|
||||
publishUserLifecyclePermanentBlocked(ctx, service.lifecyclePublisher, service.telemetry, service.logger, record)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -1177,6 +1185,40 @@ func publishSanctionChanged(
|
||||
}
|
||||
}
|
||||
|
||||
func publishUserLifecyclePermanentBlocked(
|
||||
ctx context.Context,
|
||||
publisher ports.UserLifecyclePublisher,
|
||||
telemetryRuntime *telemetry.Runtime,
|
||||
logger *slog.Logger,
|
||||
record policy.SanctionRecord,
|
||||
) {
|
||||
if publisher == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := ports.UserLifecycleEvent{
|
||||
EventType: ports.UserLifecyclePermanentBlockedEventType,
|
||||
UserID: record.UserID,
|
||||
OccurredAt: record.AppliedAt.UTC(),
|
||||
Source: adminInternalAPISource,
|
||||
Actor: record.Actor,
|
||||
ReasonCode: record.ReasonCode,
|
||||
}
|
||||
if err := publisher.PublishUserLifecycleEvent(ctx, event); err != nil {
|
||||
if telemetryRuntime != nil {
|
||||
telemetryRuntime.RecordEventPublicationFailure(ctx, string(ports.UserLifecyclePermanentBlockedEventType))
|
||||
}
|
||||
shared.LogEventPublicationFailure(logger, ctx, string(ports.UserLifecyclePermanentBlockedEventType), err,
|
||||
"use_case", "apply_sanction",
|
||||
"user_id", record.UserID.String(),
|
||||
"source", adminInternalAPISource.String(),
|
||||
"reason_code", record.ReasonCode.String(),
|
||||
"actor_type", record.Actor.Type.String(),
|
||||
"actor_id", record.Actor.ID.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func publishLimitChanged(
|
||||
ctx context.Context,
|
||||
publisher ports.LimitChangedPublisher,
|
||||
|
||||
@@ -468,7 +468,7 @@ func (store fakeAccountStore) GetByEmail(context.Context, common.Email) (account
|
||||
return account.UserAccount{}, ports.ErrNotFound
|
||||
}
|
||||
|
||||
func (store fakeAccountStore) GetByRaceName(context.Context, common.RaceName) (account.UserAccount, error) {
|
||||
func (store fakeAccountStore) GetByUserName(context.Context, common.UserName) (account.UserAccount, error) {
|
||||
return account.UserAccount{}, ports.ErrNotFound
|
||||
}
|
||||
|
||||
@@ -476,9 +476,6 @@ func (store fakeAccountStore) ExistsByUserID(_ context.Context, userID common.Us
|
||||
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
|
||||
@@ -679,7 +676,7 @@ func (generator fixedIDGenerator) NewUserID() (common.UserID, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (generator fixedIDGenerator) NewInitialRaceName() (common.RaceName, error) {
|
||||
func (generator fixedIDGenerator) NewUserName() (common.UserName, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user