From 664f7a0de0c0cfb920006be010b1c99a1d04a4a5 Mon Sep 17 00:00:00 2001 From: David Folch Agulles Date: Tue, 25 Nov 2025 10:12:04 +0100 Subject: [PATCH] Add domain layer tests (TDD - Red phase) Following TDD principles, adding tests before implementation: - Add HabitType value object tests (validation) - Add Frequency value object tests (validation) - Add User entity tests (creation, default timezone) - Add Habit entity tests (creation, archive, active status) - Add HabitEntry entity tests (creation, soft delete) Tests cover core domain logic and business rules. All tests will fail until implementation is added. --- internal/domain/entities/habit_entry_test.go | 67 +++++++++++ internal/domain/entities/habit_test.go | 105 ++++++++++++++++++ internal/domain/entities/user_test.go | 48 ++++++++ .../domain/value_objects/frequency_test.go | 26 +++++ .../domain/value_objects/habit_type_test.go | 26 +++++ 5 files changed, 272 insertions(+) create mode 100644 internal/domain/entities/habit_entry_test.go create mode 100644 internal/domain/entities/habit_test.go create mode 100644 internal/domain/entities/user_test.go create mode 100644 internal/domain/value_objects/frequency_test.go create mode 100644 internal/domain/value_objects/habit_type_test.go diff --git a/internal/domain/entities/habit_entry_test.go b/internal/domain/entities/habit_entry_test.go new file mode 100644 index 0000000..c79a730 --- /dev/null +++ b/internal/domain/entities/habit_entry_test.go @@ -0,0 +1,67 @@ +package entities + +import ( + "testing" + "time" +) + +func TestNewHabitEntry(t *testing.T) { + habitID := "habit-123" + scheduledDate := time.Now().Truncate(24 * time.Hour) + value := 5.0 + + entry := NewHabitEntry(habitID, scheduledDate, &value) + + if entry.HabitID != habitID { + t.Errorf("Expected HabitID %s, got %s", habitID, entry.HabitID) + } + + if !entry.ScheduledDate.Equal(scheduledDate) { + t.Errorf("Expected ScheduledDate %v, got %v", scheduledDate, entry.ScheduledDate) + } + + if entry.Value == nil { + t.Error("Value should not be nil") + } + + if *entry.Value != value { + t.Errorf("Expected Value %f, got %f", value, *entry.Value) + } + + if entry.CompletedAt.IsZero() { + t.Error("CompletedAt should not be zero") + } + + if entry.DeletedAt != nil { + t.Error("DeletedAt should be nil for new entry") + } +} + +func TestNewHabitEntry_BooleanHabit(t *testing.T) { + habitID := "habit-123" + scheduledDate := time.Now().Truncate(24 * time.Hour) + + entry := NewHabitEntry(habitID, scheduledDate, nil) + + if entry.Value != nil { + t.Error("Value should be nil for boolean habit") + } +} + +func TestHabitEntry_SoftDelete(t *testing.T) { + entry := NewHabitEntry("habit-123", time.Now(), nil) + + if entry.DeletedAt != nil { + t.Error("New entry should not be deleted") + } + + entry.SoftDelete() + + if entry.DeletedAt == nil { + t.Error("Entry should be deleted after calling SoftDelete()") + } + + if entry.DeletedAt.After(time.Now()) { + t.Error("DeletedAt should not be in the future") + } +} diff --git a/internal/domain/entities/habit_test.go b/internal/domain/entities/habit_test.go new file mode 100644 index 0000000..5836107 --- /dev/null +++ b/internal/domain/entities/habit_test.go @@ -0,0 +1,105 @@ +package entities + +import ( + "testing" + "time" + + "habit-tracker-api/internal/domain/value_objects" +) + +func TestNewHabit(t *testing.T) { + userID := "user-123" + name := "Morning Exercise" + habitType := value_objects.HabitTypeBoolean + frequency := value_objects.FrequencyDaily + carryOver := false + + habit := NewHabit(userID, name, habitType, frequency, carryOver) + + if habit.UserID != userID { + t.Errorf("Expected UserID %s, got %s", userID, habit.UserID) + } + + if habit.Name != name { + t.Errorf("Expected Name %s, got %s", name, habit.Name) + } + + if habit.Type != habitType { + t.Errorf("Expected Type %s, got %s", habitType, habit.Type) + } + + if habit.Frequency != frequency { + t.Errorf("Expected Frequency %s, got %s", frequency, habit.Frequency) + } + + if habit.CarryOver != carryOver { + t.Errorf("Expected CarryOver %v, got %v", carryOver, habit.CarryOver) + } + + if habit.CreatedAt.IsZero() { + t.Error("CreatedAt should not be zero") + } + + if habit.ArchivedAt != nil { + t.Error("ArchivedAt should be nil for new habit") + } +} + +func TestHabit_Archive(t *testing.T) { + habit := NewHabit("user-123", "Test Habit", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false) + + if habit.ArchivedAt != nil { + t.Error("New habit should not be archived") + } + + habit.Archive() + + if habit.ArchivedAt == nil { + t.Error("Habit should be archived after calling Archive()") + } + + if habit.ArchivedAt.After(time.Now()) { + t.Error("ArchivedAt should not be in the future") + } +} + +func TestHabit_IsActive(t *testing.T) { + habit := NewHabit("user-123", "Test Habit", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false) + + if !habit.IsActive() { + t.Error("New habit should be active") + } + + habit.Archive() + + if habit.IsActive() { + t.Error("Archived habit should not be active") + } +} + +func TestHabit_WithSpecificDays(t *testing.T) { + habit := NewHabit("user-123", "Workout", value_objects.HabitTypeBoolean, value_objects.FrequencyWeekly, false) + habit.SpecificDays = []int{1, 3, 5} // Monday, Wednesday, Friday + + if len(habit.SpecificDays) != 3 { + t.Errorf("Expected 3 specific days, got %d", len(habit.SpecificDays)) + } + + if habit.SpecificDays[0] != 1 || habit.SpecificDays[1] != 3 || habit.SpecificDays[2] != 5 { + t.Errorf("Expected days [1,3,5], got %v", habit.SpecificDays) + } +} + +func TestHabit_WithTargetValue(t *testing.T) { + habit := NewHabit("user-123", "Drink Water", value_objects.HabitTypeCounter, value_objects.FrequencyDaily, false) + targetValue := 8.0 + habit.TargetValue = &targetValue + + if habit.TargetValue == nil { + t.Error("TargetValue should not be nil") + } + + if *habit.TargetValue != 8.0 { + t.Errorf("Expected TargetValue 8.0, got %f", *habit.TargetValue) + } +} diff --git a/internal/domain/entities/user_test.go b/internal/domain/entities/user_test.go new file mode 100644 index 0000000..1b479b5 --- /dev/null +++ b/internal/domain/entities/user_test.go @@ -0,0 +1,48 @@ +package entities + +import ( + "testing" + "time" +) + +func TestNewUser(t *testing.T) { + email := "test@example.com" + passwordHash := "hashed_password_123" + timezone := "Europe/Madrid" + + user := NewUser(email, passwordHash, timezone) + + if user.Email != email { + t.Errorf("Expected email %s, got %s", email, user.Email) + } + + if user.PasswordHash != passwordHash { + t.Errorf("Expected password hash %s, got %s", passwordHash, user.PasswordHash) + } + + if user.Timezone != timezone { + t.Errorf("Expected timezone %s, got %s", timezone, user.Timezone) + } + + if user.CreatedAt.IsZero() { + t.Error("CreatedAt should not be zero") + } + + if user.UpdatedAt.IsZero() { + t.Error("UpdatedAt should not be zero") + } + + // CreatedAt and UpdatedAt should be very close in time + diff := user.UpdatedAt.Sub(user.CreatedAt) + if diff < 0 || diff > time.Second { + t.Errorf("CreatedAt and UpdatedAt should be nearly identical, diff: %v", diff) + } +} + +func TestUser_DefaultTimezone(t *testing.T) { + user := NewUser("test@example.com", "hash", "") + + if user.Timezone != "UTC" { + t.Errorf("Expected default timezone UTC, got %s", user.Timezone) + } +} diff --git a/internal/domain/value_objects/frequency_test.go b/internal/domain/value_objects/frequency_test.go new file mode 100644 index 0000000..f93610f --- /dev/null +++ b/internal/domain/value_objects/frequency_test.go @@ -0,0 +1,26 @@ +package value_objects + +import "testing" + +func TestFrequency_IsValid(t *testing.T) { + tests := []struct { + name string + freq Frequency + expected bool + }{ + {"Daily frequency is valid", FrequencyDaily, true}, + {"Weekly frequency is valid", FrequencyWeekly, true}, + {"Monthly frequency is valid", FrequencyMonthly, true}, + {"Empty string is invalid", Frequency(""), false}, + {"Random string is invalid", Frequency("YEARLY"), false}, + {"Lowercase is invalid", Frequency("daily"), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.freq.IsValid(); got != tt.expected { + t.Errorf("Frequency.IsValid() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/internal/domain/value_objects/habit_type_test.go b/internal/domain/value_objects/habit_type_test.go new file mode 100644 index 0000000..254577f --- /dev/null +++ b/internal/domain/value_objects/habit_type_test.go @@ -0,0 +1,26 @@ +package value_objects + +import "testing" + +func TestHabitType_IsValid(t *testing.T) { + tests := []struct { + name string + habType HabitType + expected bool + }{ + {"Boolean type is valid", HabitTypeBoolean, true}, + {"Counter type is valid", HabitTypeCounter, true}, + {"Value type is valid", HabitTypeValue, true}, + {"Empty string is invalid", HabitType(""), false}, + {"Random string is invalid", HabitType("RANDOM"), false}, + {"Lowercase is invalid", HabitType("boolean"), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.habType.IsValid(); got != tt.expected { + t.Errorf("HabitType.IsValid() = %v, want %v", got, tt.expected) + } + }) + } +}