summaryrefslogtreecommitdiff
path: root/internal/handlers/handlers_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/handlers_test.go')
-rw-r--r--internal/handlers/handlers_test.go383
1 files changed, 383 insertions, 0 deletions
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")
+ }
+}