Files
RenewCMS/test/user_test.go
T

458 lines
12 KiB
Go

package test
import (
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/useCases"
"errors"
"sort"
"strings"
"sync"
"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
getCalls int
updateRoleCalls int
transferCalls int
beforeUpdateRole func()
deleteIfRoleCalls int
beforeDeleteIfRole func()
beforeTransfer func()
beforeClearVerification func()
knownRoles *mockRoleRepo
confirmLock sync.Mutex
}
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) {
m.confirmLock.Lock()
defer m.confirmLock.Unlock()
m.getCalls++
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) IsEmailInUse(email string, exceptUserId uint32) (bool, error) {
m.confirmLock.Lock()
defer m.confirmLock.Unlock()
return m.emailInUse(email, exceptUserId)
}
func (m *mockUserRepo) emailInUse(email string, exceptUserId uint32) (bool, error) {
if err := m.fail.trigger("IsEmailInUse"); err != nil {
return false, err
}
for id, stored := range m.users {
if id != exceptUserId && strings.EqualFold(stored.Email, email) {
return true, nil
}
}
return false, nil
}
func (m *mockUserRepo) SetPendingEmailChange(userId uint32, pendingEmail string, code string, expiration time.Time) (user.User, error) {
if err := m.fail.trigger("SetPendingEmailChange"); err != nil {
return user.User{}, err
}
stored, ok := m.users[userId]
if !ok {
return user.User{}, user.ErrUserNotFound
}
stored.PendingEmail = pendingEmail
stored.EmailChangeCode = code
stored.EmailChangeExpiration = expiration
m.users[userId] = stored
return stored, nil
}
func (m *mockUserRepo) ClearPendingEmailChange(userId uint32) (user.User, error) {
if err := m.fail.trigger("ClearPendingEmailChange"); err != nil {
return user.User{}, err
}
stored, ok := m.users[userId]
if !ok {
return user.User{}, user.ErrUserNotFound
}
stored.PendingEmail = ""
stored.EmailChangeCode = ""
stored.EmailChangeExpiration = time.Time{}
m.users[userId] = stored
return stored, nil
}
func (m *mockUserRepo) ConfirmEmailChange(userId uint32, pendingEmail string, code string, now time.Time) (user.User, error) {
m.confirmLock.Lock()
defer m.confirmLock.Unlock()
if err := m.fail.trigger("ConfirmEmailChange"); err != nil {
return user.User{}, err
}
stored, ok := m.users[userId]
if !ok {
return user.User{}, user.ErrUserNotFound
}
if stored.PendingEmail != pendingEmail || stored.EmailChangeCode != code || !now.Before(stored.EmailChangeExpiration) {
return user.User{}, user.ErrInvalidEmailChangeCode
}
taken, _ := m.emailInUse(pendingEmail, userId)
if !taken {
stored.Email = pendingEmail
}
stored.PendingEmail = ""
stored.EmailChangeCode = ""
stored.EmailChangeExpiration = time.Time{}
m.users[userId] = stored
if taken {
return user.User{}, user.ErrEmailUnavailable
}
return stored, nil
}
func (m *mockUserRepo) GetAll() ([]user.User, error) {
m.getAllCalls++
if err := m.fail.trigger("GetAll"); err != nil {
return nil, err
}
result := make([]user.User, 0)
for _, id := range m.sortedIds() {
result = append(result, m.users[id])
}
return result, nil
}
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) DeleteIfRole(id uint32, roleID uint32) error {
m.deleteIfRoleCalls++
if err := m.fail.trigger("DeleteIfRole"); err != nil {
return err
}
if m.beforeDeleteIfRole != nil {
hook := m.beforeDeleteIfRole
m.beforeDeleteIfRole = nil
hook()
}
stored, ok := m.users[id]
if !ok {
return user.ErrUserNotFound
}
if stored.RoleID != roleID {
return user.ErrRoleChanged
}
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, assignedBy 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
stored.RoleAssignedByID = assignedBy
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
former.RoleAssignedByID = 0
successor.RoleID = ownerRoleID
successor.RoleAssignedByID = from
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")
}
}