feat: game lobby service
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
package accountdeletion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/user/internal/domain/account"
|
||||
"galaxy/user/internal/domain/common"
|
||||
"galaxy/user/internal/ports"
|
||||
"galaxy/user/internal/service/shared"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceExecuteSoftDeletesAndEmitsLifecycleEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_500, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
created := now.Add(-24 * time.Hour)
|
||||
|
||||
accounts := newFakeAccountStore()
|
||||
accounts.records[userID] = account.UserAccount{
|
||||
UserID: userID,
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: created,
|
||||
UpdatedAt: created,
|
||||
}
|
||||
|
||||
publisher := &fakeLifecyclePublisher{}
|
||||
service, err := NewService(accounts, fixedClock{now: now}, publisher)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := service.Execute(context.Background(), Input{
|
||||
UserID: userID.String(),
|
||||
ReasonCode: "user_right_to_be_forgotten",
|
||||
Actor: ActorInput{Type: "admin", ID: "admin-1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userID.String(), result.UserID)
|
||||
require.True(t, result.DeletedAt.Equal(now))
|
||||
|
||||
stored := accounts.records[userID]
|
||||
require.NotNil(t, stored.DeletedAt)
|
||||
require.True(t, stored.DeletedAt.Equal(now))
|
||||
|
||||
require.Len(t, publisher.events, 1)
|
||||
emitted := publisher.events[0]
|
||||
require.Equal(t, ports.UserLifecycleDeletedEventType, emitted.EventType)
|
||||
require.Equal(t, userID, emitted.UserID)
|
||||
require.True(t, emitted.OccurredAt.Equal(now))
|
||||
require.Equal(t, common.Source("admin_internal_api"), emitted.Source)
|
||||
require.Equal(t, common.ReasonCode("user_right_to_be_forgotten"), emitted.ReasonCode)
|
||||
require.Equal(t, common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")}, emitted.Actor)
|
||||
}
|
||||
|
||||
func TestServiceExecuteSecondCallReturnsSubjectNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_500, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
created := now.Add(-24 * time.Hour)
|
||||
alreadyDeleted := now.Add(-time.Hour)
|
||||
|
||||
accounts := newFakeAccountStore()
|
||||
accounts.records[userID] = account.UserAccount{
|
||||
UserID: userID,
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: created,
|
||||
UpdatedAt: alreadyDeleted,
|
||||
DeletedAt: &alreadyDeleted,
|
||||
}
|
||||
|
||||
publisher := &fakeLifecyclePublisher{}
|
||||
service, err := NewService(accounts, fixedClock{now: now}, publisher)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), Input{
|
||||
UserID: userID.String(),
|
||||
ReasonCode: "user_right_to_be_forgotten",
|
||||
Actor: ActorInput{Type: "admin"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeSubjectNotFound, shared.CodeOf(err))
|
||||
require.Empty(t, publisher.events)
|
||||
}
|
||||
|
||||
func TestServiceExecuteUnknownUserReturnsSubjectNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accounts := newFakeAccountStore()
|
||||
publisher := &fakeLifecyclePublisher{}
|
||||
service, err := NewService(accounts, fixedClock{now: time.Unix(1_775_240_500, 0).UTC()}, publisher)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), Input{
|
||||
UserID: "user-missing",
|
||||
ReasonCode: "manual",
|
||||
Actor: ActorInput{Type: "admin"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeSubjectNotFound, shared.CodeOf(err))
|
||||
require.Empty(t, publisher.events)
|
||||
}
|
||||
|
||||
func TestServiceExecuteInvalidActorRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accounts := newFakeAccountStore()
|
||||
publisher := &fakeLifecyclePublisher{}
|
||||
service, err := NewService(accounts, fixedClock{now: time.Unix(1_775_240_500, 0).UTC()}, publisher)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), Input{
|
||||
UserID: "user-123",
|
||||
ReasonCode: "manual",
|
||||
Actor: ActorInput{},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeInvalidRequest, shared.CodeOf(err))
|
||||
require.Empty(t, publisher.events)
|
||||
}
|
||||
|
||||
func TestServiceExecuteStoreConflictSurfacesAsConflict(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_500, 0).UTC()
|
||||
userID := common.UserID("user-123")
|
||||
created := now.Add(-24 * time.Hour)
|
||||
|
||||
accounts := newFakeAccountStore()
|
||||
accounts.records[userID] = account.UserAccount{
|
||||
UserID: userID,
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: created,
|
||||
UpdatedAt: created,
|
||||
}
|
||||
accounts.updateErr = ports.ErrConflict
|
||||
|
||||
publisher := &fakeLifecyclePublisher{}
|
||||
service, err := NewService(accounts, fixedClock{now: now}, publisher)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.Execute(context.Background(), Input{
|
||||
UserID: userID.String(),
|
||||
ReasonCode: "manual",
|
||||
Actor: ActorInput{Type: "admin"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, shared.ErrorCodeConflict, shared.CodeOf(err))
|
||||
require.Empty(t, publisher.events)
|
||||
}
|
||||
|
||||
type fakeAccountStore struct {
|
||||
records map[common.UserID]account.UserAccount
|
||||
updateErr error
|
||||
}
|
||||
|
||||
func newFakeAccountStore() *fakeAccountStore {
|
||||
return &fakeAccountStore{records: map[common.UserID]account.UserAccount{}}
|
||||
}
|
||||
|
||||
func (store *fakeAccountStore) Create(context.Context, ports.CreateAccountInput) error {
|
||||
return errors.New("unexpected Create in accountdeletion tests")
|
||||
}
|
||||
|
||||
func (store *fakeAccountStore) GetByUserID(_ context.Context, userID common.UserID) (account.UserAccount, error) {
|
||||
record, ok := store.records[userID]
|
||||
if !ok {
|
||||
return account.UserAccount{}, ports.ErrNotFound
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
record, ok := store.records[userID]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
return !record.IsDeleted(), nil
|
||||
}
|
||||
|
||||
func (store *fakeAccountStore) Update(_ context.Context, record account.UserAccount) error {
|
||||
if store.updateErr != nil {
|
||||
return store.updateErr
|
||||
}
|
||||
store.records[record.UserID] = record
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeLifecyclePublisher struct {
|
||||
events []ports.UserLifecycleEvent
|
||||
err error
|
||||
}
|
||||
|
||||
func (publisher *fakeLifecyclePublisher) PublishUserLifecycleEvent(_ context.Context, event ports.UserLifecycleEvent) error {
|
||||
if publisher.err != nil {
|
||||
return publisher.err
|
||||
}
|
||||
publisher.events = append(publisher.events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fixedClock struct {
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (clock fixedClock) Now() time.Time {
|
||||
return clock.now
|
||||
}
|
||||
Reference in New Issue
Block a user