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:
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -238,3 +239,11 @@ func (m *mockHabitRepo) FindActiveByUserIDWithPagination(ctx context.Context, us
|
|||||||
func (m *mockHabitRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockHabitRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockHabitRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHabitRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -141,3 +142,11 @@ func (m *mockDeleteUserRepo) FindActiveByUserIDWithPagination(ctx context.Contex
|
|||||||
func (m *mockDeleteUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockDeleteUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockDeleteUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockDeleteUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -554,3 +555,19 @@ func (m *mockHabitRepoForMark) FindActiveByUserIDWithPagination(ctx context.Cont
|
|||||||
func (m *mockHabitRepoForMark) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockHabitRepoForMark) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockEntryRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockEntryRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHabitRepoForMark) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHabitRepoForMark) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -304,3 +305,11 @@ func (m *mockUserRepo) FindActiveByUserIDWithPagination(ctx context.Context, use
|
|||||||
func (m *mockUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -351,3 +352,19 @@ func (m *mockRequestResetUserRepo) FindActiveByUserIDWithPagination(ctx context.
|
|||||||
func (m *mockRequestResetUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockRequestResetUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockRequestResetTokenRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRequestResetTokenRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRequestResetUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRequestResetUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -396,3 +397,19 @@ func (m *mockResetPasswordUserRepo) FindActiveByUserIDWithPagination(ctx context
|
|||||||
func (m *mockResetPasswordUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockResetPasswordUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockPasswordResetTokenRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPasswordResetTokenRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockResetPasswordUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockResetPasswordUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -195,3 +196,11 @@ func (m *mockRefreshTokenRepo) FindActiveByUserIDWithPagination(ctx context.Cont
|
|||||||
func (m *mockRefreshTokenRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockRefreshTokenRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -146,3 +147,11 @@ func (m *mockEntryRepoForUnmark) FindActiveByUserIDWithPagination(ctx context.Co
|
|||||||
func (m *mockEntryRepoForUnmark) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockEntryRepoForUnmark) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockEntryRepoForUnmark) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockEntryRepoForUnmark) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -345,3 +346,11 @@ func (m *mockVerifyEmailUserRepo) FindActiveByUserIDWithPagination(ctx context.C
|
|||||||
func (m *mockVerifyEmailUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockVerifyEmailUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockVerifyEmailUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockVerifyEmailUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -324,3 +325,11 @@ func TestGetTodaysHabitsHandler_CarryOverDisabled(t *testing.T) {
|
|||||||
t.Fatalf("Expected 0 habits (no carry-over), got %d", len(results))
|
t.Fatalf("Expected 0 habits (no carry-over), got %d", len(results))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockHabitRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHabitRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,17 @@ type HabitDTO struct {
|
|||||||
SpecificDays []int
|
SpecificDays []int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilterParams struct {
|
||||||
|
Type *value_objects.HabitType
|
||||||
|
Frequency *value_objects.Frequency
|
||||||
|
IncludeArchived bool
|
||||||
|
Search string
|
||||||
|
}
|
||||||
|
|
||||||
type GetUserHabitsQuery struct {
|
type GetUserHabitsQuery struct {
|
||||||
UserID string
|
UserID string
|
||||||
PaginationParams *pagination.Params
|
PaginationParams *pagination.Params
|
||||||
|
FilterParams *FilterParams
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUserHabitsResult struct {
|
type GetUserHabitsResult struct {
|
||||||
@@ -45,7 +53,29 @@ func (h *GetUserHabitsHandler) Handle(ctx context.Context, query GetUserHabitsQu
|
|||||||
var paginationResponse *pagination.Response
|
var paginationResponse *pagination.Response
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
if query.FilterParams != nil {
|
||||||
|
filter := repositories.HabitFilter{
|
||||||
|
Type: query.FilterParams.Type,
|
||||||
|
Frequency: query.FilterParams.Frequency,
|
||||||
|
IncludeArchived: query.FilterParams.IncludeArchived,
|
||||||
|
Search: query.FilterParams.Search,
|
||||||
|
}
|
||||||
|
|
||||||
|
habits, err = h.habitRepo.FindByUserIDFiltered(ctx, query.UserID, filter, query.PaginationParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if query.PaginationParams != nil {
|
if query.PaginationParams != nil {
|
||||||
|
totalItems, err := h.habitRepo.CountByUserIDFiltered(ctx, query.UserID, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := pagination.NewResponse(*query.PaginationParams, totalItems)
|
||||||
|
paginationResponse = &response
|
||||||
|
}
|
||||||
|
} else if query.PaginationParams != nil {
|
||||||
habits, err = h.habitRepo.FindActiveByUserIDWithPagination(ctx, query.UserID, *query.PaginationParams)
|
habits, err = h.habitRepo.FindActiveByUserIDWithPagination(ctx, query.UserID, *query.PaginationParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -277,3 +278,142 @@ func TestGetUserHabitsHandler_WithPagination(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockGetUserHabitsRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
var filtered []*entities.Habit
|
||||||
|
|
||||||
|
for _, habit := range m.habits {
|
||||||
|
if filter.Type != nil && habit.Type != *filter.Type {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filter.Frequency != nil && habit.Frequency != *filter.Frequency {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !filter.IncludeArchived && habit.ArchivedAt != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, habit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if paginationParams != nil {
|
||||||
|
offset := paginationParams.Offset()
|
||||||
|
limit := paginationParams.Limit()
|
||||||
|
|
||||||
|
if offset >= len(filtered) {
|
||||||
|
return []*entities.Habit{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end := offset + limit
|
||||||
|
if end > len(filtered) {
|
||||||
|
end = len(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered[offset:end], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGetUserHabitsRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
for _, habit := range m.habits {
|
||||||
|
if filter.Type != nil && habit.Type != *filter.Type {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filter.Frequency != nil && habit.Frequency != *filter.Frequency {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !filter.IncludeArchived && habit.ArchivedAt != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserHabitsHandler_WithFilters(t *testing.T) {
|
||||||
|
habit1 := entities.NewHabit("user-123", "Exercise", value_objects.HabitTypeBoolean, value_objects.FrequencyDaily, false, false)
|
||||||
|
habit1.ID = "habit-1"
|
||||||
|
|
||||||
|
habit2 := entities.NewHabit("user-123", "Read", value_objects.HabitTypeCounter, value_objects.FrequencyWeekly, false, false)
|
||||||
|
habit2.ID = "habit-2"
|
||||||
|
|
||||||
|
habit3 := entities.NewHabit("user-123", "Water", value_objects.HabitTypeValue, value_objects.FrequencyDaily, false, false)
|
||||||
|
habit3.ID = "habit-3"
|
||||||
|
|
||||||
|
habitRepo := &mockGetUserHabitsRepo{habits: []*entities.Habit{habit1, habit2, habit3}}
|
||||||
|
handler := NewGetUserHabitsHandler(habitRepo)
|
||||||
|
|
||||||
|
t.Run("FilterByType", func(t *testing.T) {
|
||||||
|
habitType := value_objects.HabitTypeBoolean
|
||||||
|
query := GetUserHabitsQuery{
|
||||||
|
UserID: "user-123",
|
||||||
|
FilterParams: &FilterParams{
|
||||||
|
Type: &habitType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Habits) != 1 {
|
||||||
|
t.Errorf("Expected 1 BOOLEAN habit, got %d", len(result.Habits))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Habits[0].Type != value_objects.HabitTypeBoolean {
|
||||||
|
t.Errorf("Expected BOOLEAN type, got %s", result.Habits[0].Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FilterByFrequency", func(t *testing.T) {
|
||||||
|
frequency := value_objects.FrequencyDaily
|
||||||
|
query := GetUserHabitsQuery{
|
||||||
|
UserID: "user-123",
|
||||||
|
FilterParams: &FilterParams{
|
||||||
|
Frequency: &frequency,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Habits) != 2 {
|
||||||
|
t.Errorf("Expected 2 DAILY habits, got %d", len(result.Habits))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FilterWithPagination", func(t *testing.T) {
|
||||||
|
frequency := value_objects.FrequencyDaily
|
||||||
|
params := pagination.NewParams(1, 1)
|
||||||
|
query := GetUserHabitsQuery{
|
||||||
|
UserID: "user-123",
|
||||||
|
FilterParams: &FilterParams{
|
||||||
|
Frequency: &frequency,
|
||||||
|
},
|
||||||
|
PaginationParams: ¶ms,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := handler.Handle(context.Background(), query)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Habits) != 1 {
|
||||||
|
t.Errorf("Expected 1 habit on first page, got %d", len(result.Habits))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Pagination == nil {
|
||||||
|
t.Fatal("Expected pagination metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Pagination.TotalItems != 2 {
|
||||||
|
t.Errorf("Expected 2 total DAILY habits, got %d", result.Pagination.TotalItems)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -213,3 +214,11 @@ func (m *mockLoginUserRepo) FindActiveByUserIDWithPagination(ctx context.Context
|
|||||||
func (m *mockLoginUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockLoginUserRepo) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockLoginUserRepo) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLoginUserRepo) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package queries
|
package queries
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -283,3 +284,19 @@ func (m *mockUserRepositoryForRefresh) FindActiveByUserIDWithPagination(ctx cont
|
|||||||
func (m *mockUserRepositoryForRefresh) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
func (m *mockUserRepositoryForRefresh) CountActiveByUserID(ctx context.Context, userID string) (int, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRefreshTokenRepository) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepositoryForRefresh) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepositoryForRefresh) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,16 +4,26 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"apocapoc-api/internal/domain/entities"
|
"apocapoc-api/internal/domain/entities"
|
||||||
|
"apocapoc-api/internal/domain/value_objects"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type HabitFilter struct {
|
||||||
|
Type *value_objects.HabitType
|
||||||
|
Frequency *value_objects.Frequency
|
||||||
|
IncludeArchived bool
|
||||||
|
Search string
|
||||||
|
}
|
||||||
|
|
||||||
type HabitRepository interface {
|
type HabitRepository interface {
|
||||||
Create(ctx context.Context, habit *entities.Habit) error
|
Create(ctx context.Context, habit *entities.Habit) error
|
||||||
FindByID(ctx context.Context, id string) (*entities.Habit, error)
|
FindByID(ctx context.Context, id string) (*entities.Habit, error)
|
||||||
FindByUserID(ctx context.Context, userID string) ([]*entities.Habit, error)
|
FindByUserID(ctx context.Context, userID string) ([]*entities.Habit, error)
|
||||||
FindActiveByUserID(ctx context.Context, userID string) ([]*entities.Habit, error)
|
FindActiveByUserID(ctx context.Context, userID string) ([]*entities.Habit, error)
|
||||||
FindActiveByUserIDWithPagination(ctx context.Context, userID string, params pagination.Params) ([]*entities.Habit, error)
|
FindActiveByUserIDWithPagination(ctx context.Context, userID string, params pagination.Params) ([]*entities.Habit, error)
|
||||||
|
FindByUserIDFiltered(ctx context.Context, userID string, filter HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error)
|
||||||
CountActiveByUserID(ctx context.Context, userID string) (int, error)
|
CountActiveByUserID(ctx context.Context, userID string) (int, error)
|
||||||
|
CountByUserIDFiltered(ctx context.Context, userID string, filter HabitFilter) (int, error)
|
||||||
Update(ctx context.Context, habit *entities.Habit) error
|
Update(ctx context.Context, habit *entities.Habit) error
|
||||||
Delete(ctx context.Context, id string) error
|
Delete(ctx context.Context, id string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"apocapoc-api/internal/application/commands"
|
"apocapoc-api/internal/application/commands"
|
||||||
"apocapoc-api/internal/application/queries"
|
"apocapoc-api/internal/application/queries"
|
||||||
|
"apocapoc-api/internal/domain/value_objects"
|
||||||
"apocapoc-api/internal/i18n"
|
"apocapoc-api/internal/i18n"
|
||||||
"apocapoc-api/internal/shared/errors"
|
"apocapoc-api/internal/shared/errors"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
@@ -109,12 +110,16 @@ func (h *HabitHandlers) CreateHabit(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// GetUserHabits godoc
|
// GetUserHabits godoc
|
||||||
// @Summary Get all user habits
|
// @Summary Get all user habits
|
||||||
// @Description Get all active habits for the authenticated user with optional pagination
|
// @Description Get all active habits for the authenticated user with optional pagination and filters
|
||||||
// @Tags habits
|
// @Tags habits
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Param page query int false "Page number (default: 1)"
|
// @Param page query int false "Page number (default: 1)"
|
||||||
// @Param page_size query int false "Page size (default: 50, max: 100)"
|
// @Param page_size query int false "Page size (default: 50, max: 100)"
|
||||||
|
// @Param type query string false "Filter by type (BOOLEAN, COUNTER, VALUE)"
|
||||||
|
// @Param frequency query string false "Filter by frequency (DAILY, WEEKLY, MONTHLY)"
|
||||||
|
// @Param archived query boolean false "Include archived habits (default: false)"
|
||||||
|
// @Param search query string false "Search by name or description"
|
||||||
// @Success 200 {object} GetUserHabitsResponse
|
// @Success 200 {object} GetUserHabitsResponse
|
||||||
// @Failure 401 {object} ErrorResponse
|
// @Failure 401 {object} ErrorResponse
|
||||||
// @Failure 500 {object} ErrorResponse
|
// @Failure 500 {object} ErrorResponse
|
||||||
@@ -153,6 +158,39 @@ func (h *HabitHandlers) GetUserHabits(w http.ResponseWriter, r *http.Request) {
|
|||||||
query.PaginationParams = ¶ms
|
query.PaginationParams = ¶ms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typeStr := r.URL.Query().Get("type")
|
||||||
|
frequencyStr := r.URL.Query().Get("frequency")
|
||||||
|
archivedStr := r.URL.Query().Get("archived")
|
||||||
|
searchStr := r.URL.Query().Get("search")
|
||||||
|
|
||||||
|
if typeStr != "" || frequencyStr != "" || archivedStr != "" || searchStr != "" {
|
||||||
|
filterParams := &queries.FilterParams{}
|
||||||
|
|
||||||
|
if typeStr != "" {
|
||||||
|
habitType := value_objects.HabitType(typeStr)
|
||||||
|
if habitType.IsValid() {
|
||||||
|
filterParams.Type = &habitType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if frequencyStr != "" {
|
||||||
|
frequency := value_objects.Frequency(frequencyStr)
|
||||||
|
if frequency.IsValid() {
|
||||||
|
filterParams.Frequency = &frequency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if archivedStr == "true" {
|
||||||
|
filterParams.IncludeArchived = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if searchStr != "" {
|
||||||
|
filterParams.Search = searchStr
|
||||||
|
}
|
||||||
|
|
||||||
|
query.FilterParams = filterParams
|
||||||
|
}
|
||||||
|
|
||||||
result, err := h.getUserHabitsHandler.Handle(r.Context(), query)
|
result, err := h.getUserHabitsHandler.Handle(r.Context(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_habits")
|
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_habits")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"apocapoc-api/internal/domain/entities"
|
"apocapoc-api/internal/domain/entities"
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/shared/errors"
|
"apocapoc-api/internal/shared/errors"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"apocapoc-api/internal/shared/pagination"
|
||||||
|
|
||||||
@@ -282,3 +283,93 @@ func (r *HabitRepository) CountActiveByUserID(ctx context.Context, userID string
|
|||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HabitRepository) FindByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter, paginationParams *pagination.Params) ([]*entities.Habit, error) {
|
||||||
|
baseQuery := `
|
||||||
|
SELECT id, user_id, name, description, type, frequency,
|
||||||
|
specific_days, specific_dates, carry_over, is_negative, target_value,
|
||||||
|
created_at, archived_at
|
||||||
|
FROM habits
|
||||||
|
WHERE user_id = ?`
|
||||||
|
|
||||||
|
args := []interface{}{userID}
|
||||||
|
conditions := []string{}
|
||||||
|
|
||||||
|
if !filter.IncludeArchived {
|
||||||
|
conditions = append(conditions, "archived_at IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Type != nil {
|
||||||
|
conditions = append(conditions, "type = ?")
|
||||||
|
args = append(args, string(*filter.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Frequency != nil {
|
||||||
|
conditions = append(conditions, "frequency = ?")
|
||||||
|
args = append(args, string(*filter.Frequency))
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Search != "" {
|
||||||
|
conditions = append(conditions, "(name LIKE ? OR description LIKE ?)")
|
||||||
|
searchPattern := "%" + filter.Search + "%"
|
||||||
|
args = append(args, searchPattern, searchPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range conditions {
|
||||||
|
baseQuery += " AND " + condition
|
||||||
|
}
|
||||||
|
|
||||||
|
baseQuery += " ORDER BY created_at DESC"
|
||||||
|
|
||||||
|
if paginationParams != nil {
|
||||||
|
baseQuery += " LIMIT ? OFFSET ?"
|
||||||
|
args = append(args, paginationParams.Limit(), paginationParams.Offset())
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, baseQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find habits: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
return r.scanHabits(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HabitRepository) CountByUserIDFiltered(ctx context.Context, userID string, filter repositories.HabitFilter) (int, error) {
|
||||||
|
baseQuery := `SELECT COUNT(*) FROM habits WHERE user_id = ?`
|
||||||
|
|
||||||
|
args := []interface{}{userID}
|
||||||
|
conditions := []string{}
|
||||||
|
|
||||||
|
if !filter.IncludeArchived {
|
||||||
|
conditions = append(conditions, "archived_at IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Type != nil {
|
||||||
|
conditions = append(conditions, "type = ?")
|
||||||
|
args = append(args, string(*filter.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Frequency != nil {
|
||||||
|
conditions = append(conditions, "frequency = ?")
|
||||||
|
args = append(args, string(*filter.Frequency))
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Search != "" {
|
||||||
|
conditions = append(conditions, "(name LIKE ? OR description LIKE ?)")
|
||||||
|
searchPattern := "%" + filter.Search + "%"
|
||||||
|
args = append(args, searchPattern, searchPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, condition := range conditions {
|
||||||
|
baseQuery += " AND " + condition
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := r.db.QueryRowContext(ctx, baseQuery, args...).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to count habits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apocapoc-api/internal/domain/entities"
|
"apocapoc-api/internal/domain/entities"
|
||||||
|
"apocapoc-api/internal/domain/repositories"
|
||||||
"apocapoc-api/internal/domain/value_objects"
|
"apocapoc-api/internal/domain/value_objects"
|
||||||
"apocapoc-api/internal/shared/errors"
|
"apocapoc-api/internal/shared/errors"
|
||||||
"apocapoc-api/internal/shared/pagination"
|
"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