diff options
Diffstat (limited to 'internal/api/google_tasks_test.go')
| -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) + } +} |
