From 8dbb6f43577b8a86e94ef7aaee196f9743356643 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 18 Jan 2026 15:24:58 -1000 Subject: Implement unified task completion with cache sync (Phase 3 Step 7) Add checkbox UI to Tasks tab for completing Todoist tasks and archiving Trello cards. Fix cache synchronization so completed items stay gone after page reload by deleting them from SQLite cache after API success. - Add HandleCompleteAtom handler routing to Todoist/Trello APIs - Add DeleteTask/DeleteCard store methods for cache removal - Add htmx.process() calls after innerHTML updates in app.js - Add comprehensive tests for completion and cache behavior Co-Authored-By: Claude Opus 4.5 --- internal/handlers/handlers_test.go | 269 +++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) (limited to 'internal/handlers/handlers_test.go') diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index ac940bb..3ea2a3e 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -395,3 +395,272 @@ func TestHandleGetMeals(t *testing.T) { t.Errorf("Expected 0 meals when client is nil, got %d", len(meals)) } } + +// mockTodoistClientWithComplete tracks CompleteTask calls +type mockTodoistClientWithComplete struct { + mockTodoistClient + completedTaskIDs []string + completeErr error +} + +func (m *mockTodoistClientWithComplete) CompleteTask(ctx context.Context, taskID string) error { + if m.completeErr != nil { + return m.completeErr + } + m.completedTaskIDs = append(m.completedTaskIDs, taskID) + return nil +} + +// mockTrelloClientWithUpdate tracks UpdateCard calls +type mockTrelloClientWithUpdate struct { + mockTrelloClient + updatedCards []string + updateErr error +} + +func (m *mockTrelloClientWithUpdate) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error { + if m.updateErr != nil { + return m.updateErr + } + m.updatedCards = append(m.updatedCards, cardID) + return nil +} + +// TestHandleCompleteAtom_Todoist tests completing a Todoist task +func TestHandleCompleteAtom_Todoist(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + // Save a task to the cache + tasks := []models.Task{ + { + ID: "task123", + Content: "Test task", + Completed: false, + Labels: []string{}, + CreatedAt: time.Now(), + }, + } + if err := db.SaveTasks(tasks); err != nil { + t.Fatalf("Failed to save test task: %v", err) + } + + // Verify task exists in cache + cachedTasks, _ := db.GetTasks() + if len(cachedTasks) != 1 { + t.Fatalf("Expected 1 task in cache, got %d", len(cachedTasks)) + } + + // Create handler with mock client + mockTodoist := &mockTodoistClientWithComplete{} + h := &Handler{ + store: db, + todoistClient: mockTodoist, + config: &config.Config{}, + } + + // Create request + req := httptest.NewRequest("POST", "/complete-atom", nil) + req.Form = map[string][]string{ + "id": {"task123"}, + "source": {"todoist"}, + } + w := httptest.NewRecorder() + + // Execute handler + h.HandleCompleteAtom(w, req) + + // Check response + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } + + // Verify CompleteTask was called on the API + if len(mockTodoist.completedTaskIDs) != 1 || mockTodoist.completedTaskIDs[0] != "task123" { + t.Errorf("Expected CompleteTask to be called with 'task123', got %v", mockTodoist.completedTaskIDs) + } + + // Verify task was deleted from cache + cachedTasks, _ = db.GetTasks() + if len(cachedTasks) != 0 { + t.Errorf("Expected task to be deleted from cache, but found %d tasks", len(cachedTasks)) + } +} + +// TestHandleCompleteAtom_Trello tests completing a Trello card +func TestHandleCompleteAtom_Trello(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + // Save a board with a card to the cache + boards := []models.Board{ + { + ID: "board1", + Name: "Test Board", + Cards: []models.Card{ + { + ID: "card456", + Name: "Test Card", + ListID: "list1", + ListName: "To Do", + }, + }, + }, + } + if err := db.SaveBoards(boards); err != nil { + t.Fatalf("Failed to save test board: %v", err) + } + + // Verify card exists in cache + cachedBoards, _ := db.GetBoards() + if len(cachedBoards) != 1 || len(cachedBoards[0].Cards) != 1 { + t.Fatalf("Expected 1 board with 1 card in cache") + } + + // Create handler with mock client + mockTrello := &mockTrelloClientWithUpdate{} + h := &Handler{ + store: db, + trelloClient: mockTrello, + config: &config.Config{}, + } + + // Create request + req := httptest.NewRequest("POST", "/complete-atom", nil) + req.Form = map[string][]string{ + "id": {"card456"}, + "source": {"trello"}, + } + w := httptest.NewRecorder() + + // Execute handler + h.HandleCompleteAtom(w, req) + + // Check response + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } + + // Verify UpdateCard was called on the API + if len(mockTrello.updatedCards) != 1 || mockTrello.updatedCards[0] != "card456" { + t.Errorf("Expected UpdateCard to be called with 'card456', got %v", mockTrello.updatedCards) + } + + // Verify card was deleted from cache + cachedBoards, _ = db.GetBoards() + if len(cachedBoards[0].Cards) != 0 { + t.Errorf("Expected card to be deleted from cache, but found %d cards", len(cachedBoards[0].Cards)) + } +} + +// TestHandleCompleteAtom_MissingParams tests error handling for missing parameters +func TestHandleCompleteAtom_MissingParams(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + h := &Handler{ + store: db, + config: &config.Config{}, + } + + tests := []struct { + name string + id string + source string + }{ + {"missing id", "", "todoist"}, + {"missing source", "task123", ""}, + {"both missing", "", ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("POST", "/complete-atom", nil) + req.Form = map[string][]string{ + "id": {tc.id}, + "source": {tc.source}, + } + w := httptest.NewRecorder() + + h.HandleCompleteAtom(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status 400 for %s, got %d", tc.name, w.Code) + } + }) + } +} + +// TestHandleCompleteAtom_UnknownSource tests error handling for unknown source +func TestHandleCompleteAtom_UnknownSource(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + h := &Handler{ + store: db, + config: &config.Config{}, + } + + req := httptest.NewRequest("POST", "/complete-atom", nil) + req.Form = map[string][]string{ + "id": {"task123"}, + "source": {"unknown"}, + } + w := httptest.NewRecorder() + + h.HandleCompleteAtom(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("Expected status 400 for unknown source, got %d", w.Code) + } +} + +// TestHandleCompleteAtom_APIError tests that API errors are handled correctly +func TestHandleCompleteAtom_APIError(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + // Save a task to the cache + tasks := []models.Task{ + { + ID: "task123", + Content: "Test task", + Completed: false, + Labels: []string{}, + CreatedAt: time.Now(), + }, + } + if err := db.SaveTasks(tasks); err != nil { + t.Fatalf("Failed to save test task: %v", err) + } + + // Create handler with mock client that returns an error + mockTodoist := &mockTodoistClientWithComplete{ + completeErr: context.DeadlineExceeded, + } + h := &Handler{ + store: db, + todoistClient: mockTodoist, + config: &config.Config{}, + } + + req := httptest.NewRequest("POST", "/complete-atom", nil) + req.Form = map[string][]string{ + "id": {"task123"}, + "source": {"todoist"}, + } + w := httptest.NewRecorder() + + h.HandleCompleteAtom(w, req) + + // Should return 500 on API error + if w.Code != http.StatusInternalServerError { + t.Errorf("Expected status 500 on API error, got %d", w.Code) + } + + // Verify task was NOT deleted from cache (rollback behavior) + cachedTasks, _ := db.GetTasks() + if len(cachedTasks) != 1 { + t.Errorf("Task should NOT be deleted from cache on API error, found %d tasks", len(cachedTasks)) + } +} -- cgit v1.2.3