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

@@ -840,3 +840,159 @@ func TestTaskHandler_JSONResponses(t *testing.T) {
assert.IsType(t, []interface{}{}, response["columns"])
})
}
// =============================================================================
// Part 3: Handler-Level Edge Cases (TDD)
// =============================================================================
func TestTaskHandler_CreateCompletion_RatingValidation(t *testing.T) {
handler, e, db := setupTaskHandler(t)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
authGroup := e.Group("/api/task-completions")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateCompletion)
tests := []struct {
name string
rating int
wantStatus int
}{
{"rating_0_rejected", 0, http.StatusBadRequest},
{"rating_negative1_rejected", -1, http.StatusBadRequest},
{"rating_1_accepted", 1, http.StatusCreated},
{"rating_5_accepted", 5, http.StatusCreated},
{"rating_6_rejected", 6, http.StatusBadRequest},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a fresh task for each accepted rating (otherwise it's completed)
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Rate Me "+tt.name)
completedAt := time.Now().UTC()
rating := tt.rating
req := requests.CreateTaskCompletionRequest{
TaskID: task.ID,
CompletedAt: &completedAt,
Rating: &rating,
}
w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")
testutil.AssertStatusCode(t, w, tt.wantStatus)
})
}
}
func TestTaskHandler_CreateTask_TitleBoundary(t *testing.T) {
handler, e, db := setupTaskHandler(t)
testutil.SeedLookupData(t, db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
authGroup := e.Group("/api/tasks")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateTask)
t.Run("title_exactly_200_chars_accepted", func(t *testing.T) {
title200 := ""
for i := 0; i < 200; i++ {
title200 += "A"
}
req := requests.CreateTaskRequest{
ResidenceID: residence.ID,
Title: title200,
}
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
})
t.Run("title_201_chars_rejected", func(t *testing.T) {
title201 := ""
for i := 0; i < 201; i++ {
title201 += "A"
}
req := requests.CreateTaskRequest{
ResidenceID: residence.ID,
Title: title201,
}
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
}
func TestTaskHandler_CreateTask_DescriptionBoundary(t *testing.T) {
handler, e, db := setupTaskHandler(t)
testutil.SeedLookupData(t, db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
authGroup := e.Group("/api/tasks")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateTask)
t.Run("description_exactly_10000_chars_accepted", func(t *testing.T) {
desc10000 := ""
for i := 0; i < 10000; i++ {
desc10000 += "B"
}
req := requests.CreateTaskRequest{
ResidenceID: residence.ID,
Title: "Long Desc Task",
Description: desc10000,
}
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
})
}
func TestTaskHandler_CreateTask_CustomIntervalDaysValidation(t *testing.T) {
handler, e, db := setupTaskHandler(t)
testutil.SeedLookupData(t, db)
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
authGroup := e.Group("/api/tasks")
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.POST("/", handler.CreateTask)
t.Run("custom_interval_days_0_rejected", func(t *testing.T) {
// Validation tag: min=1, so 0 should be rejected
interval := 0
req := map[string]interface{}{
"residence_id": residence.ID,
"title": "Custom Interval Zero",
"custom_interval_days": interval,
}
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("custom_interval_days_negative1_rejected", func(t *testing.T) {
interval := -1
req := map[string]interface{}{
"residence_id": residence.ID,
"title": "Custom Interval Negative",
"custom_interval_days": interval,
}
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
})
t.Run("custom_interval_days_1_accepted", func(t *testing.T) {
interval := 1
req := requests.CreateTaskRequest{
ResidenceID: residence.ID,
Title: "Custom Interval One",
CustomIntervalDays: &interval,
}
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
testutil.AssertStatusCode(t, w, http.StatusCreated)
})
}