Files
galaxy-game/user/internal/service/accountdeletion/service_test.go
T
2026-04-25 23:20:55 +02:00

230 lines
6.9 KiB
Go

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
}