summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/handlers.go55
-rw-r--r--internal/handlers/handlers_test.go269
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))
+ }
+}