This refactor eliminates duplicate task logic across the codebase by creating a centralized task package with three layers: - predicates/: Pure Go functions defining task state logic (IsCompleted, IsOverdue, IsDueSoon, IsUpcoming, IsActive, IsInProgress, EffectiveDate) - scopes/: GORM scope functions mirroring predicates for database queries - categorization/: Chain of Responsibility pattern for kanban column assignment Key fixes: - Fixed PostgreSQL DATE vs TIMESTAMP comparison bug in scopes (added explicit ::timestamp casts) that caused summary/kanban count mismatches - Fixed models/task.go IsOverdue() and IsDueSoon() to use EffectiveDate (NextDueDate ?? DueDate) instead of only DueDate - Removed duplicate isTaskCompleted() helpers from task_repo.go and task_button_types.go Files refactored to use consolidated logic: - task_repo.go: Uses scopes for statistics, predicates for filtering - task_button_types.go: Uses predicates instead of inline logic - responses/task.go: Delegates to categorization package - dashboard_handler.go: Uses scopes for task statistics - residence_service.go: Uses predicates for report generation - worker/jobs/handler.go: Documented SQL with predicate references Added comprehensive tests: - predicates_test.go: Unit tests for all predicate functions - scopes_test.go: Integration tests verifying scopes match predicates - consistency_test.go: Three-layer consistency tests ensuring predicates, scopes, and categorization all return identical results 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
707 lines
20 KiB
Go
707 lines
20 KiB
Go
package scopes_test
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/treytartt/casera-api/internal/models"
|
|
"github.com/treytartt/casera-api/internal/task/predicates"
|
|
"github.com/treytartt/casera-api/internal/task/scopes"
|
|
)
|
|
|
|
// testDB holds the database connection for integration tests
|
|
var testDB *gorm.DB
|
|
|
|
// TestMain sets up the database connection for all tests in this package
|
|
func TestMain(m *testing.M) {
|
|
// Get database URL from environment or use default
|
|
dsn := os.Getenv("TEST_DATABASE_URL")
|
|
if dsn == "" {
|
|
dsn = "host=localhost user=postgres password=postgres dbname=mycrib_test port=5432 sslmode=disable"
|
|
}
|
|
|
|
var err error
|
|
testDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
// Print message and skip tests if database is not available
|
|
println("Skipping scope integration tests: database not available")
|
|
println("Set TEST_DATABASE_URL to run these tests")
|
|
println("Error:", err.Error())
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Verify connection works
|
|
sqlDB, err := testDB.DB()
|
|
if err != nil {
|
|
println("Failed to get underlying DB:", err.Error())
|
|
os.Exit(0)
|
|
}
|
|
if err := sqlDB.Ping(); err != nil {
|
|
println("Failed to ping database:", err.Error())
|
|
os.Exit(0)
|
|
}
|
|
|
|
println("Database connected successfully, running integration tests...")
|
|
|
|
// Run migrations for test tables
|
|
err = testDB.AutoMigrate(
|
|
&models.Task{},
|
|
&models.TaskCompletion{},
|
|
&models.TaskStatus{},
|
|
&models.Residence{},
|
|
)
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Run tests
|
|
code := m.Run()
|
|
|
|
// Cleanup
|
|
cleanupTestData()
|
|
|
|
os.Exit(code)
|
|
}
|
|
|
|
// cleanupTestData removes all test data
|
|
func cleanupTestData() {
|
|
if testDB == nil {
|
|
return
|
|
}
|
|
testDB.Exec("DELETE FROM task_taskcompletion WHERE task_id IN (SELECT id FROM task_task WHERE title LIKE 'test_%')")
|
|
testDB.Exec("DELETE FROM task_task WHERE title LIKE 'test_%'")
|
|
testDB.Exec("DELETE FROM task_taskstatus WHERE name LIKE 'test_%'")
|
|
testDB.Exec("DELETE FROM residence_residence WHERE name LIKE 'test_%'")
|
|
}
|
|
|
|
// Helper to create a time pointer
|
|
func timePtr(t time.Time) *time.Time {
|
|
return &t
|
|
}
|
|
|
|
// testUserID is a user ID that exists in the database for foreign key constraints
|
|
var testUserID uint = 1
|
|
|
|
// createTestResidence creates a test residence and returns its ID
|
|
func createTestResidence(t *testing.T) uint {
|
|
residence := &models.Residence{
|
|
Name: "test_residence_" + time.Now().Format("20060102150405"),
|
|
OwnerID: testUserID,
|
|
IsActive: true,
|
|
}
|
|
if err := testDB.Create(residence).Error; err != nil {
|
|
t.Fatalf("Failed to create test residence: %v", err)
|
|
}
|
|
return residence.ID
|
|
}
|
|
|
|
// createTestStatus creates a test status and returns it
|
|
func createTestStatus(t *testing.T, name string) *models.TaskStatus {
|
|
status := &models.TaskStatus{
|
|
Name: "test_" + name,
|
|
}
|
|
if err := testDB.Create(status).Error; err != nil {
|
|
t.Fatalf("Failed to create test status: %v", err)
|
|
}
|
|
return status
|
|
}
|
|
|
|
// createTestTask creates a task with the given properties
|
|
func createTestTask(t *testing.T, residenceID uint, task *models.Task) *models.Task {
|
|
task.ResidenceID = residenceID
|
|
task.Title = "test_" + task.Title
|
|
task.CreatedByID = testUserID // Required foreign key
|
|
if err := testDB.Create(task).Error; err != nil {
|
|
t.Fatalf("Failed to create test task: %v", err)
|
|
}
|
|
return task
|
|
}
|
|
|
|
// createTestCompletion creates a completion for a task
|
|
func createTestCompletion(t *testing.T, taskID uint) *models.TaskCompletion {
|
|
completion := &models.TaskCompletion{
|
|
TaskID: taskID,
|
|
CompletedByID: testUserID, // Required foreign key
|
|
CompletedAt: time.Now().UTC(),
|
|
}
|
|
if err := testDB.Create(completion).Error; err != nil {
|
|
t.Fatalf("Failed to create test completion: %v", err)
|
|
}
|
|
return completion
|
|
}
|
|
|
|
// TestScopeActiveMatchesPredicate verifies ScopeActive produces same results as IsActive
|
|
func TestScopeActiveMatchesPredicate(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
// Create tasks with different active states
|
|
tasks := []*models.Task{
|
|
{Title: "active_task", IsCancelled: false, IsArchived: false},
|
|
{Title: "cancelled_task", IsCancelled: true, IsArchived: false},
|
|
{Title: "archived_task", IsCancelled: false, IsArchived: true},
|
|
{Title: "both_task", IsCancelled: true, IsArchived: true},
|
|
}
|
|
|
|
for _, task := range tasks {
|
|
createTestTask(t, residenceID, task)
|
|
}
|
|
|
|
// Query using scope
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeActive).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query all tasks and filter with predicate
|
|
var allTasks []models.Task
|
|
testDB.Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsActive(&task) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Compare results
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("ScopeActive returned %d tasks, IsActive predicate returned %d tasks",
|
|
len(scopeResults), len(predicateResults))
|
|
}
|
|
|
|
// Should only have the active task
|
|
if len(scopeResults) != 1 {
|
|
t.Errorf("Expected 1 active task, got %d", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeCompletedMatchesPredicate verifies ScopeCompleted produces same results as IsCompleted
|
|
func TestScopeCompletedMatchesPredicate(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
now := time.Now().UTC()
|
|
nextWeek := now.AddDate(0, 0, 7)
|
|
|
|
// Create tasks with different completion states
|
|
// Completed: NextDueDate nil AND has completions
|
|
completedTask := createTestTask(t, residenceID, &models.Task{
|
|
Title: "completed_task",
|
|
NextDueDate: nil,
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
createTestCompletion(t, completedTask.ID)
|
|
|
|
// Not completed: has completions but NextDueDate set (recurring)
|
|
recurringTask := createTestTask(t, residenceID, &models.Task{
|
|
Title: "recurring_with_completion",
|
|
NextDueDate: timePtr(nextWeek),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
createTestCompletion(t, recurringTask.ID)
|
|
|
|
// Not completed: NextDueDate nil but no completions
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "no_completions",
|
|
NextDueDate: nil,
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not completed: has NextDueDate, no completions
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "pending_task",
|
|
NextDueDate: timePtr(nextWeek),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Query using scope
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeCompleted).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query all tasks with completions preloaded and filter with predicate
|
|
var allTasks []models.Task
|
|
testDB.Preload("Completions").Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsCompleted(&task) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Compare results
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("ScopeCompleted returned %d tasks, IsCompleted predicate returned %d tasks",
|
|
len(scopeResults), len(predicateResults))
|
|
}
|
|
|
|
// Should only have the completed task (nil NextDueDate + has completion)
|
|
if len(scopeResults) != 1 {
|
|
t.Errorf("Expected 1 completed task, got %d", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeOverdueMatchesPredicate verifies ScopeOverdue produces same results as IsOverdue
|
|
func TestScopeOverdueMatchesPredicate(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
now := time.Now().UTC()
|
|
yesterday := now.AddDate(0, 0, -1)
|
|
tomorrow := now.AddDate(0, 0, 1)
|
|
|
|
// Overdue: NextDueDate in past, active, not completed
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "overdue_task",
|
|
NextDueDate: timePtr(yesterday),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Overdue: DueDate in past (NextDueDate nil, no completions)
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "overdue_duedate",
|
|
NextDueDate: nil,
|
|
DueDate: timePtr(yesterday),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not overdue: future date
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "future_task",
|
|
NextDueDate: timePtr(tomorrow),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not overdue: cancelled
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "cancelled_overdue",
|
|
NextDueDate: timePtr(yesterday),
|
|
IsCancelled: true,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not overdue: completed (NextDueDate nil with completion)
|
|
completedTask := createTestTask(t, residenceID, &models.Task{
|
|
Title: "completed_past_due",
|
|
NextDueDate: nil,
|
|
DueDate: timePtr(yesterday),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
createTestCompletion(t, completedTask.ID)
|
|
|
|
// Not overdue: no due date
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "no_due_date",
|
|
NextDueDate: nil,
|
|
DueDate: nil,
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Query using scope
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeOverdue(now)).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query all tasks with completions preloaded and filter with predicate
|
|
var allTasks []models.Task
|
|
testDB.Preload("Completions").Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsOverdue(&task, now) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Compare results
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("ScopeOverdue returned %d tasks, IsOverdue predicate returned %d tasks",
|
|
len(scopeResults), len(predicateResults))
|
|
t.Logf("Scope results: %v", getTaskTitles(scopeResults))
|
|
t.Logf("Predicate results: %v", getTaskTitles(predicateResults))
|
|
}
|
|
|
|
// Should have 2 overdue tasks
|
|
if len(scopeResults) != 2 {
|
|
t.Errorf("Expected 2 overdue tasks, got %d", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeOverdueWithSameDayTask tests the DATE vs TIMESTAMP comparison edge case
|
|
// This is a regression test for the bug where tasks due "today" were not counted as overdue
|
|
func TestScopeOverdueWithSameDayTask(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
// Create a task due at midnight today (simulating a DATE column)
|
|
now := time.Now().UTC()
|
|
todayMidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
|
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "due_today_midnight",
|
|
NextDueDate: timePtr(todayMidnight),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Query using scope with current time (after midnight)
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeOverdue(now)).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query with predicate
|
|
var allTasks []models.Task
|
|
testDB.Preload("Completions").Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsOverdue(&task, now) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Both should agree: if it's past midnight, the task due at midnight is overdue
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("DATE vs TIMESTAMP mismatch! Scope returned %d, predicate returned %d",
|
|
len(scopeResults), len(predicateResults))
|
|
t.Logf("This indicates the PostgreSQL DATE/TIMESTAMP comparison bug may have returned")
|
|
}
|
|
|
|
// If current time is after midnight, task should be overdue
|
|
if now.After(todayMidnight) && len(scopeResults) != 1 {
|
|
t.Errorf("Task due at midnight should be overdue after midnight, got %d results", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeDueSoonMatchesPredicate verifies ScopeDueSoon produces same results as IsDueSoon
|
|
func TestScopeDueSoonMatchesPredicate(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
now := time.Now().UTC()
|
|
yesterday := now.AddDate(0, 0, -1)
|
|
in5Days := now.AddDate(0, 0, 5)
|
|
in60Days := now.AddDate(0, 0, 60)
|
|
daysThreshold := 30
|
|
|
|
// Due soon: within threshold
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "due_soon",
|
|
NextDueDate: timePtr(in5Days),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not due soon: beyond threshold
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "far_future",
|
|
NextDueDate: timePtr(in60Days),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not due soon: overdue (in past)
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "overdue",
|
|
NextDueDate: timePtr(yesterday),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not due soon: cancelled
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "cancelled",
|
|
NextDueDate: timePtr(in5Days),
|
|
IsCancelled: true,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Query using scope
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeDueSoon(now, daysThreshold)).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query all tasks and filter with predicate
|
|
var allTasks []models.Task
|
|
testDB.Preload("Completions").Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsDueSoon(&task, now, daysThreshold) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Compare results
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("ScopeDueSoon returned %d tasks, IsDueSoon predicate returned %d tasks",
|
|
len(scopeResults), len(predicateResults))
|
|
}
|
|
|
|
// Should have 1 due soon task
|
|
if len(scopeResults) != 1 {
|
|
t.Errorf("Expected 1 due soon task, got %d", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeUpcomingMatchesPredicate verifies ScopeUpcoming produces same results as IsUpcoming
|
|
func TestScopeUpcomingMatchesPredicate(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
now := time.Now().UTC()
|
|
in5Days := now.AddDate(0, 0, 5)
|
|
in60Days := now.AddDate(0, 0, 60)
|
|
daysThreshold := 30
|
|
|
|
// Upcoming: beyond threshold
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "far_future",
|
|
NextDueDate: timePtr(in60Days),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Upcoming: no due date
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "no_due_date",
|
|
NextDueDate: nil,
|
|
DueDate: nil,
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not upcoming: within due soon threshold
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "due_soon",
|
|
NextDueDate: timePtr(in5Days),
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Not upcoming: cancelled
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "cancelled",
|
|
NextDueDate: timePtr(in60Days),
|
|
IsCancelled: true,
|
|
IsArchived: false,
|
|
})
|
|
|
|
// Query using scope
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeUpcoming(now, daysThreshold)).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query all tasks and filter with predicate
|
|
var allTasks []models.Task
|
|
testDB.Preload("Completions").Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsUpcoming(&task, now, daysThreshold) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Compare results
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("ScopeUpcoming returned %d tasks, IsUpcoming predicate returned %d tasks",
|
|
len(scopeResults), len(predicateResults))
|
|
}
|
|
|
|
// Should have 2 upcoming tasks
|
|
if len(scopeResults) != 2 {
|
|
t.Errorf("Expected 2 upcoming tasks, got %d", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeInProgressMatchesPredicate verifies ScopeInProgress produces same results as IsInProgress
|
|
func TestScopeInProgressMatchesPredicate(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID := createTestResidence(t)
|
|
|
|
// For InProgress, we need to use the exact status name "In Progress" because
|
|
// the scope joins on task_taskstatus.name = 'In Progress'
|
|
// First, try to find existing "In Progress" status, or create one
|
|
var inProgressStatus models.TaskStatus
|
|
if err := testDB.Where("name = ?", "In Progress").First(&inProgressStatus).Error; err != nil {
|
|
// Create it if it doesn't exist
|
|
inProgressStatus = models.TaskStatus{Name: "In Progress"}
|
|
testDB.Create(&inProgressStatus)
|
|
}
|
|
|
|
var pendingStatus models.TaskStatus
|
|
if err := testDB.Where("name = ?", "Pending").First(&pendingStatus).Error; err != nil {
|
|
pendingStatus = models.TaskStatus{Name: "Pending"}
|
|
testDB.Create(&pendingStatus)
|
|
}
|
|
|
|
defer cleanupTestData()
|
|
|
|
// In progress task
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "in_progress",
|
|
StatusID: &inProgressStatus.ID,
|
|
})
|
|
|
|
// Not in progress: different status
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "pending",
|
|
StatusID: &pendingStatus.ID,
|
|
})
|
|
|
|
// Not in progress: no status
|
|
createTestTask(t, residenceID, &models.Task{
|
|
Title: "no_status",
|
|
StatusID: nil,
|
|
})
|
|
|
|
// Query using scope
|
|
var scopeResults []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeInProgress).
|
|
Find(&scopeResults).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
// Query all tasks with status preloaded and filter with predicate
|
|
var allTasks []models.Task
|
|
testDB.Preload("Status").Where("residence_id = ?", residenceID).Find(&allTasks)
|
|
|
|
var predicateResults []models.Task
|
|
for _, task := range allTasks {
|
|
if predicates.IsInProgress(&task) {
|
|
predicateResults = append(predicateResults, task)
|
|
}
|
|
}
|
|
|
|
// Compare results
|
|
if len(scopeResults) != len(predicateResults) {
|
|
t.Errorf("ScopeInProgress returned %d tasks, IsInProgress predicate returned %d tasks",
|
|
len(scopeResults), len(predicateResults))
|
|
}
|
|
|
|
// Should have 1 in progress task
|
|
if len(scopeResults) != 1 {
|
|
t.Errorf("Expected 1 in progress task, got %d", len(scopeResults))
|
|
}
|
|
}
|
|
|
|
// TestScopeForResidences verifies filtering by multiple residence IDs
|
|
func TestScopeForResidences(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Database not available")
|
|
}
|
|
|
|
residenceID1 := createTestResidence(t)
|
|
residenceID2 := createTestResidence(t)
|
|
residenceID3 := createTestResidence(t)
|
|
defer cleanupTestData()
|
|
|
|
// Create tasks in different residences
|
|
createTestTask(t, residenceID1, &models.Task{Title: "task_r1"})
|
|
createTestTask(t, residenceID2, &models.Task{Title: "task_r2"})
|
|
createTestTask(t, residenceID3, &models.Task{Title: "task_r3"})
|
|
|
|
// Query for residences 1 and 2 only
|
|
var results []models.Task
|
|
err := testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidences([]uint{residenceID1, residenceID2})).
|
|
Find(&results).Error
|
|
if err != nil {
|
|
t.Fatalf("Scope query failed: %v", err)
|
|
}
|
|
|
|
if len(results) != 2 {
|
|
t.Errorf("Expected 2 tasks from residences 1 and 2, got %d", len(results))
|
|
}
|
|
|
|
// Verify empty slice returns no results
|
|
var emptyResults []models.Task
|
|
testDB.Model(&models.Task{}).
|
|
Scopes(scopes.ScopeForResidences([]uint{})).
|
|
Find(&emptyResults)
|
|
|
|
if len(emptyResults) != 0 {
|
|
t.Errorf("Expected 0 tasks for empty residence list, got %d", len(emptyResults))
|
|
}
|
|
}
|
|
|
|
// Helper to get task titles for debugging
|
|
func getTaskTitles(tasks []models.Task) []string {
|
|
titles := make([]string, len(tasks))
|
|
for i, task := range tasks {
|
|
titles[i] = task.Title
|
|
}
|
|
return titles
|
|
}
|