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.
This commit is contained in:
2026-03-27 00:32:10 +01:00
parent 67fc508384
commit a07d033e93
10 changed files with 42 additions and 16 deletions
@@ -73,7 +73,7 @@ func (h *RegisterUserHandler) Handle(ctx context.Context, cmd RegisterUserComman
user := entities.NewUser(cmd.Email, hashedPassword)
emailVerificationRequired := false
if h.emailService != nil {
if h.emailService.IsEnabled() {
token, err := h.generateVerificationToken()
if err != nil {
return nil, fmt.Errorf("failed to generate verification token: %w", err)
@@ -2,6 +2,7 @@ package commands
import (
"apocapoc-api/internal/domain/repositories"
"apocapoc-api/internal/domain/services"
"apocapoc-api/internal/shared/pagination"
"context"
"errors"
@@ -71,7 +72,7 @@ func TestRegisterUserHandler_Success(t *testing.T) {
},
}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
@@ -88,7 +89,7 @@ func TestRegisterUserHandler_Success(t *testing.T) {
}
if result.EmailVerificationRequired {
t.Error("expected email verification to not be required when emailService is nil")
t.Error("expected email verification to not be required when email is disabled")
}
if createdUser == nil {
@@ -103,7 +104,7 @@ func TestRegisterUserHandler_Success(t *testing.T) {
func TestRegisterUserHandler_InvalidEmail(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
tests := []struct {
name string
@@ -135,7 +136,7 @@ func TestRegisterUserHandler_InvalidEmail(t *testing.T) {
func TestRegisterUserHandler_InvalidPassword(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
tests := []struct {
name string
@@ -174,7 +175,7 @@ func TestRegisterUserHandler_EmailAlreadyExists(t *testing.T) {
},
}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
@@ -195,7 +196,7 @@ func TestRegisterUserHandler_PasswordHashingError(t *testing.T) {
return "", expectedErr
},
}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
@@ -216,7 +217,7 @@ func TestRegisterUserHandler_RepositoryError(t *testing.T) {
},
}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
@@ -232,7 +233,7 @@ func TestRegisterUserHandler_RepositoryError(t *testing.T) {
func TestRegisterUserHandler_EdgeCases(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "open", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "open", false)
tests := []struct {
name string
@@ -285,7 +286,7 @@ func TestRegisterUserHandler_EdgeCases(t *testing.T) {
func TestRegisterUserHandler_ClosedRegistration(t *testing.T) {
repo := &mockUserRepo{}
hasher := &mockPasswordHasher{}
handler := NewRegisterUserHandler(repo, hasher, nil, "", "closed", false)
handler := NewRegisterUserHandler(repo, hasher, &services.NoOpEmailService{}, "", "closed", false)
cmd := RegisterUserCommand{
Email: "test@example.com",
@@ -89,6 +89,10 @@ func (m *mockRequestResetEmailService) HealthCheck() error {
return nil
}
func (m *mockRequestResetEmailService) IsEnabled() bool {
return true
}
func TestRequestPasswordResetHandler_Success(t *testing.T) {
user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123"
@@ -69,6 +69,10 @@ func (m *mockEmailService) HealthCheck() error {
return nil
}
func (m *mockEmailService) IsEnabled() bool {
return true
}
func TestVerifyEmailHandler_Success(t *testing.T) {
token := "valid-token"
expiry := time.Now().Add(24 * time.Hour)