From 5723d6635f9462e60960b0f9548273d95a86d8f3 Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 18 Mar 2026 10:15:54 +0000 Subject: test: add coverage for planning tab, meals, Google Tasks, bug handlers, completed task parsing - HandleTabPlanning: happy-path test verifying tasks/cards land in correct sections (scheduled/unscheduled/upcoming); boundary test confirming a task due exactly at midnight of tomorrow lands in Upcoming, not Scheduled - HandleTabMeals: grouping test verifying two meals sharing date+mealType produce one CombinedMeal with both recipe names merged - Google Tasks GetTasksByDateRange: four boundary tests (start inclusive, end exclusive, no-due-date always included, out-of-range excluded) using redirectingTransport mock server pattern - HandleGetBugs: data assertions verifying bug list and empty-list cases - HandleReportBug: success test verifying bug is saved and bugs template is re-rendered - GetCompletedTasks: timestamp parsing test ensuring CompletedAt is not zero when inserted with a known "2006-01-02 15:04:05" string Co-Authored-By: Claude Sonnet 4.6 --- internal/handlers/handlers_test.go | 383 +++++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) (limited to 'internal/handlers') diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 9a2287f..e30c6d5 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -2955,3 +2955,386 @@ func TestHandleSyncSources_AddsLogEntry(t *testing.T) { t.Errorf("Expected event_type 'sync', got %q", entries[0].EventType) } } + +// ============================================================================= +// HandleTabPlanning data tests +// ============================================================================= + +// TestHandleTabPlanning_HappyPath verifies that tasks, events, and cards are +// placed into the correct planning sections (scheduled/unscheduled/upcoming). +func TestHandleTabPlanning_HappyPath(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + today := config.Today() + + // Task at 10am today (has time) → goes to Scheduled + taskWithTime := today.Add(10 * time.Hour) + // Task at midnight today (no time component) → goes to Unscheduled + taskNoTime := today + // Task 2 days from now → goes to Upcoming (today+4 is the upper bound) + taskUpcoming := today.AddDate(0, 0, 2) + + tasks := []models.Task{ + {ID: "t-sched", Content: "Scheduled Task", DueDate: &taskWithTime, Labels: []string{}, CreatedAt: time.Now()}, + {ID: "t-unsched", Content: "Unscheduled Task", DueDate: &taskNoTime, Labels: []string{}, CreatedAt: time.Now()}, + {ID: "t-upcoming", Content: "Upcoming Task", DueDate: &taskUpcoming, Labels: []string{}, CreatedAt: time.Now()}, + } + if err := db.SaveTasks(tasks); err != nil { + t.Fatalf("Failed to save tasks: %v", err) + } + + // Card at 2pm today → goes to Scheduled + cardWithTime := today.Add(14 * time.Hour) + boards := []models.Board{{ + ID: "board1", + Name: "Board 1", + Cards: []models.Card{ + {ID: "c-sched", Name: "Scheduled Card", DueDate: &cardWithTime, URL: "http://trello.com/c1"}, + }, + }} + if err := db.SaveBoards(boards); err != nil { + t.Fatalf("Failed to save boards: %v", err) + } + + renderer := NewMockRenderer() + h := &Handler{ + store: db, + renderer: renderer, + config: &config.Config{CacheTTLMinutes: 5}, + } + + req := httptest.NewRequest("GET", "/tabs/planning", nil) + w := httptest.NewRecorder() + h.HandleTabPlanning(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Code) + } + + var planningCall *RenderCall + for i, call := range renderer.Calls { + if call.Name == "planning-tab" { + c := renderer.Calls[i] + planningCall = &c + break + } + } + if planningCall == nil { + t.Fatal("Expected planning-tab to be rendered") + } + + data, ok := planningCall.Data.(struct { + Scheduled []ScheduledItem + Unscheduled []models.Atom + Upcoming []ScheduledItem + Boards []models.Board + Today string + }) + if !ok { + t.Fatalf("Expected planning data struct, got %T", planningCall.Data) + } + + // t-sched (10am) and c-sched (2pm) should be in Scheduled + var foundTaskSched, foundCardSched bool + for _, s := range data.Scheduled { + if s.ID == "t-sched" { + foundTaskSched = true + } + if s.ID == "c-sched" { + foundCardSched = true + } + } + if !foundTaskSched { + t.Error("Expected t-sched (task with time today) in Scheduled") + } + if !foundCardSched { + t.Error("Expected c-sched (card with time today) in Scheduled") + } + + // t-unsched (midnight, no time) should be in Unscheduled + var foundUnsched bool + for _, u := range data.Unscheduled { + if u.ID == "t-unsched" { + foundUnsched = true + } + } + if !foundUnsched { + t.Error("Expected t-unsched (midnight task) in Unscheduled") + } + + // t-upcoming (2 days out) should be in Upcoming + var foundUpcoming bool + for _, u := range data.Upcoming { + if u.ID == "t-upcoming" { + foundUpcoming = true + } + } + if !foundUpcoming { + t.Error("Expected t-upcoming (task in 2 days) in Upcoming") + } +} + +// TestHandleTabPlanning_TomorrowBoundary verifies that a task due exactly at +// midnight of "tomorrow" is NOT before tomorrow, so it lands in Upcoming. +func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + tomorrow := config.Today().AddDate(0, 0, 1) // midnight tomorrow + tasks := []models.Task{ + {ID: "t-boundary", Content: "Midnight Tomorrow", DueDate: &tomorrow, Labels: []string{}, CreatedAt: time.Now()}, + } + if err := db.SaveTasks(tasks); err != nil { + t.Fatalf("Failed to save tasks: %v", err) + } + + renderer := NewMockRenderer() + h := &Handler{ + store: db, + renderer: renderer, + config: &config.Config{CacheTTLMinutes: 5}, + } + + req := httptest.NewRequest("GET", "/tabs/planning", nil) + w := httptest.NewRecorder() + h.HandleTabPlanning(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected 200, got %d", w.Code) + } + + var planningCall *RenderCall + for i, call := range renderer.Calls { + if call.Name == "planning-tab" { + c := renderer.Calls[i] + planningCall = &c + break + } + } + if planningCall == nil { + t.Fatal("Expected planning-tab to be rendered") + } + + data, ok := planningCall.Data.(struct { + Scheduled []ScheduledItem + Unscheduled []models.Atom + Upcoming []ScheduledItem + Boards []models.Board + Today string + }) + if !ok { + t.Fatalf("Expected planning data struct, got %T", planningCall.Data) + } + + // midnight-tomorrow: !dueDate.Before(tomorrow) → not in scheduled/unscheduled + for _, s := range data.Scheduled { + if s.ID == "t-boundary" { + t.Error("Midnight-tomorrow task must not appear in Scheduled") + } + } + for _, u := range data.Unscheduled { + if u.ID == "t-boundary" { + t.Error("Midnight-tomorrow task must not appear in Unscheduled") + } + } + // dueDate.Before(in3Days=today+4) → lands in Upcoming + var foundUpcoming bool + for _, u := range data.Upcoming { + if u.ID == "t-boundary" { + foundUpcoming = true + } + } + if !foundUpcoming { + t.Error("Expected midnight-tomorrow task in Upcoming") + } +} + +// ============================================================================= +// HandleTabMeals grouping test +// ============================================================================= + +// TestHandleTabMeals_GroupingMergesRecipes verifies that multiple meals sharing +// the same date+mealType are combined into a single CombinedMeal entry whose +// RecipeNames contains all merged recipe names. +func TestHandleTabMeals_GroupingMergesRecipes(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + today := config.Today() + meals := []models.Meal{ + {ID: "m1", RecipeName: "Pasta", Date: today, MealType: "dinner", RecipeURL: "http://example.com/pasta"}, + {ID: "m2", RecipeName: "Salad", Date: today, MealType: "dinner", RecipeURL: "http://example.com/salad"}, + {ID: "m3", RecipeName: "Oatmeal", Date: today, MealType: "breakfast", RecipeURL: "http://example.com/oatmeal"}, + } + if err := db.SaveMeals(meals); err != nil { + t.Fatalf("Failed to save meals: %v", err) + } + + renderer := NewMockRenderer() + h := &Handler{ + store: db, + renderer: renderer, + config: &config.Config{CacheTTLMinutes: 5}, + } + + req := httptest.NewRequest("GET", "/tabs/meals", nil) + w := httptest.NewRecorder() + h.HandleTabMeals(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Code) + } + + var mealsCall *RenderCall + for i, call := range renderer.Calls { + if call.Name == "meals-tab" { + c := renderer.Calls[i] + mealsCall = &c + break + } + } + if mealsCall == nil { + t.Fatal("Expected meals-tab to be rendered") + } + + data, ok := mealsCall.Data.(struct{ Meals []CombinedMeal }) + if !ok { + t.Fatalf("Expected meals data struct, got %T", mealsCall.Data) + } + + // m1 + m2 share date+dinner → 1 CombinedMeal; m3 is breakfast → 1 CombinedMeal + if len(data.Meals) != 2 { + t.Errorf("Expected 2 combined meals, got %d", len(data.Meals)) + } + + var dinner *CombinedMeal + for i := range data.Meals { + if data.Meals[i].MealType == "dinner" { + dinner = &data.Meals[i] + break + } + } + if dinner == nil { + t.Fatal("Expected a dinner CombinedMeal") + } + if len(dinner.RecipeNames) != 2 { + t.Errorf("Expected dinner to have 2 merged recipes, got %d: %v", len(dinner.RecipeNames), dinner.RecipeNames) + } +} + +// ============================================================================= +// HandleGetBugs and HandleReportBug data tests +// ============================================================================= + +// TestHandleGetBugs_ReturnsBugList verifies that the bugs template receives a +// non-empty bug list when bugs exist in the store. +func TestHandleGetBugs_ReturnsBugList(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + _ = h.store.SaveBug("login button not working") + _ = h.store.SaveBug("dashboard shows wrong date") + + req := httptest.NewRequest("GET", "/bugs", nil) + w := httptest.NewRecorder() + h.HandleGetBugs(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Code) + } + + mr := h.renderer.(*MockRenderer) + var found bool + for _, call := range mr.Calls { + if call.Name == "bugs" { + found = true + bugs, ok := call.Data.([]store.Bug) + if !ok { + t.Fatalf("Expected []store.Bug data, got %T", call.Data) + } + if len(bugs) != 2 { + t.Errorf("Expected 2 bugs in template data, got %d", len(bugs)) + } + break + } + } + if !found { + t.Error("Expected renderer called with 'bugs' template") + } +} + +// TestHandleGetBugs_EmptyList verifies that the bugs template receives an +// empty (non-nil) slice when no bugs exist. +func TestHandleGetBugs_EmptyList(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + req := httptest.NewRequest("GET", "/bugs", nil) + w := httptest.NewRecorder() + h.HandleGetBugs(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Code) + } + + mr := h.renderer.(*MockRenderer) + var found bool + for _, call := range mr.Calls { + if call.Name == "bugs" { + found = true + bugs, ok := call.Data.([]store.Bug) + if !ok { + t.Fatalf("Expected []store.Bug data, got %T", call.Data) + } + if len(bugs) != 0 { + t.Errorf("Expected 0 bugs in template data, got %d", len(bugs)) + } + break + } + } + if !found { + t.Error("Expected renderer called with 'bugs' template") + } +} + +// TestHandleReportBug_Success verifies that a valid bug report is saved and the +// updated bugs list is re-rendered. +func TestHandleReportBug_Success(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + req := httptest.NewRequest("POST", "/report-bug", strings.NewReader("description=button+broken")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + h.HandleReportBug(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Code) + } + + // Verify bug was saved to store + bugs, err := h.store.GetBugs() + if err != nil { + t.Fatalf("GetBugs failed: %v", err) + } + if len(bugs) != 1 { + t.Fatalf("Expected 1 bug saved, got %d", len(bugs)) + } + if bugs[0].Description != "button broken" { + t.Errorf("Expected description 'button broken', got %q", bugs[0].Description) + } + + // Verify renderer was called with "bugs" template (HandleGetBugs is called internally) + mr := h.renderer.(*MockRenderer) + var bugsRendered bool + for _, call := range mr.Calls { + if call.Name == "bugs" { + bugsRendered = true + break + } + } + if !bugsRendered { + t.Error("Expected 'bugs' template to be rendered after successful report") + } +} -- cgit v1.2.3