//go:build unit package service import ( "context" "fmt" "testing" "time" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/stretchr/testify/require" ) type ensureEmailCall struct { userID int64 email string } type replaceEmailCall struct { userID int64 oldEmail string newEmail string } type emailSyncRepoStub struct { user *User nextID int64 updateCalls int created []*User updated []*User ensureCalls []ensureEmailCall replaceCalls []replaceEmailCall } func (s *emailSyncRepoStub) Create(_ context.Context, user *User) error { if s.nextID != 0 && user.ID == 0 { user.ID = s.nextID } s.created = append(s.created, user) s.user = user return nil } func (s *emailSyncRepoStub) GetByID(_ context.Context, _ int64) (*User, error) { if s.user == nil { return nil, ErrUserNotFound } cloned := *s.user return &cloned, nil } func (s *emailSyncRepoStub) GetByEmail(_ context.Context, _ string) (*User, error) { return nil, ErrUserNotFound } func (s *emailSyncRepoStub) GetFirstAdmin(context.Context) (*User, error) { return nil, fmt.Errorf("unexpected GetFirstAdmin call") } func (s *emailSyncRepoStub) Update(_ context.Context, user *User) error { s.updateCalls++ s.updated = append(s.updated, user) s.user = user return nil } func (s *emailSyncRepoStub) Delete(context.Context, int64) error { return nil } func (s *emailSyncRepoStub) GetUserAvatar(context.Context, int64) (*UserAvatar, error) { return nil, fmt.Errorf("unexpected GetUserAvatar call") } func (s *emailSyncRepoStub) UpsertUserAvatar(context.Context, int64, UpsertUserAvatarInput) (*UserAvatar, error) { return nil, fmt.Errorf("unexpected UpsertUserAvatar call") } func (s *emailSyncRepoStub) DeleteUserAvatar(context.Context, int64) error { return fmt.Errorf("unexpected DeleteUserAvatar call") } func (s *emailSyncRepoStub) List(context.Context, pagination.PaginationParams) ([]User, *pagination.PaginationResult, error) { return nil, nil, fmt.Errorf("unexpected List call") } func (s *emailSyncRepoStub) ListWithFilters(context.Context, pagination.PaginationParams, UserListFilters) ([]User, *pagination.PaginationResult, error) { return nil, nil, fmt.Errorf("unexpected ListWithFilters call") } func (s *emailSyncRepoStub) GetLatestUsedAtByUserIDs(context.Context, []int64) (map[int64]*time.Time, error) { return map[int64]*time.Time{}, nil } func (s *emailSyncRepoStub) GetLatestUsedAtByUserID(context.Context, int64) (*time.Time, error) { return nil, nil } func (s *emailSyncRepoStub) UpdateBalance(context.Context, int64, float64) error { return nil } func (s *emailSyncRepoStub) DeductBalance(context.Context, int64, float64) error { return nil } func (s *emailSyncRepoStub) UpdateConcurrency(context.Context, int64, int) error { return nil } func (s *emailSyncRepoStub) ExistsByEmail(context.Context, string) (bool, error) { return false, nil } func (s *emailSyncRepoStub) RemoveGroupFromAllowedGroups(context.Context, int64) (int64, error) { return 0, nil } func (s *emailSyncRepoStub) AddGroupToAllowedGroups(context.Context, int64, int64) error { return nil } func (s *emailSyncRepoStub) RemoveGroupFromUserAllowedGroups(context.Context, int64, int64) error { return nil } func (s *emailSyncRepoStub) ListUserAuthIdentities(context.Context, int64) ([]UserAuthIdentityRecord, error) { return nil, nil } func (s *emailSyncRepoStub) UpdateTotpSecret(context.Context, int64, *string) error { return nil } func (s *emailSyncRepoStub) EnableTotp(context.Context, int64) error { return nil } func (s *emailSyncRepoStub) DisableTotp(context.Context, int64) error { return nil } func (s *emailSyncRepoStub) EnsureEmailAuthIdentity(_ context.Context, userID int64, email string) error { s.ensureCalls = append(s.ensureCalls, ensureEmailCall{userID: userID, email: email}) return nil } func (s *emailSyncRepoStub) ReplaceEmailAuthIdentity(_ context.Context, userID int64, oldEmail, newEmail string) error { s.replaceCalls = append(s.replaceCalls, replaceEmailCall{ userID: userID, oldEmail: oldEmail, newEmail: newEmail, }) return nil } func TestAdminService_CreateUser_EnsuresEmailAuthIdentity(t *testing.T) { repo := &emailSyncRepoStub{nextID: 55} svc := &adminServiceImpl{userRepo: repo} user, err := svc.CreateUser(context.Background(), &CreateUserInput{ Email: "admin-created@example.com", Password: "strong-pass", }) require.NoError(t, err) require.NotNil(t, user) require.Equal(t, []ensureEmailCall{{ userID: 55, email: "admin-created@example.com", }}, repo.ensureCalls) require.Empty(t, repo.replaceCalls) } func TestAdminService_UpdateUser_ReplacesEmailAuthIdentity(t *testing.T) { repo := &emailSyncRepoStub{ user: &User{ ID: 91, Email: "before@example.com", Role: RoleUser, Status: StatusActive, Concurrency: 3, }, } svc := &adminServiceImpl{userRepo: repo} updated, err := svc.UpdateUser(context.Background(), 91, &UpdateUserInput{ Email: "after@example.com", }) require.NoError(t, err) require.NotNil(t, updated) require.Equal(t, "after@example.com", updated.Email) require.Equal(t, []replaceEmailCall{{ userID: 91, oldEmail: "before@example.com", newEmail: "after@example.com", }}, repo.replaceCalls) require.Empty(t, repo.ensureCalls) }