Add filtering support to GET /api/v1/habits endpoint
Implemented comprehensive filtering capabilities for the habits list endpoint: - Filter by type (BOOLEAN, COUNTER, VALUE) - Filter by frequency (DAILY, WEEKLY, MONTHLY) - Filter by archived status - Text search in habit name and description - All filters can be combined - Filters work with pagination Technical changes: - Added FilterParams to GetUserHabitsQuery - Created HabitFilter struct in repository interface - Implemented dynamic SQL query building in SQLite repository - Updated HTTP handler to parse filter query parameters - Added comprehensive tests for repository and handler filtering - Updated all test mocks with new filter methods
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/domain/entities"
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"apocapoc-api/internal/domain/value_objects"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
"apocapoc-api/internal/shared/pagination"
|
||||
@@ -422,3 +423,213 @@ func TestHabitRepositoryCountActiveByUserID(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHabitRepositoryFindByUserIDFiltered(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := NewHabitRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
userID := "user-filter-test"
|
||||
|
||||
habit1 := entities.NewHabit(userID, "Morning Exercise", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
|
||||
habit1.Description = "Daily morning workout"
|
||||
repo.Create(ctx, habit1)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
habit2 := entities.NewHabit(userID, "Read Books", value_objects.HabitTypeCounter, value_objects.FrequencyWeekly, false, false)
|
||||
habit2.Description = "Read at least 3 books per week"
|
||||
repo.Create(ctx, habit2)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
habit3 := entities.NewHabit(userID, "Drink Water", value_objects.HabitTypeValue, value_objects.FrequencyDaily, false, false)
|
||||
habit3.Description = "Drink 2 liters of water daily"
|
||||
repo.Create(ctx, habit3)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
habit4 := entities.NewHabit(userID, "Weekly Run", value_objects.HabitTypeBoolean, value_objects.FrequencyWeekly, false, false)
|
||||
repo.Create(ctx, habit4)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
now := time.Now()
|
||||
habit4.ArchivedAt = &now
|
||||
repo.Update(ctx, habit4)
|
||||
|
||||
t.Run("FilterByType", func(t *testing.T) {
|
||||
habitType := value_objects.HabitTypeBoolean
|
||||
filter := repositories.HabitFilter{
|
||||
Type: &habitType,
|
||||
}
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 1 {
|
||||
t.Errorf("Expected 1 active BOOLEAN habit, got %d", len(habits))
|
||||
}
|
||||
|
||||
if habits[0].Type != value_objects.HabitTypeBoolean {
|
||||
t.Errorf("Expected BOOLEAN type, got %s", habits[0].Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterByFrequency", func(t *testing.T) {
|
||||
frequency := value_objects.FrequencyDaily
|
||||
filter := repositories.HabitFilter{
|
||||
Frequency: &frequency,
|
||||
}
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 2 {
|
||||
t.Errorf("Expected 2 DAILY habits, got %d", len(habits))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterIncludeArchived", func(t *testing.T) {
|
||||
filter := repositories.HabitFilter{
|
||||
IncludeArchived: true,
|
||||
}
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 4 {
|
||||
t.Errorf("Expected 4 habits (including archived), got %d", len(habits))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterBySearch", func(t *testing.T) {
|
||||
filter := repositories.HabitFilter{
|
||||
Search: "Exercise",
|
||||
}
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 1 {
|
||||
t.Errorf("Expected 1 habit matching 'Exercise', got %d", len(habits))
|
||||
}
|
||||
|
||||
if habits[0].Name != "Morning Exercise" {
|
||||
t.Errorf("Expected 'Morning Exercise', got %s", habits[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterBySearchInDescription", func(t *testing.T) {
|
||||
filter := repositories.HabitFilter{
|
||||
Search: "books",
|
||||
}
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 1 {
|
||||
t.Errorf("Expected 1 habit matching 'books' in description, got %d", len(habits))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CombineFilters", func(t *testing.T) {
|
||||
habitType := value_objects.HabitTypeBoolean
|
||||
frequency := value_objects.FrequencyWeekly
|
||||
filter := repositories.HabitFilter{
|
||||
Type: &habitType,
|
||||
Frequency: &frequency,
|
||||
IncludeArchived: true,
|
||||
}
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 1 {
|
||||
t.Errorf("Expected 1 BOOLEAN WEEKLY habit (archived), got %d", len(habits))
|
||||
}
|
||||
|
||||
if habits[0].Name != "Weekly Run" {
|
||||
t.Errorf("Expected 'Weekly Run', got %s", habits[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WithPagination", func(t *testing.T) {
|
||||
filter := repositories.HabitFilter{}
|
||||
params := pagination.NewParams(1, 2)
|
||||
|
||||
habits, err := repo.FindByUserIDFiltered(ctx, userID, filter, ¶ms)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if len(habits) != 2 {
|
||||
t.Errorf("Expected 2 habits on first page, got %d", len(habits))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHabitRepositoryCountByUserIDFiltered(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
repo := NewHabitRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
userID := "user-count-filter-test"
|
||||
|
||||
habit1 := entities.NewHabit(userID, "Test1", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
|
||||
repo.Create(ctx, habit1)
|
||||
|
||||
habit2 := entities.NewHabit(userID, "Test2", value_objects.HabitTypeCounter, value_objects.FrequencyDaily, false, false)
|
||||
repo.Create(ctx, habit2)
|
||||
|
||||
habit3 := entities.NewHabit(userID, "Test3", value_objects.HabitTypeBoolean, value_objects.FrequencyWeekly, false, false)
|
||||
now := time.Now()
|
||||
habit3.ArchivedAt = &now
|
||||
repo.Create(ctx, habit3)
|
||||
repo.Update(ctx, habit3)
|
||||
|
||||
t.Run("CountByType", func(t *testing.T) {
|
||||
habitType := value_objects.HabitTypeBoolean
|
||||
filter := repositories.HabitFilter{
|
||||
Type: &habitType,
|
||||
}
|
||||
|
||||
count, err := repo.CountByUserIDFiltered(ctx, userID, filter)
|
||||
if err != nil {
|
||||
t.Fatalf("CountByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
t.Errorf("Expected 1 active BOOLEAN habit, got %d", count)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CountWithArchived", func(t *testing.T) {
|
||||
habitType := value_objects.HabitTypeBoolean
|
||||
filter := repositories.HabitFilter{
|
||||
Type: &habitType,
|
||||
IncludeArchived: true,
|
||||
}
|
||||
|
||||
count, err := repo.CountByUserIDFiltered(ctx, userID, filter)
|
||||
if err != nil {
|
||||
t.Fatalf("CountByUserIDFiltered failed: %v", err)
|
||||
}
|
||||
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 BOOLEAN habits (including archived), got %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user