package test import ( "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "errors" "sort" "testing" "time" ) type stubHasher struct { hashErr error hashCalls int compareCalls int } func (s *stubHasher) Hash(value string) (string, error) { s.hashCalls++ if s.hashErr != nil { return "", s.hashErr } return "hashed:" + value, nil } func (s *stubHasher) Compare(hashed string, value string) error { s.compareCalls++ if hashed == "hashed:"+value { return nil } return errors.New("hash mismatch") } type mockUserRepo struct { users map[uint32]user.User nextID uint32 findByIDsCalls int fail failSwitch clearVerificationCodeCalls int clearVerificationCodeUserIds []uint32 updatePasswordResetCodeCalls int lastUpdatePasswordResetCodeUserId uint32 lastUpdatePasswordResetCodeCode string lastUpdatePasswordResetCodeExpiration time.Time resetPasswordCalls int lastResetPasswordUserId uint32 lastResetPasswordHash string updateDisplayNameCalls int lastUpdateDisplayName string getAllCalls int updateRoleCalls int transferCalls int beforeUpdateRole func() beforeTransfer func() beforeClearVerification func() knownRoles *mockRoleRepo } func newMockUserRepo() *mockUserRepo { return &mockUserRepo{users: make(map[uint32]user.User)} } func newVerifiedUserRepo() *mockUserRepo { repo := newMockUserRepo() repo.users[1] = user.User{ID: 1, IsVerified: true} return repo } func (m *mockUserRepo) sortedIds() []uint32 { ids := make([]uint32, 0, len(m.users)) for id := range m.users { ids = append(ids, id) } sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) return ids } func (m *mockUserRepo) Get(id uint32) (user.User, error) { if err := m.fail.trigger("Get"); err != nil { return user.User{}, err } u, ok := m.users[id] if !ok { return user.User{}, user.ErrUserNotFound } return u, nil } func (m *mockUserRepo) GetByUsername(username string) (user.User, error) { if err := m.fail.trigger("GetByUsername"); err != nil { return user.User{}, err } for _, id := range m.sortedIds() { u := m.users[id] if u.Username == username { return u, nil } } return user.User{}, user.ErrUserNotFound } func (m *mockUserRepo) GetByEmail(email string) (user.User, error) { if err := m.fail.trigger("GetByEmail"); err != nil { return user.User{}, err } for _, id := range m.sortedIds() { u := m.users[id] if u.Email == email { return u, nil } } return user.User{}, user.ErrUserNotFound } func (m *mockUserRepo) GetAll() []user.User { m.getAllCalls++ result := make([]user.User, 0) for _, id := range m.sortedIds() { result = append(result, m.users[id]) } return result } func (m *mockUserRepo) FindByIDs(ids []uint32) ([]user.User, error) { if err := m.fail.trigger("FindByIDs"); err != nil { return nil, err } m.findByIDsCalls++ found := []user.User{} for _, id := range ids { if stored, exists := m.users[id]; exists { found = append(found, stored) } } return found, nil } func (m *mockUserRepo) Create(u user.User) (user.User, error) { if err := m.fail.trigger("Create"); err != nil { return user.User{}, err } m.nextID++ u.ID = m.nextID m.users[u.ID] = u return u, nil } func (m *mockUserRepo) Delete(id uint32) error { if err := m.fail.trigger("Delete"); err != nil { return err } if _, ok := m.users[id]; !ok { return user.ErrUserNotFound } delete(m.users, id) return nil } func (m *mockUserRepo) UpdateVerificationStatus(userId uint32, isVerified bool) (user.User, error) { if err := m.fail.trigger("UpdateVerificationStatus"); err != nil { return user.User{}, err } u, ok := m.users[userId] if !ok { return user.User{}, user.ErrUserNotFound } u.IsVerified = isVerified m.users[userId] = u return u, nil } func (m *mockUserRepo) UpdatePassword(userId uint32, password string) (user.User, error) { if err := m.fail.trigger("UpdatePassword"); err != nil { return user.User{}, err } u, ok := m.users[userId] if !ok { return user.User{}, user.ErrUserNotFound } u.Password = password m.users[userId] = u return u, nil } func (m *mockUserRepo) UpdatePasswordResetCode(userId uint32, code string, expiration time.Time) (user.User, error) { m.updatePasswordResetCodeCalls++ m.lastUpdatePasswordResetCodeUserId = userId m.lastUpdatePasswordResetCodeCode = code m.lastUpdatePasswordResetCodeExpiration = expiration if err := m.fail.trigger("UpdatePasswordResetCode"); err != nil { return user.User{}, err } u, ok := m.users[userId] if !ok { return user.User{}, user.ErrUserNotFound } u.PasswordResetCode = code u.PasswordResetExpiration = expiration m.users[userId] = u return u, nil } func (m *mockUserRepo) ClearVerificationCode(userId uint32) (user.User, error) { m.clearVerificationCodeCalls++ m.clearVerificationCodeUserIds = append(m.clearVerificationCodeUserIds, userId) if err := m.fail.trigger("ClearVerificationCode"); err != nil { return user.User{}, err } if m.beforeClearVerification != nil { hook := m.beforeClearVerification m.beforeClearVerification = nil hook() } u, ok := m.users[userId] if !ok { return user.User{}, user.ErrUserNotFound } if u.RoleID == ownerRoleID { for id, other := range m.users { if id != userId && other.RoleID == ownerRoleID && other.IsVerified { return user.User{}, user.ErrInvalidVerificationCode } } } u.IsVerified = true u.VerificationCode = "" m.users[userId] = u return u, nil } func (m *mockUserRepo) ResetPassword(userId uint32, hashedPassword string) (user.User, error) { m.resetPasswordCalls++ m.lastResetPasswordUserId = userId m.lastResetPasswordHash = hashedPassword if err := m.fail.trigger("ResetPassword"); err != nil { return user.User{}, err } u, ok := m.users[userId] if !ok { return user.User{}, user.ErrUserNotFound } u.Password = hashedPassword u.PasswordResetCode = "" u.PasswordResetExpiration = time.Time{} u.TokenVersion++ m.users[userId] = u return u, nil } func (m *mockUserRepo) UpdateDisplayName(userId uint32, displayName string) (user.User, error) { m.updateDisplayNameCalls++ m.lastUpdateDisplayName = displayName if err := m.fail.trigger("UpdateDisplayName"); err != nil { return user.User{}, err } u, ok := m.users[userId] if !ok { return user.User{}, user.ErrUserNotFound } u.DisplayName = displayName m.users[userId] = u return u, nil } func (m *mockUserRepo) roleExists(id uint32) bool { if m.knownRoles == nil { return true } _, present := m.knownRoles.roles[id] return present } func (m *mockUserRepo) UpdateRole(id uint32, from uint32, to uint32) (user.User, error) { m.updateRoleCalls++ if err := m.fail.trigger("UpdateRole"); err != nil { return user.User{}, err } if m.beforeUpdateRole != nil { hook := m.beforeUpdateRole m.beforeUpdateRole = nil hook() } stored, ok := m.users[id] if !ok { return user.User{}, user.ErrUserNotFound } if !m.roleExists(to) { return user.User{}, role.ErrRoleNotFound } if stored.RoleID != from { return user.User{}, user.ErrRoleChanged } stored.RoleID = to m.users[id] = stored return stored, nil } func (m *mockUserRepo) TransferOwnership(from uint32, to uint32, ownerRoleID uint32, formerRoleID uint32) error { m.transferCalls++ if err := m.fail.trigger("TransferOwnership"); err != nil { return err } if m.beforeTransfer != nil { hook := m.beforeTransfer m.beforeTransfer = nil hook() } former, formerFound := m.users[from] if !formerFound || former.RoleID != ownerRoleID { return user.ErrRoleChanged } successor, successorFound := m.users[to] if !successorFound || !successor.IsVerified { return user.ErrInvalidTransfer } former.RoleID = formerRoleID successor.RoleID = ownerRoleID m.users[from] = former m.users[to] = successor return nil } var _ user.Repository = &mockUserRepo{} func TestCreateUser(t *testing.T) { repo := newMockUserRepo() uc := useCases.NewCreateUserUseCase(repo, newSeededMockRoleRepo(), &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder) cmd := useCases.CreateUserCommand{ Username: "testuser", Password: "password123", Email: "test@example.com", } res, err := uc.CreateUser(testCtx, cmd) if err != nil { t.Fatalf("User creation failed: %v", err) } if res.Username != cmd.Username { t.Errorf("Username mismatch") } if res.VerificationCode == "" { t.Errorf("Verification code was not generated") } }