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