Files
honeyDueAPI/internal/task/predicates/predicates_test.go
Trey t c5b0225422 Replace status_id with in_progress boolean field
- 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>
2025-12-08 20:48:16 -06:00

515 lines
11 KiB
Go

package predicates_test
import (
"testing"
"time"
"github.com/treytartt/casera-api/internal/models"
"github.com/treytartt/casera-api/internal/task/predicates"
)
// Helper to create a time pointer
func timePtr(t time.Time) *time.Time {
return &t
}
func TestIsCompleted(t *testing.T) {
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "completed: NextDueDate nil with completions",
task: &models.Task{
NextDueDate: nil,
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
},
expected: true,
},
{
name: "not completed: NextDueDate set with completions",
task: &models.Task{
NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)),
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
},
expected: false,
},
{
name: "not completed: NextDueDate nil without completions",
task: &models.Task{
NextDueDate: nil,
Completions: []models.TaskCompletion{},
},
expected: false,
},
{
name: "not completed: NextDueDate set without completions",
task: &models.Task{
NextDueDate: timePtr(time.Now().AddDate(0, 0, 7)),
Completions: []models.TaskCompletion{},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsCompleted(tt.task)
if result != tt.expected {
t.Errorf("IsCompleted() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsActive(t *testing.T) {
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "active: not cancelled, not archived",
task: &models.Task{IsCancelled: false, IsArchived: false},
expected: true,
},
{
name: "not active: cancelled",
task: &models.Task{IsCancelled: true, IsArchived: false},
expected: false,
},
{
name: "not active: archived",
task: &models.Task{IsCancelled: false, IsArchived: true},
expected: false,
},
{
name: "not active: both cancelled and archived",
task: &models.Task{IsCancelled: true, IsArchived: true},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsActive(tt.task)
if result != tt.expected {
t.Errorf("IsActive() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsInProgress(t *testing.T) {
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "in progress: InProgress is true",
task: &models.Task{InProgress: true},
expected: true,
},
{
name: "not in progress: InProgress is false",
task: &models.Task{InProgress: false},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsInProgress(tt.task)
if result != tt.expected {
t.Errorf("IsInProgress() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestEffectiveDate(t *testing.T) {
now := time.Now()
nextWeek := now.AddDate(0, 0, 7)
nextMonth := now.AddDate(0, 1, 0)
tests := []struct {
name string
task *models.Task
expected *time.Time
}{
{
name: "prefers NextDueDate when both set",
task: &models.Task{
NextDueDate: timePtr(nextWeek),
DueDate: timePtr(nextMonth),
},
expected: timePtr(nextWeek),
},
{
name: "falls back to DueDate when NextDueDate nil",
task: &models.Task{
NextDueDate: nil,
DueDate: timePtr(nextMonth),
},
expected: timePtr(nextMonth),
},
{
name: "returns nil when both nil",
task: &models.Task{
NextDueDate: nil,
DueDate: nil,
},
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.EffectiveDate(tt.task)
if tt.expected == nil {
if result != nil {
t.Errorf("EffectiveDate() = %v, expected nil", result)
}
} else {
if result == nil {
t.Errorf("EffectiveDate() = nil, expected %v", tt.expected)
} else if !result.Equal(*tt.expected) {
t.Errorf("EffectiveDate() = %v, expected %v", result, tt.expected)
}
}
})
}
}
func TestIsOverdue(t *testing.T) {
now := time.Now().UTC()
yesterday := now.AddDate(0, 0, -1)
tomorrow := now.AddDate(0, 0, 1)
tests := []struct {
name string
task *models.Task
now time.Time
expected bool
}{
{
name: "overdue: effective date in past",
task: &models.Task{
NextDueDate: timePtr(yesterday),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
expected: true,
},
{
name: "not overdue: effective date in future",
task: &models.Task{
NextDueDate: timePtr(tomorrow),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
expected: false,
},
{
name: "not overdue: cancelled task",
task: &models.Task{
NextDueDate: timePtr(yesterday),
IsCancelled: true,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
expected: false,
},
{
name: "not overdue: archived task",
task: &models.Task{
NextDueDate: timePtr(yesterday),
IsCancelled: false,
IsArchived: true,
Completions: []models.TaskCompletion{},
},
now: now,
expected: false,
},
{
name: "not overdue: completed task (NextDueDate nil with completions)",
task: &models.Task{
NextDueDate: nil,
DueDate: timePtr(yesterday),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
},
now: now,
expected: false,
},
{
name: "not overdue: no due date",
task: &models.Task{
NextDueDate: nil,
DueDate: nil,
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
expected: false,
},
{
name: "overdue: uses DueDate when NextDueDate nil (no completions)",
task: &models.Task{
NextDueDate: nil,
DueDate: timePtr(yesterday),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsOverdue(tt.task, tt.now)
if result != tt.expected {
t.Errorf("IsOverdue() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsDueSoon(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)
tests := []struct {
name string
task *models.Task
now time.Time
daysThreshold int
expected bool
}{
{
name: "due soon: within threshold",
task: &models.Task{
NextDueDate: timePtr(in5Days),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: true,
},
{
name: "not due soon: beyond threshold",
task: &models.Task{
NextDueDate: timePtr(in60Days),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
{
name: "not due soon: overdue (in past)",
task: &models.Task{
NextDueDate: timePtr(yesterday),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
{
name: "not due soon: cancelled",
task: &models.Task{
NextDueDate: timePtr(in5Days),
IsCancelled: true,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
{
name: "not due soon: completed",
task: &models.Task{
NextDueDate: nil,
DueDate: timePtr(in5Days),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
},
now: now,
daysThreshold: 30,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsDueSoon(tt.task, tt.now, tt.daysThreshold)
if result != tt.expected {
t.Errorf("IsDueSoon() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsUpcoming(t *testing.T) {
now := time.Now().UTC()
in5Days := now.AddDate(0, 0, 5)
in60Days := now.AddDate(0, 0, 60)
tests := []struct {
name string
task *models.Task
now time.Time
daysThreshold int
expected bool
}{
{
name: "upcoming: beyond threshold",
task: &models.Task{
NextDueDate: timePtr(in60Days),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: true,
},
{
name: "upcoming: no due date",
task: &models.Task{
NextDueDate: nil,
DueDate: nil,
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: true,
},
{
name: "not upcoming: within due soon threshold",
task: &models.Task{
NextDueDate: timePtr(in5Days),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
{
name: "not upcoming: cancelled",
task: &models.Task{
NextDueDate: timePtr(in60Days),
IsCancelled: true,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsUpcoming(tt.task, tt.now, tt.daysThreshold)
if result != tt.expected {
t.Errorf("IsUpcoming() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestHasCompletions(t *testing.T) {
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "has completions",
task: &models.Task{Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}},
expected: true,
},
{
name: "no completions",
task: &models.Task{Completions: []models.TaskCompletion{}},
expected: false,
},
{
name: "nil completions",
task: &models.Task{Completions: nil},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.HasCompletions(tt.task)
if result != tt.expected {
t.Errorf("HasCompletions() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsRecurring(t *testing.T) {
days := 7
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "recurring: frequency with days",
task: &models.Task{Frequency: &models.TaskFrequency{Days: &days}},
expected: true,
},
{
name: "not recurring: frequency without days (one-time)",
task: &models.Task{Frequency: &models.TaskFrequency{Days: nil}},
expected: false,
},
{
name: "not recurring: no frequency",
task: &models.Task{Frequency: nil},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsRecurring(tt.task)
if result != tt.expected {
t.Errorf("IsRecurring() = %v, expected %v", result, tt.expected)
}
})
}
}