Files
apocapoc-api/internal/application/commands/register_user_test.go
T
david 5d92820591 Remove timezone from User model and pass as request parameter
Timezone is now sent from the client on each request that needs it,
instead of storing it in the database. This simplifies the model and
allows timezone to be dynamic (useful for traveling users).

Changes:
- Remove timezone field from User entity
- Remove timezone from user registration
- GET /habits/today now requires ?timezone= query param
- Add migration to drop timezone column from database
- Update related tests
2025-11-29 16:18:04 +01:00

298 lines
7.3 KiB
Go

package commands
import (
"context"
"errors"
"testing"
"apocapoc-api/internal/domain/entities"
appErrors "apocapoc-api/internal/shared/errors"
)
type mockUserRepo struct {
findByEmailFunc func(ctx context.Context, email string) (*entities.User, error)
createFunc func(ctx context.Context, user *entities.User) error
}
func (m *mockUserRepo) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
if m.findByEmailFunc != nil {
return m.findByEmailFunc(ctx, email)
}
return nil, appErrors.ErrNotFound
}
func (m *mockUserRepo) FindByID(ctx context.Context, id string) (*entities.User, error) {
return nil, nil
}
func (m *mockUserRepo) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
return nil, appErrors.ErrNotFound
}
func (m *mockUserRepo) Create(ctx context.Context, user *entities.User) error {
if m.createFunc != nil {
return m.createFunc(ctx, user)
}
return nil
}
func (m *mockUserRepo) Update(ctx context.Context, user *entities.User) error {
return nil
}
func (m *mockUserRepo) Delete(ctx context.Context, id string) error {
return nil
}
type mockPasswordHasher struct {
hashFunc func(password string) (string, error)
}
func (m *mockPasswordHasher) Hash(password string) (string, error) {
if m.hashFunc != nil {
return m.hashFunc(password)
}
return "hashed_" + password, nil
}
func (m *mockPasswordHasher) Compare(hashedPassword, password string) error {
return nil
}
func TestRegisterUserHandler_Success(t *testing.T) {
var createdUser *entities.User
repo := &mockUserRepo{
createFunc: func(ctx context.Context, user *entities.User) error {
user.ID = "test-user-id-123"
createdUser = user
return nil
},
}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
Password: "Secure123!",
}
result, err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if result.UserID == "" {
t.Error("expected user ID, got empty string")
}
if result.EmailVerificationRequired {
t.Error("expected email verification to not be required when emailService is nil")
}
if createdUser == nil {
t.Fatal("expected user to be created")
}
if createdUser.Email != cmd.Email {
t.Errorf("expected email %q, got %q", cmd.Email, createdUser.Email)
}
}
func TestRegisterUserHandler_InvalidEmail(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
tests := []struct {
name string
email string
}{
{"empty email", ""},
{"invalid format", "not-an-email"},
{"missing @", "testexample.com"},
{"missing domain", "test@"},
{"spaces in email", "test user@example.com"},
{"too long local part", string(make([]byte, 65)) + "@example.com"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := RegisterUserCommand{
Email: tt.email,
Password: "Secure123!",
}
_, err := handler.Handle(context.Background(), cmd)
if !errors.Is(err, appErrors.ErrInvalidInput) {
t.Errorf("expected ErrInvalidInput, got %v", err)
}
})
}
}
func TestRegisterUserHandler_InvalidPassword(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
tests := []struct {
name string
password string
}{
{"empty password", ""},
{"too short", "Short1!"},
{"no uppercase", "secure123!"},
{"no lowercase", "SECURE123!"},
{"no digit", "SecurePass!"},
{"no special char", "SecurePass1"},
{"only letters", "OnlyLetters"},
{"only numbers", "12345678"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := RegisterUserCommand{
Email: "test@example.com",
Password: tt.password,
}
_, err := handler.Handle(context.Background(), cmd)
if !errors.Is(err, appErrors.ErrInvalidInput) {
t.Errorf("expected ErrInvalidInput, got %v", err)
}
})
}
}
func TestRegisterUserHandler_EmailAlreadyExists(t *testing.T) {
existingUser := entities.NewUser("test@example.com", "hashed")
repo := &mockUserRepo{
findByEmailFunc: func(ctx context.Context, email string) (*entities.User, error) {
return existingUser, nil
},
}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
Password: "Secure123!",
}
_, err := handler.Handle(context.Background(), cmd)
if !errors.Is(err, appErrors.ErrAlreadyExists) {
t.Errorf("expected ErrAlreadyExists, got %v", err)
}
}
func TestRegisterUserHandler_PasswordHashingError(t *testing.T) {
expectedErr := errors.New("hashing error")
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{
hashFunc: func(password string) (string, error) {
return "", expectedErr
},
}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
Password: "Secure123!",
}
_, err := handler.Handle(context.Background(), cmd)
if err != expectedErr {
t.Errorf("expected hashing error, got %v", err)
}
}
func TestRegisterUserHandler_RepositoryError(t *testing.T) {
expectedErr := errors.New("repository error")
repo := &mockUserRepo{
createFunc: func(ctx context.Context, user *entities.User) error {
return expectedErr
},
}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
Password: "Secure123!",
}
_, err := handler.Handle(context.Background(), cmd)
if err != expectedErr {
t.Errorf("expected repository error, got %v", err)
}
}
func TestRegisterUserHandler_EdgeCases(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
tests := []struct {
name string
cmd RegisterUserCommand
wantErr error
}{
{
"email with plus addressing",
RegisterUserCommand{
Email: "user+tag@example.com",
Password: "Secure123!",
},
nil,
},
{
"email with subdomain",
RegisterUserCommand{
Email: "user@mail.example.com",
Password: "Secure123!",
},
nil,
},
{
"password with unicode",
RegisterUserCommand{
Email: "user@example.com",
Password: "Sëcure123!",
},
nil,
},
{
"very long valid password",
RegisterUserCommand{
Email: "user@example.com",
Password: "ValidP@ss1" + string(make([]byte, 100)),
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := handler.Handle(context.Background(), tt.cmd)
if err != tt.wantErr {
t.Errorf("expected error %v, got %v", tt.wantErr, err)
}
})
}
}
func TestRegisterUserHandler_ClosedRegistration(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "closed", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
Password: "Secure123!",
}
_, err := handler.Handle(context.Background(), cmd)
if !errors.Is(err, appErrors.ErrRegistrationClosed) {
t.Errorf("expected ErrRegistrationClosed, got %v", err)
}
}