diff options
Diffstat (limited to 'internal/handlers')
| -rw-r--r-- | internal/handlers/handlers.go | 55 | ||||
| -rw-r--r-- | internal/handlers/handlers_test.go | 269 |
2 files changed, 324 insertions, 0 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 9ba6351..b3bc8e4 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -672,3 +672,58 @@ func (h *Handler) HandleCompleteTask(w http.ResponseWriter, r *http.Request) { // Return empty response (task will be removed from DOM) w.WriteHeader(http.StatusOK) } + +// HandleCompleteAtom handles completion of a unified task (Atom) +func (h *Handler) HandleCompleteAtom(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if err := r.ParseForm(); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + log.Printf("Error parsing form: %v", err) + return + } + + id := r.FormValue("id") + source := r.FormValue("source") + + if id == "" || source == "" { + http.Error(w, "Missing id or source", http.StatusBadRequest) + return + } + + var err error + switch source { + case "todoist": + err = h.todoistClient.CompleteTask(ctx, id) + case "trello": + // Archive the card (closed = true) + updates := map[string]interface{}{ + "closed": true, + } + err = h.trelloClient.UpdateCard(ctx, id, updates) + default: + http.Error(w, "Unknown source: "+source, http.StatusBadRequest) + return + } + + if err != nil { + http.Error(w, "Failed to complete task", http.StatusInternalServerError) + log.Printf("Error completing atom (source=%s, id=%s): %v", source, id, err) + return + } + + // Remove from local cache + switch source { + case "todoist": + if err := h.store.DeleteTask(id); err != nil { + log.Printf("Warning: failed to delete task from cache: %v", err) + } + case "trello": + if err := h.store.DeleteCard(id); err != nil { + log.Printf("Warning: failed to delete card from cache: %v", err) + } + } + + // Return 200 OK with empty body to remove the element from DOM + w.WriteHeader(http.StatusOK) +} 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)) + } +} |
