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) } }