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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user