Files
apocapoc-api/internal/application/commands/register_user_test.go
T
david a07d033e93 Add NoOp email service, /docs shortcut and fix streak timezone bug
Replace nil email service pattern with NoOpEmailService (Null Object)
to eliminate nil pointer panics across all handlers.

Add /docs route as a shortcut to Swagger UI.

Fix streak calculation returning 0 when server timezone differs from
UTC — CreatedAt was not converted to UTC before date extraction.
2026-03-27 00:32:10 +01:00

317 lines
8.2 KiB
Go

package commands
import (
"apocapoc-api/internal/domain/repositories"
"apocapoc-api/internal/domain/services"
"apocapoc-api/internal/shared/pagination"
"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, &services.NoOpEmailService{}, "", "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 email is disabled")
}
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, &services.NoOpEmailService{}, "", "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, &services.NoOpEmailService{}, "", "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, &services.NoOpEmailService{}, "", "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, &services.NoOpEmailService{}, "", "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, &services.NoOpEmailService{}, "", "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, &services.NoOpEmailService{}, "", "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, &services.NoOpEmailService{}, "", "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)
}
}
func (m *mockUserRepo) FindActiveByUserIDWithPagination(ctx context.Context, userID string, params pagination.Params) ([]*entities.Habit, error) {
return nil, nil
}
func (m *mockUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
return 0, nil
}
func (m *mockUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
return nil, nil
}
func (m *mockUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
return 0, nil
}