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 }