Comprehensive TDD test suite for task logic — ~80 new tests

Predicates (20 cases): IsRecurring, IsOneTime, IsDueSoon,
HasCompletions, GetCompletionCount, IsUpcoming edge cases

Task creation (10): NextDueDate initialization, all frequency types,
past dates, all optional fields, access validation

One-time completion (8): NextDueDate→nil, InProgress reset,
notes/cost/rating, double completion, backdated completed_at

Recurring completion (16): Daily/Weekly/BiWeekly/Monthly/Quarterly/
Yearly/Custom frequencies, late/early completion timing, multiple
sequential completions, no-original-DueDate, CompletedFromColumn capture

QuickComplete (5): one-time, recurring, widget notes, 404, 403

State transitions (10): Cancel→Complete, Archive→Complete, InProgress
cycles, recurring full lifecycle, Archive→Unarchive column restore

Kanban column priority (7): verify chain priority order for all columns

Optimistic locking (7): correct/stale version, conflict on complete/
cancel/archive/mark-in-progress, rollback verification

Deletion (5): single/multi/middle completion deletion, NextDueDate
recalculation, InProgress restore behavior documented

Edge cases (9): boundary dates, late/early recurring, nil/zero frequency
days, custom intervals, version conflicts

Handler validation (4): rating bounds, title/description length,
custom interval validation

All 679 tests pass.
This commit is contained in:
Trey T
2026-03-26 17:36:50 -05:00
parent 7f0300cc95
commit 4c9a818bd9
4 changed files with 3546 additions and 3 deletions

View File

@@ -506,7 +506,9 @@ func TestHasCompletions(t *testing.T) {
}
func TestIsRecurring(t *testing.T) {
days := 7
days7 := 7
days0 := 0
days14 := 14
tests := []struct {
name string
@@ -514,8 +516,8 @@ func TestIsRecurring(t *testing.T) {
expected bool
}{
{
name: "recurring: frequency with days",
task: &models.Task{Frequency: &models.TaskFrequency{Days: &days}},
name: "recurring: frequency with days > 0",
task: &models.Task{Frequency: &models.TaskFrequency{Days: &days7}},
expected: true,
},
{
@@ -528,6 +530,20 @@ func TestIsRecurring(t *testing.T) {
task: &models.Task{Frequency: nil},
expected: false,
},
{
name: "not recurring: frequency with days = 0 (Once)",
task: &models.Task{Frequency: &models.TaskFrequency{Days: &days0}},
expected: false,
},
{
name: "recurring: FrequencyID set with CustomIntervalDays",
task: &models.Task{
FrequencyID: uintPtr(5),
Frequency: &models.TaskFrequency{Days: &days14},
CustomIntervalDays: intPtr(14),
},
expected: true,
},
}
for _, tt := range tests {
@@ -539,3 +555,224 @@ func TestIsRecurring(t *testing.T) {
})
}
}
func TestIsOneTime(t *testing.T) {
days7 := 7
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "one-time: no frequency",
task: &models.Task{Frequency: nil},
expected: true,
},
{
name: "one-time: Once frequency (days nil)",
task: &models.Task{Frequency: &models.TaskFrequency{Name: "Once", Days: nil}},
expected: true,
},
{
name: "not one-time: recurring frequency",
task: &models.Task{Frequency: &models.TaskFrequency{Name: "Weekly", Days: &days7}},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.IsOneTime(tt.task)
if result != tt.expected {
t.Errorf("IsOneTime() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsDueSoon_AdditionalCases(t *testing.T) {
now := time.Now().UTC()
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
tests := []struct {
name string
task *models.Task
now time.Time
daysThreshold int
expected bool
}{
{
name: "not due soon: no due date",
task: &models.Task{
NextDueDate: nil,
DueDate: nil,
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
{
name: "due soon: task due today (start of day)",
task: &models.Task{
NextDueDate: timePtr(startOfToday),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: true,
},
}
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 TestHasCompletions_CompletionCount(t *testing.T) {
tests := []struct {
name string
task *models.Task
expected bool
}{
{
name: "has completions via CompletionCount",
task: &models.Task{Completions: nil, CompletionCount: 3},
expected: true,
},
{
name: "no completions: CompletionCount zero, no preloaded completions",
task: &models.Task{Completions: nil, CompletionCount: 0},
expected: false,
},
{
name: "has completions via preloaded slice (ignores CompletionCount)",
task: &models.Task{
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}},
CompletionCount: 0,
},
expected: true,
},
}
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 TestGetCompletionCount(t *testing.T) {
tests := []struct {
name string
task *models.Task
expected int
}{
{
name: "zero completions: empty slice",
task: &models.Task{Completions: []models.TaskCompletion{}, CompletionCount: 0},
expected: 0,
},
{
name: "three completions via preloaded slice",
task: &models.Task{
Completions: []models.TaskCompletion{
{BaseModel: models.BaseModel{ID: 1}},
{BaseModel: models.BaseModel{ID: 2}},
{BaseModel: models.BaseModel{ID: 3}},
},
},
expected: 3,
},
{
name: "count via CompletionCount when slice is empty",
task: &models.Task{Completions: nil, CompletionCount: 5},
expected: 5,
},
{
name: "nil completions and zero CompletionCount",
task: &models.Task{Completions: nil, CompletionCount: 0},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := predicates.GetCompletionCount(tt.task)
if result != tt.expected {
t.Errorf("GetCompletionCount() = %v, expected %v", result, tt.expected)
}
})
}
}
func TestIsUpcoming_AdditionalCases(t *testing.T) {
now := time.Now().UTC()
yesterday := now.AddDate(0, 0, -1)
tests := []struct {
name string
task *models.Task
now time.Time
daysThreshold int
expected bool
}{
{
name: "not upcoming: overdue task",
task: &models.Task{
NextDueDate: timePtr(yesterday),
IsCancelled: false,
IsArchived: false,
Completions: []models.TaskCompletion{},
},
now: now,
daysThreshold: 30,
expected: false,
},
{
name: "not upcoming: completed task",
task: &models.Task{
NextDueDate: nil,
DueDate: nil,
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.IsUpcoming(tt.task, tt.now, tt.daysThreshold)
if result != tt.expected {
t.Errorf("IsUpcoming() = %v, expected %v", result, tt.expected)
}
})
}
}
// Helper to create a uint pointer
func uintPtr(v uint) *uint {
return &v
}
// Helper to create an int pointer
func intPtr(v int) *int {
return &v
}