summaryrefslogtreecommitdiff
path: root/internal/api/google_tasks_test.go
diff options
context:
space:
mode:
authorAgent <agent@workspace.local>2026-03-18 10:15:54 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-19 00:11:16 +0000
commit5723d6635f9462e60960b0f9548273d95a86d8f3 (patch)
tree2ce8ee5829f317012d9b1b3f9097fbfb923192c0 /internal/api/google_tasks_test.go
parente85b42d373de55781af9d699b246c0d6a492aec1 (diff)
test: add coverage for planning tab, meals, Google Tasks, bug handlers, completed task parsing
- HandleTabPlanning: happy-path test verifying tasks/cards land in correct sections (scheduled/unscheduled/upcoming); boundary test confirming a task due exactly at midnight of tomorrow lands in Upcoming, not Scheduled - HandleTabMeals: grouping test verifying two meals sharing date+mealType produce one CombinedMeal with both recipe names merged - Google Tasks GetTasksByDateRange: four boundary tests (start inclusive, end exclusive, no-due-date always included, out-of-range excluded) using redirectingTransport mock server pattern - HandleGetBugs: data assertions verifying bug list and empty-list cases - HandleReportBug: success test verifying bug is saved and bugs template is re-rendered - GetCompletedTasks: timestamp parsing test ensuring CompletedAt is not zero when inserted with a known "2006-01-02 15:04:05" string Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/google_tasks_test.go')
-rw-r--r--internal/api/google_tasks_test.go187
1 files changed, 187 insertions, 0 deletions
diff --git a/internal/api/google_tasks_test.go b/internal/api/google_tasks_test.go
new file mode 100644
index 0000000..fbfdd63
--- /dev/null
+++ b/internal/api/google_tasks_test.go
@@ -0,0 +1,187 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "google.golang.org/api/option"
+ gtasks "google.golang.org/api/tasks/v1"
+)
+
+func newTestGoogleTasksClient(t *testing.T, server *httptest.Server, tasklistID string, tz *time.Location) *GoogleTasksClient {
+ t.Helper()
+ if tz == nil {
+ tz = time.UTC
+ }
+ httpClient := &http.Client{Transport: &redirectingTransport{server: server}}
+ srv, err := gtasks.NewService(context.Background(),
+ option.WithHTTPClient(httpClient),
+ option.WithoutAuthentication(),
+ )
+ if err != nil {
+ t.Fatalf("failed to create test tasks service: %v", err)
+ }
+ return &GoogleTasksClient{
+ srv: srv,
+ tasklistID: tasklistID,
+ displayTZ: tz,
+ }
+}
+
+func tasksAPIResponse(items []map[string]interface{}) string {
+ b, _ := json.Marshal(map[string]interface{}{
+ "kind": "tasks#tasks",
+ "items": items,
+ })
+ return string(b)
+}
+
+// newTasksServer returns an httptest.Server that serves a fixed tasks JSON body.
+func newTasksServer(body string) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !strings.Contains(r.URL.Path, "/lists/") {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(body))
+ }))
+}
+
+// --- GetTasksByDateRange boundary tests ---
+
+func TestGetTasksByDateRange_StartBoundaryIncluded(t *testing.T) {
+ // A task due exactly on the start date should be included (inclusive lower bound).
+ start := time.Date(2026, 3, 17, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
+
+ body := tasksAPIResponse([]map[string]interface{}{
+ {
+ "id": "task-start",
+ "title": "Task on start date",
+ "status": "needsAction",
+ "due": "2026-03-17T00:00:00Z",
+ "updated": "2026-03-17T00:00:00Z",
+ },
+ })
+ server := newTasksServer(body)
+ defer server.Close()
+
+ client := newTestGoogleTasksClient(t, server, "@default", time.UTC)
+ tasks, err := client.GetTasksByDateRange(context.Background(), start, end)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tasks) != 1 {
+ t.Fatalf("expected 1 task (start boundary included), got %d", len(tasks))
+ }
+ if tasks[0].ID != "task-start" {
+ t.Errorf("expected task-start, got %s", tasks[0].ID)
+ }
+}
+
+func TestGetTasksByDateRange_EndBoundaryExcluded(t *testing.T) {
+ // A task due exactly on the end date should be excluded (exclusive upper bound).
+ start := time.Date(2026, 3, 17, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
+
+ body := tasksAPIResponse([]map[string]interface{}{
+ {
+ "id": "task-end",
+ "title": "Task on end date",
+ "status": "needsAction",
+ "due": "2026-03-20T00:00:00Z",
+ "updated": "2026-03-17T00:00:00Z",
+ },
+ })
+ server := newTasksServer(body)
+ defer server.Close()
+
+ client := newTestGoogleTasksClient(t, server, "@default", time.UTC)
+ tasks, err := client.GetTasksByDateRange(context.Background(), start, end)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tasks) != 0 {
+ t.Fatalf("expected 0 tasks (end boundary excluded), got %d", len(tasks))
+ }
+}
+
+func TestGetTasksByDateRange_NoDueDateAlwaysIncluded(t *testing.T) {
+ // Tasks without a due date are always included regardless of the range.
+ start := time.Date(2026, 3, 17, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
+
+ body := tasksAPIResponse([]map[string]interface{}{
+ {
+ "id": "task-no-due",
+ "title": "No due date",
+ "status": "needsAction",
+ "updated": "2026-03-17T00:00:00Z",
+ // no "due" field
+ },
+ })
+ server := newTasksServer(body)
+ defer server.Close()
+
+ client := newTestGoogleTasksClient(t, server, "@default", time.UTC)
+ tasks, err := client.GetTasksByDateRange(context.Background(), start, end)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tasks) != 1 {
+ t.Fatalf("expected 1 task (no due date always included), got %d", len(tasks))
+ }
+ if tasks[0].DueDate != nil {
+ t.Errorf("expected nil DueDate, got %v", tasks[0].DueDate)
+ }
+}
+
+func TestGetTasksByDateRange_OutOfRangeExcluded(t *testing.T) {
+ // A task before start or after end should be excluded.
+ start := time.Date(2026, 3, 17, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
+
+ body := tasksAPIResponse([]map[string]interface{}{
+ {
+ "id": "task-before",
+ "title": "Task before range",
+ "status": "needsAction",
+ "due": "2026-03-16T00:00:00Z",
+ "updated": "2026-03-17T00:00:00Z",
+ },
+ {
+ "id": "task-after",
+ "title": "Task after range",
+ "status": "needsAction",
+ "due": "2026-03-21T00:00:00Z",
+ "updated": "2026-03-17T00:00:00Z",
+ },
+ {
+ "id": "task-in-range",
+ "title": "Task in range",
+ "status": "needsAction",
+ "due": "2026-03-18T00:00:00Z",
+ "updated": "2026-03-17T00:00:00Z",
+ },
+ })
+ server := newTasksServer(body)
+ defer server.Close()
+
+ client := newTestGoogleTasksClient(t, server, "@default", time.UTC)
+ tasks, err := client.GetTasksByDateRange(context.Background(), start, end)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tasks) != 1 {
+ t.Fatalf("expected 1 task (only in-range), got %d", len(tasks))
+ }
+ if tasks[0].ID != "task-in-range" {
+ t.Errorf("expected task-in-range, got %s", tasks[0].ID)
+ }
+}