summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/handlers.go6
-rw-r--r--internal/handlers/timeline_logic_test.go1
-rw-r--r--internal/handlers/widget.go168
-rw-r--r--internal/handlers/widget_test.go361
4 files changed, 531 insertions, 5 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 0d8b2da..408006d 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -698,7 +698,7 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) {
for _, b := range boards {
for _, c := range b.Cards {
if c.ID == id {
- title = c.Name
+ title, description = c.Name, c.Description
break
}
}
@@ -740,7 +740,7 @@ func (h *Handler) HandleTaskDetailPage(w http.ResponseWriter, r *http.Request) {
for _, b := range boards {
for _, c := range b.Cards {
if c.ID == id {
- title = c.Name
+ title, description = c.Name, c.Description
break
}
}
@@ -785,7 +785,7 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) {
var err error
switch source {
case "doot":
- err = h.store.UpdateNativeTask(id, "", description)
+ err = h.store.UpdateNativeTaskDescription(id, description)
case "trello":
err = h.trelloClient.UpdateCard(r.Context(), id, map[string]interface{}{"desc": description})
default:
diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go
index 5e7cb27..1da2d8a 100644
--- a/internal/handlers/timeline_logic_test.go
+++ b/internal/handlers/timeline_logic_test.go
@@ -75,6 +75,7 @@ func setupTestStore(t *testing.T) *store.Store {
CREATE TABLE IF NOT EXISTS cards (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
+ description TEXT DEFAULT '',
board_id TEXT NOT NULL,
list_id TEXT,
list_name TEXT,
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
index 6b1e774..f1f7452 100644
--- a/internal/handlers/widget.go
+++ b/internal/handlers/widget.go
@@ -46,9 +46,12 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem {
wi.Type = "event"
case models.TimelineItemTypeMeal:
wi.Type = "event"
- case models.TimelineItemTypeCard, models.TimelineItemTypeGTask:
+ case models.TimelineItemTypeCard:
wi.Type = "task"
- // not completable via widget API
+ wi.Completable = true
+ case models.TimelineItemTypeGTask:
+ wi.Type = "task"
+ wi.Completable = true
default:
wi.Type = "task"
wi.Completable = item.Source == "doot"
@@ -164,6 +167,135 @@ func (h *Handler) HandleWidgetReschedule(w http.ResponseWriter, r *http.Request)
w.WriteHeader(http.StatusOK)
}
+// findGoogleTask looks up a cached Google Task by ID.
+func (h *Handler) findGoogleTask(id string) (models.GoogleTask, bool) {
+ gTasks, err := h.store.GetGoogleTasks()
+ if err != nil {
+ return models.GoogleTask{}, false
+ }
+ for _, t := range gTasks {
+ if t.ID == id {
+ return t, true
+ }
+ }
+ return models.GoogleTask{}, false
+}
+
+// findCard looks up a cached Trello card by ID.
+func (h *Handler) findCard(id string) (models.Card, bool) {
+ boards, err := h.store.GetBoards()
+ if err != nil {
+ return models.Card{}, false
+ }
+ for _, b := range boards {
+ for _, c := range b.Cards {
+ if c.ID == id {
+ return c, true
+ }
+ }
+ }
+ return models.Card{}, false
+}
+
+type widgetDetailResponse struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Editable bool `json:"editable"`
+}
+
+// HandleWidgetDetail returns a task's title/description for the widget's edit popup.
+func (h *Handler) HandleWidgetDetail(w http.ResponseWriter, r *http.Request) {
+ id := r.URL.Query().Get("id")
+ source := r.URL.Query().Get("source")
+ if id == "" || source == "" {
+ http.Error(w, "missing id or source", http.StatusBadRequest)
+ return
+ }
+
+ var resp widgetDetailResponse
+ switch source {
+ case "doot":
+ tasks, err := h.store.GetNativeTasks()
+ if err != nil {
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ for _, t := range tasks {
+ if t.ID == id {
+ resp = widgetDetailResponse{Title: t.Content, Description: t.Description, Editable: true}
+ break
+ }
+ }
+ case "gtasks":
+ if t, ok := h.findGoogleTask(id); ok {
+ resp = widgetDetailResponse{Title: t.Title, Description: t.Notes, Editable: h.googleTasksClient != nil}
+ }
+ case "trello":
+ if c, ok := h.findCard(id); ok {
+ resp = widgetDetailResponse{Title: c.Name, Description: c.Description, Editable: h.trelloClient != nil}
+ }
+ default:
+ http.Error(w, "unsupported source", http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(resp)
+}
+
+type widgetUpdateRequest struct {
+ ID string `json:"id"`
+ Source string `json:"source"`
+ Description string `json:"description"`
+}
+
+// HandleWidgetUpdate saves an edited description from the widget's edit popup.
+func (h *Handler) HandleWidgetUpdate(w http.ResponseWriter, r *http.Request) {
+ var req widgetUpdateRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "bad request", http.StatusBadRequest)
+ return
+ }
+
+ switch req.Source {
+ case "doot":
+ if err := h.store.UpdateNativeTaskDescription(req.ID, req.Description); err != nil {
+ http.Error(w, "failed to update task", http.StatusInternalServerError)
+ return
+ }
+ case "gtasks":
+ if h.googleTasksClient == nil {
+ http.Error(w, "google tasks not configured", http.StatusServiceUnavailable)
+ return
+ }
+ t, ok := h.findGoogleTask(req.ID)
+ if !ok {
+ http.Error(w, "task not found", http.StatusNotFound)
+ return
+ }
+ if err := h.googleTasksClient.UpdateTaskNotes(r.Context(), t.ListID, req.ID, req.Description); err != nil {
+ http.Error(w, "failed to update task", http.StatusInternalServerError)
+ return
+ }
+ _ = h.store.InvalidateCache(store.CacheKeyGoogleTasks)
+ case "trello":
+ if h.trelloClient == nil {
+ http.Error(w, "trello not configured", http.StatusServiceUnavailable)
+ return
+ }
+ if err := h.trelloClient.UpdateCard(r.Context(), req.ID, map[string]interface{}{"desc": req.Description}); err != nil {
+ http.Error(w, "failed to update task", http.StatusInternalServerError)
+ return
+ }
+ _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards)
+ default:
+ http.Error(w, "source not editable", http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
// HandleWidgetComplete proxies a task completion to the source API or native store.
func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) {
var req widgetCompleteRequest
@@ -187,6 +319,38 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) {
return
}
_ = h.store.SaveCompletedTask("doot", req.ID, "", nil)
+ case "gtasks":
+ if h.googleTasksClient == nil {
+ http.Error(w, "google tasks not configured", http.StatusServiceUnavailable)
+ return
+ }
+ t, ok := h.findGoogleTask(req.ID)
+ if !ok {
+ http.Error(w, "task not found", http.StatusNotFound)
+ return
+ }
+ if err := h.googleTasksClient.CompleteTask(r.Context(), t.ListID, req.ID); err != nil {
+ http.Error(w, "failed to complete task", http.StatusInternalServerError)
+ return
+ }
+ _ = h.store.SaveCompletedTask("gtasks", req.ID, t.Title, t.DueDate)
+ _ = h.store.InvalidateCache(store.CacheKeyGoogleTasks)
+ case "trello":
+ if h.trelloClient == nil {
+ http.Error(w, "trello not configured", http.StatusServiceUnavailable)
+ return
+ }
+ c, ok := h.findCard(req.ID)
+ if !ok {
+ http.Error(w, "task not found", http.StatusNotFound)
+ return
+ }
+ if err := h.trelloClient.UpdateCard(r.Context(), req.ID, map[string]interface{}{"closed": true}); err != nil {
+ http.Error(w, "failed to complete task", http.StatusInternalServerError)
+ return
+ }
+ _ = h.store.SaveCompletedTask("trello", req.ID, c.Name, c.DueDate)
+ _ = h.store.DeleteCard(req.ID)
default:
http.Error(w, "source not completable", http.StatusBadRequest)
return
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index 610a55c..3116918 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -1,6 +1,7 @@
package handlers
import (
+ "context"
"encoding/json"
"fmt"
"net/http"
@@ -10,8 +11,65 @@ import (
"time"
"task-dashboard/internal/models"
+ "task-dashboard/internal/store"
)
+// mockGoogleTasksClient is a minimal api.GoogleTasksAPI stub for widget handler tests.
+type mockGoogleTasksClient struct {
+ completedListID, completedTaskID string
+ notesListID, notesTaskID, notes string
+}
+
+func (m *mockGoogleTasksClient) GetTasks(ctx context.Context) ([]models.GoogleTask, error) {
+ return nil, nil
+}
+func (m *mockGoogleTasksClient) GetTasksByDateRange(ctx context.Context, start, end time.Time) ([]models.GoogleTask, error) {
+ return nil, nil
+}
+func (m *mockGoogleTasksClient) CompleteTask(ctx context.Context, listID, taskID string) error {
+ m.completedListID, m.completedTaskID = listID, taskID
+ return nil
+}
+func (m *mockGoogleTasksClient) UncompleteTask(ctx context.Context, listID, taskID string) error {
+ return nil
+}
+func (m *mockGoogleTasksClient) UpdateTaskNotes(ctx context.Context, listID, taskID, notes string) error {
+ m.notesListID, m.notesTaskID, m.notes = listID, taskID, notes
+ return nil
+}
+func (m *mockGoogleTasksClient) GetTaskLists(ctx context.Context) ([]models.TaskListInfo, error) {
+ return nil, nil
+}
+func (m *mockGoogleTasksClient) SetTaskListID(id string) {}
+
+// mockTrelloWidgetClient is a minimal api.TrelloAPI stub for widget handler tests
+// that records UpdateCard calls (the shared mockTrelloClient in handlers_test.go doesn't).
+type mockTrelloWidgetClient struct {
+ updatedCardID string
+ updates map[string]interface{}
+}
+
+func (m *mockTrelloWidgetClient) GetBoards(ctx context.Context) ([]models.Board, error) {
+ return nil, nil
+}
+func (m *mockTrelloWidgetClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) {
+ return nil, nil
+}
+func (m *mockTrelloWidgetClient) GetLists(ctx context.Context, boardID string) ([]models.List, error) {
+ return nil, nil
+}
+func (m *mockTrelloWidgetClient) GetBoardsWithCards(ctx context.Context) ([]models.Board, error) {
+ return nil, nil
+}
+func (m *mockTrelloWidgetClient) CreateCard(ctx context.Context, listID, name, description string, dueDate *time.Time) (*models.Card, error) {
+ return nil, nil
+}
+func (m *mockTrelloWidgetClient) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error {
+ m.updatedCardID = cardID
+ m.updates = updates
+ return nil
+}
+
func TestWidgetAuthMiddleware_NoToken(t *testing.T) {
called := false
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })
@@ -295,6 +353,149 @@ func TestHandleWidgetComplete_NonCompletable(t *testing.T) {
}
}
+func TestHandleWidgetDetail_NativeTask(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ task := models.Task{ID: "t1", Content: "Buy milk", Description: "2%, not skim"}
+ if err := db.CreateNativeTask(task); err != nil {
+ t.Fatalf("failed to seed native task: %v", err)
+ }
+
+ h := &Handler{store: db}
+ req := httptest.NewRequest("GET", "/api/widget/detail?id=t1&source=doot", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ var resp widgetDetailResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+ if resp.Title != "Buy milk" || resp.Description != "2%, not skim" || !resp.Editable {
+ t.Errorf("unexpected detail: %+v", resp)
+ }
+}
+
+func TestHandleWidgetDetail_UnsupportedSource(t *testing.T) {
+ h := &Handler{}
+ req := httptest.NewRequest("GET", "/api/widget/detail?id=x&source=calendar", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400, got %d", w.Code)
+ }
+}
+
+func TestHandleWidgetUpdate_NativeTask_PreservesContent(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ task := models.Task{ID: "t2", Content: "Original title", Description: "old desc"}
+ if err := db.CreateNativeTask(task); err != nil {
+ t.Fatalf("failed to seed native task: %v", err)
+ }
+
+ h := &Handler{store: db}
+ body := `{"id":"t2","source":"doot","description":"new desc"}`
+ req := httptest.NewRequest("POST", "/api/widget/update", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetUpdate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ tasks, err := db.GetNativeTasks()
+ if err != nil {
+ t.Fatalf("failed to read back native tasks: %v", err)
+ }
+ if len(tasks) != 1 {
+ t.Fatalf("expected 1 native task, got %d", len(tasks))
+ }
+ if tasks[0].Content != "Original title" {
+ t.Errorf("title should be preserved, got %q", tasks[0].Content)
+ }
+ if tasks[0].Description != "new desc" {
+ t.Errorf("description should be updated, got %q", tasks[0].Description)
+ }
+}
+
+func TestHandleWidgetUpdate_UnsupportedSource(t *testing.T) {
+ h := &Handler{}
+ body := `{"id":"x","source":"calendar","description":"anything"}`
+ req := httptest.NewRequest("POST", "/api/widget/update", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetUpdate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Fatalf("expected 400, got %d", w.Code)
+ }
+}
+
+func TestTimelineItemToWidgetItem_GTask_Completable(t *testing.T) {
+ item := models.TimelineItem{
+ ID: "g1",
+ Title: "Renew passport",
+ Source: "gtasks",
+ Type: models.TimelineItemTypeGTask,
+ }
+
+ wi := TimelineItemToWidgetItem(item)
+
+ if wi.Type != "task" {
+ t.Errorf("Type: got %q, want %q", wi.Type, "task")
+ }
+ if !wi.Completable {
+ t.Error("gtask should be completable")
+ }
+}
+
+func TestTimelineItemToWidgetItem_Card_Completable(t *testing.T) {
+ item := models.TimelineItem{
+ ID: "c1",
+ Title: "Trello card",
+ Source: "trello",
+ Type: models.TimelineItemTypeCard,
+ }
+
+ wi := TimelineItemToWidgetItem(item)
+
+ if !wi.Completable {
+ t.Error("trello card should be completable via widget")
+ }
+}
+
+func TestHandleWidgetComplete_GoogleTask(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ if err := db.SaveGoogleTasks([]models.GoogleTask{
+ {ID: "g1", Title: "Renew passport", ListID: "list-a", UpdatedAt: time.Now()},
+ }); err != nil {
+ t.Fatalf("failed to seed google task: %v", err)
+ }
+
+ mock := &mockGoogleTasksClient{}
+ h := &Handler{store: db, googleTasksClient: mock}
+
+ body := `{"id":"g1","source":"gtasks"}`
+ req := httptest.NewRequest("POST", "/api/widget/complete", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetComplete).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ if mock.completedListID != "list-a" || mock.completedTaskID != "g1" {
+ t.Errorf("unexpected complete call: listID=%q taskID=%q", mock.completedListID, mock.completedTaskID)
+ }
+}
+
// TestHandleWidgetComplete_UnknownID_Returns404 proves the 2026-07-12 fix at
// the handler layer: a "doot" completion for an id that doesn't exist must
// surface as 404, not the previous silent 200 (see
@@ -315,6 +516,166 @@ func TestHandleWidgetComplete_UnknownID_Returns404(t *testing.T) {
}
}
+func TestHandleWidgetComplete_GoogleTask_NotConfigured(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ h := &Handler{store: db}
+ body := `{"id":"g1","source":"gtasks"}`
+ req := httptest.NewRequest("POST", "/api/widget/complete", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetComplete).ServeHTTP(w, req)
+
+ if w.Code != http.StatusServiceUnavailable {
+ t.Fatalf("expected 503, got %d", w.Code)
+ }
+}
+
+func TestHandleWidgetDetail_GoogleTask(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ if err := db.SaveGoogleTasks([]models.GoogleTask{
+ {ID: "g1", Title: "Renew passport", Notes: "bring photo", ListID: "list-a", UpdatedAt: time.Now()},
+ }); err != nil {
+ t.Fatalf("failed to seed google task: %v", err)
+ }
+
+ h := &Handler{store: db, googleTasksClient: &mockGoogleTasksClient{}}
+ req := httptest.NewRequest("GET", "/api/widget/detail?id=g1&source=gtasks", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ var resp widgetDetailResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+ if resp.Title != "Renew passport" || resp.Description != "bring photo" || !resp.Editable {
+ t.Errorf("unexpected detail: %+v", resp)
+ }
+}
+
+func TestHandleWidgetUpdate_GoogleTask(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ if err := db.SaveGoogleTasks([]models.GoogleTask{
+ {ID: "g1", Title: "Renew passport", ListID: "list-a", UpdatedAt: time.Now()},
+ }); err != nil {
+ t.Fatalf("failed to seed google task: %v", err)
+ }
+
+ mock := &mockGoogleTasksClient{}
+ h := &Handler{store: db, googleTasksClient: mock}
+
+ body := `{"id":"g1","source":"gtasks","description":"bring photo and $170"}`
+ req := httptest.NewRequest("POST", "/api/widget/update", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetUpdate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ if mock.notesListID != "list-a" || mock.notesTaskID != "g1" || mock.notes != "bring photo and $170" {
+ t.Errorf("unexpected update call: listID=%q taskID=%q notes=%q", mock.notesListID, mock.notesTaskID, mock.notes)
+ }
+}
+
+func seedTestCard(t *testing.T, db *store.Store) {
+ t.Helper()
+ err := db.SaveBoards([]models.Board{
+ {
+ ID: "board1",
+ Name: "Test Board",
+ Cards: []models.Card{
+ {ID: "c1", Name: "Ship the widget", Description: "don't forget the checkbox", ListName: "Doing"},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("failed to seed card: %v", err)
+ }
+}
+
+func TestHandleWidgetDetail_TrelloCard(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ seedTestCard(t, db)
+
+ h := &Handler{store: db, trelloClient: &mockTrelloWidgetClient{}}
+ req := httptest.NewRequest("GET", "/api/widget/detail?id=c1&source=trello", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ var resp widgetDetailResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+ if resp.Title != "Ship the widget" || resp.Description != "don't forget the checkbox" || !resp.Editable {
+ t.Errorf("unexpected detail: %+v", resp)
+ }
+}
+
+func TestHandleWidgetUpdate_TrelloCard(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ seedTestCard(t, db)
+
+ mock := &mockTrelloWidgetClient{}
+ h := &Handler{store: db, trelloClient: mock}
+
+ body := `{"id":"c1","source":"trello","description":"updated desc"}`
+ req := httptest.NewRequest("POST", "/api/widget/update", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetUpdate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ if mock.updatedCardID != "c1" || mock.updates["desc"] != "updated desc" {
+ t.Errorf("unexpected update call: id=%q updates=%v", mock.updatedCardID, mock.updates)
+ }
+}
+
+func TestHandleWidgetComplete_TrelloCard_Archives(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+ seedTestCard(t, db)
+
+ mock := &mockTrelloWidgetClient{}
+ h := &Handler{store: db, trelloClient: mock}
+
+ body := `{"id":"c1","source":"trello"}`
+ req := httptest.NewRequest("POST", "/api/widget/complete", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetComplete).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ if mock.updatedCardID != "c1" || mock.updates["closed"] != true {
+ t.Errorf("expected card to be archived (closed=true), got id=%q updates=%v", mock.updatedCardID, mock.updates)
+ }
+
+ boards, err := db.GetBoards()
+ if err != nil {
+ t.Fatalf("failed to read back boards: %v", err)
+ }
+ for _, b := range boards {
+ for _, c := range b.Cards {
+ if c.ID == "c1" {
+ t.Error("completed card should have been removed from local cache")
+ }
+ }
+ }
+}
+
// TestHandleWidgetAdd_CreatesTask proves the quick-add feature: POSTing a
// title to /api/widget/add creates an undated native task the same way the
// web UI's HandleUnifiedAdd does, but via the widget's bearer-token JSON