summaryrefslogtreecommitdiff
path: root/internal/handlers/widget_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-14 20:35:55 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:44:52 +0000
commit146034b08446c4bf0099bb41ab5e689d418e6dce (patch)
tree46db3cca04eb3d57b58a5195bbe42d0c193a9043 /internal/handlers/widget_test.go
parentba5205213f6f1995ca4e64b08925bcc6c48b8211 (diff)
feat(tasks): add HTTP endpoints for task detail, update, and recurrence
GET /api/widget/task, POST /api/widget/task/update, POST /api/widget/task/recurrence, POST /api/widget/task/next-date. Doot-only; the recurrence/next-date fields are null in the detail response for a non-recurring task.
Diffstat (limited to 'internal/handlers/widget_test.go')
-rw-r--r--internal/handlers/widget_test.go197
1 files changed, 197 insertions, 0 deletions
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index 2a6967e..35c21e2 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -816,3 +816,200 @@ func TestTimelineItemToWidgetItem_NonRecurring_EmptyRecurringEventID(t *testing.
}
}
+func TestHandleWidgetTaskDetail_ReturnsFullDetail(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ if _, err := s.DB().Exec(`
+ INSERT INTO native_tasks (id, content, description, due_date, recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id)
+ VALUES ('rec-1', 'Water plants', 'Use the blue can', '2026-07-13', 'weekly', 1, '1,3,5', 'series-1')
+ `); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/api/widget/task?id=rec-1&source=doot", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ var resp taskDetailResponse
+ if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+ if resp.Title != "Water plants" {
+ t.Errorf("Title = %q, want %q", resp.Title, "Water plants")
+ }
+ if resp.Description != "Use the blue can" {
+ t.Errorf("Description = %q, want %q", resp.Description, "Use the blue can")
+ }
+ if resp.Recurrence == nil {
+ t.Fatal("expected non-nil Recurrence")
+ }
+ if resp.Recurrence.Freq != "weekly" || len(resp.Recurrence.Weekdays) != 3 {
+ t.Errorf("Recurrence = %+v, want freq=weekly with 3 weekdays", resp.Recurrence)
+ }
+ if resp.NextDate == nil {
+ t.Fatal("expected computed NextDate for a recurring task")
+ }
+}
+
+func TestHandleWidgetTaskDetail_NonRecurring_NilRecurrenceAndNextDate(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ if _, err := s.DB().Exec(`INSERT INTO native_tasks (id, content) VALUES ('plain-1', 'Buy milk')`); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/api/widget/task?id=plain-1&source=doot", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+ var resp taskDetailResponse
+ if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+ if resp.Recurrence != nil {
+ t.Errorf("expected nil Recurrence for a non-recurring task, got %+v", resp.Recurrence)
+ }
+ if resp.NextDate != nil {
+ t.Errorf("expected nil NextDate for a non-recurring task, got %v", resp.NextDate)
+ }
+}
+
+func TestHandleWidgetTaskDetail_UnknownID_Returns404(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ req := httptest.NewRequest("GET", "/api/widget/task?id=does-not-exist&source=doot", nil)
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskDetail).ServeHTTP(w, req)
+
+ if w.Code != http.StatusNotFound {
+ t.Errorf("expected 404, got %d", w.Code)
+ }
+}
+
+func TestHandleWidgetTaskUpdate_UpdatesTitleAndDescription(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ if _, err := s.DB().Exec(`INSERT INTO native_tasks (id, content) VALUES ('plain-1', 'Old title')`); err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"plain-1","title":"New title","description":"New description"}`
+ req := httptest.NewRequest("POST", "/api/widget/task/update", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskUpdate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ task, err := s.GetNativeTaskByID("plain-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.Content != "New title" || task.Description != "New description" {
+ t.Errorf("task = %+v, want title/description updated", task)
+ }
+}
+
+func TestHandleWidgetTaskRecurrence_SetsPattern(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ if _, err := s.DB().Exec(`INSERT INTO native_tasks (id, content) VALUES ('plain-1', 'Water plants')`); err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"plain-1","freq":"weekly","interval":2,"weekdays":[1,3]}`
+ req := httptest.NewRequest("POST", "/api/widget/task/recurrence", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskRecurrence).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ task, err := s.GetNativeTaskByID("plain-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.RecurrenceFreq != "weekly" || task.RecurrenceInterval != 2 || task.RecurrenceSeriesID == "" {
+ t.Errorf("task = %+v, want recurrence set with a generated series id", task)
+ }
+}
+
+func TestHandleWidgetTaskRecurrence_InvalidFreq_Returns400(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ body := `{"id":"plain-1","freq":"bogus","interval":1}`
+ req := httptest.NewRequest("POST", "/api/widget/task/recurrence", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskRecurrence).ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected 400 for an invalid freq, got %d", w.Code)
+ }
+}
+
+func TestHandleWidgetTaskNextDate_SetsOverride(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ if _, err := s.DB().Exec(`
+ INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_series_id)
+ VALUES ('rec-1', 'Water plants', '2026-07-13', 'weekly', 'series-1')
+ `); err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"rec-1","date":"2026-08-01"}`
+ req := httptest.NewRequest("POST", "/api/widget/task/next-date", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskNextDate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ task, err := s.GetNativeTaskByID("rec-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.NextOccurrenceOverride == nil || task.NextOccurrenceOverride.Format("2006-01-02") != "2026-08-01" {
+ t.Errorf("NextOccurrenceOverride = %v, want 2026-08-01", task.NextOccurrenceOverride)
+ }
+}
+
+func TestHandleWidgetTaskNextDate_NonRecurringTask_Returns400(t *testing.T) {
+ s, cleanup := setupTestDB(t)
+ defer cleanup()
+ h := &Handler{store: s}
+
+ if _, err := s.DB().Exec(`INSERT INTO native_tasks (id, content) VALUES ('plain-1', 'Buy milk')`); err != nil {
+ t.Fatal(err)
+ }
+
+ body := `{"id":"plain-1","date":"2026-08-01"}`
+ req := httptest.NewRequest("POST", "/api/widget/task/next-date", strings.NewReader(body))
+ w := httptest.NewRecorder()
+ http.HandlerFunc(h.HandleWidgetTaskNextDate).ServeHTTP(w, req)
+
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("expected 400 for a non-recurring task, got %d", w.Code)
+ }
+}
+