- Remove task_statuses lookup table and StatusID foreign key - Add InProgress boolean field to Task model - Add database migration (005_replace_status_with_in_progress) - Update all handlers, services, and repositories - Update admin frontend to display in_progress as checkbox/boolean - Remove Task Statuses tab from admin lookups page - Update tests to use InProgress instead of StatusID - Task categorization now uses InProgress for kanban column assignment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
236 lines
7.6 KiB
Go
236 lines
7.6 KiB
Go
package categorization_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/treytartt/casera-api/internal/models"
|
|
"github.com/treytartt/casera-api/internal/task/categorization"
|
|
)
|
|
|
|
// Helper to create a time pointer
|
|
func timePtr(t time.Time) *time.Time {
|
|
return &t
|
|
}
|
|
|
|
func TestCategorizeTask_PriorityOrder(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
yesterday := now.AddDate(0, 0, -1)
|
|
in5Days := now.AddDate(0, 0, 5)
|
|
in60Days := now.AddDate(0, 0, 60)
|
|
daysThreshold := 30
|
|
|
|
tests := []struct {
|
|
name string
|
|
task *models.Task
|
|
expected categorization.KanbanColumn
|
|
}{
|
|
// Priority 1: Cancelled
|
|
{
|
|
name: "cancelled takes priority over everything",
|
|
task: &models.Task{
|
|
IsCancelled: true,
|
|
NextDueDate: timePtr(yesterday), // Would be overdue
|
|
InProgress: true, // Would be in progress
|
|
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}, // Would be completed if NextDueDate was nil
|
|
},
|
|
expected: categorization.ColumnCancelled,
|
|
},
|
|
|
|
// Priority 2: Completed
|
|
{
|
|
name: "completed: NextDueDate nil with completions",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: nil,
|
|
DueDate: timePtr(yesterday), // Would be overdue if not completed
|
|
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
|
},
|
|
expected: categorization.ColumnCompleted,
|
|
},
|
|
{
|
|
name: "not completed when NextDueDate set (recurring task with completions)",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: timePtr(in5Days),
|
|
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
|
|
},
|
|
expected: categorization.ColumnDueSoon, // Falls through to due soon
|
|
},
|
|
|
|
// Priority 3: In Progress
|
|
{
|
|
name: "in progress takes priority over overdue",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: timePtr(yesterday), // Would be overdue
|
|
InProgress: true,
|
|
Completions: []models.TaskCompletion{},
|
|
},
|
|
expected: categorization.ColumnInProgress,
|
|
},
|
|
|
|
// Priority 4: Overdue
|
|
{
|
|
name: "overdue: effective date in past",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: timePtr(yesterday),
|
|
Completions: []models.TaskCompletion{},
|
|
},
|
|
expected: categorization.ColumnOverdue,
|
|
},
|
|
{
|
|
name: "overdue: uses DueDate when NextDueDate nil (no completions)",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: nil,
|
|
DueDate: timePtr(yesterday),
|
|
Completions: []models.TaskCompletion{},
|
|
},
|
|
expected: categorization.ColumnOverdue,
|
|
},
|
|
|
|
// Priority 5: Due Soon
|
|
{
|
|
name: "due soon: within threshold",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: timePtr(in5Days),
|
|
Completions: []models.TaskCompletion{},
|
|
},
|
|
expected: categorization.ColumnDueSoon,
|
|
},
|
|
|
|
// Priority 6: Upcoming (default)
|
|
{
|
|
name: "upcoming: beyond threshold",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: timePtr(in60Days),
|
|
Completions: []models.TaskCompletion{},
|
|
},
|
|
expected: categorization.ColumnUpcoming,
|
|
},
|
|
{
|
|
name: "upcoming: no due date",
|
|
task: &models.Task{
|
|
IsCancelled: false,
|
|
IsArchived: false,
|
|
NextDueDate: nil,
|
|
DueDate: nil,
|
|
Completions: []models.TaskCompletion{},
|
|
},
|
|
expected: categorization.ColumnUpcoming,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := categorization.CategorizeTask(tt.task, daysThreshold)
|
|
if result != tt.expected {
|
|
t.Errorf("CategorizeTask() = %v, expected %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCategorizeTasksIntoColumns(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
yesterday := now.AddDate(0, 0, -1)
|
|
in5Days := now.AddDate(0, 0, 5)
|
|
in60Days := now.AddDate(0, 0, 60)
|
|
daysThreshold := 30
|
|
|
|
tasks := []models.Task{
|
|
{BaseModel: models.BaseModel{ID: 1}, IsCancelled: true}, // Cancelled
|
|
{BaseModel: models.BaseModel{ID: 2}, NextDueDate: nil, Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}}, // Completed
|
|
{BaseModel: models.BaseModel{ID: 3}, InProgress: true}, // In Progress
|
|
{BaseModel: models.BaseModel{ID: 4}, NextDueDate: timePtr(yesterday)}, // Overdue
|
|
{BaseModel: models.BaseModel{ID: 5}, NextDueDate: timePtr(in5Days)}, // Due Soon
|
|
{BaseModel: models.BaseModel{ID: 6}, NextDueDate: timePtr(in60Days)}, // Upcoming
|
|
{BaseModel: models.BaseModel{ID: 7}}, // Upcoming (no due date)
|
|
}
|
|
|
|
result := categorization.CategorizeTasksIntoColumns(tasks, daysThreshold)
|
|
|
|
// Check each column has the expected tasks
|
|
if len(result[categorization.ColumnCancelled]) != 1 || result[categorization.ColumnCancelled][0].ID != 1 {
|
|
t.Errorf("Expected task 1 in Cancelled column")
|
|
}
|
|
if len(result[categorization.ColumnCompleted]) != 1 || result[categorization.ColumnCompleted][0].ID != 2 {
|
|
t.Errorf("Expected task 2 in Completed column")
|
|
}
|
|
if len(result[categorization.ColumnInProgress]) != 1 || result[categorization.ColumnInProgress][0].ID != 3 {
|
|
t.Errorf("Expected task 3 in InProgress column")
|
|
}
|
|
if len(result[categorization.ColumnOverdue]) != 1 || result[categorization.ColumnOverdue][0].ID != 4 {
|
|
t.Errorf("Expected task 4 in Overdue column")
|
|
}
|
|
if len(result[categorization.ColumnDueSoon]) != 1 || result[categorization.ColumnDueSoon][0].ID != 5 {
|
|
t.Errorf("Expected task 5 in DueSoon column")
|
|
}
|
|
if len(result[categorization.ColumnUpcoming]) != 2 {
|
|
t.Errorf("Expected 2 tasks in Upcoming column, got %d", len(result[categorization.ColumnUpcoming]))
|
|
}
|
|
}
|
|
|
|
func TestDetermineKanbanColumn_ReturnsString(t *testing.T) {
|
|
task := &models.Task{IsCancelled: true}
|
|
result := categorization.DetermineKanbanColumn(task, 30)
|
|
|
|
if result != "cancelled_tasks" {
|
|
t.Errorf("DetermineKanbanColumn() = %v, expected %v", result, "cancelled_tasks")
|
|
}
|
|
}
|
|
|
|
func TestKanbanColumnConstants(t *testing.T) {
|
|
// Verify column string values match expected API values
|
|
tests := []struct {
|
|
column categorization.KanbanColumn
|
|
expected string
|
|
}{
|
|
{categorization.ColumnOverdue, "overdue_tasks"},
|
|
{categorization.ColumnDueSoon, "due_soon_tasks"},
|
|
{categorization.ColumnUpcoming, "upcoming_tasks"},
|
|
{categorization.ColumnInProgress, "in_progress_tasks"},
|
|
{categorization.ColumnCompleted, "completed_tasks"},
|
|
{categorization.ColumnCancelled, "cancelled_tasks"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if tt.column.String() != tt.expected {
|
|
t.Errorf("Column %v.String() = %v, expected %v", tt.column, tt.column.String(), tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewContext_DefaultThreshold(t *testing.T) {
|
|
task := &models.Task{}
|
|
|
|
// Zero threshold should default to 30
|
|
ctx := categorization.NewContext(task, 0)
|
|
if ctx.DaysThreshold != 30 {
|
|
t.Errorf("NewContext with 0 threshold should default to 30, got %d", ctx.DaysThreshold)
|
|
}
|
|
|
|
// Negative threshold should default to 30
|
|
ctx = categorization.NewContext(task, -5)
|
|
if ctx.DaysThreshold != 30 {
|
|
t.Errorf("NewContext with negative threshold should default to 30, got %d", ctx.DaysThreshold)
|
|
}
|
|
|
|
// Positive threshold should be used
|
|
ctx = categorization.NewContext(task, 45)
|
|
if ctx.DaysThreshold != 45 {
|
|
t.Errorf("NewContext with 45 threshold should be 45, got %d", ctx.DaysThreshold)
|
|
}
|
|
}
|