diff options
| author | Agent <agent@workspace.local> | 2026-03-18 10:15:54 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-19 00:11:16 +0000 |
| commit | 5723d6635f9462e60960b0f9548273d95a86d8f3 (patch) | |
| tree | 2ce8ee5829f317012d9b1b3f9097fbfb923192c0 /internal/api | |
| parent | e85b42d373de55781af9d699b246c0d6a492aec1 (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')
| -rw-r--r-- | internal/api/google_tasks_test.go | 187 |
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) + } +} |
