From 945c34590677e161a711ccaff0e1077197b1b178 Mon Sep 17 00:00:00 2001 From: Doot Agent Date: Mon, 6 Jul 2026 00:00:59 +0000 Subject: feat: remove Todoist integration entirely Native tasks (native_tasks table) fully replace Todoist. All Todoist API code, store functions, handlers, routes, templates, and tests have been removed. Migration 021 drops the now-unused tasks cache table. Co-Authored-By: Claude Sonnet 4.6 --- internal/api/todoist_test.go | 399 ------------------------------------------- 1 file changed, 399 deletions(-) delete mode 100644 internal/api/todoist_test.go (limited to 'internal/api/todoist_test.go') diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go deleted file mode 100644 index d2c8da5..0000000 --- a/internal/api/todoist_test.go +++ /dev/null @@ -1,399 +0,0 @@ -package api - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" -) - -// newTestTodoistClient creates a TodoistClient for testing with custom base URL -func newTestTodoistClient(baseURL, apiKey string) *TodoistClient { - client := NewTodoistClient(apiKey) - client.BaseURL = baseURL - return client -} - -func TestTodoistClient_CreateTask(t *testing.T) { - // Mock server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify method and path - if r.Method != "POST" { - t.Errorf("Expected POST request, got %s", r.Method) - } - if r.URL.Path != "/tasks" { - t.Errorf("Expected path /tasks, got %s", r.URL.Path) - } - - // Verify Authorization header - if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { - t.Errorf("Expected Bearer token in Authorization header") - } - - // Parse JSON body - var payload map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - t.Errorf("Failed to decode request body: %v", err) - } - - // Verify content - if payload["content"] != "Test Task" { - t.Errorf("Expected content=Test Task, got %v", payload["content"]) - } - - if payload["project_id"] != "project-123" { - t.Errorf("Expected project_id=project-123, got %v", payload["project_id"]) - } - - // Return mock response - response := todoistTaskResponse{ - ID: "task-456", - Content: "Test Task", - Description: "", - ProjectID: "project-123", - Priority: 1, - Labels: []string{}, - URL: "https://todoist.com/task/456", - CreatedAt: time.Now().Format(time.RFC3339), - } - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - // Create client with mock server URL - client := newTestTodoistClient(server.URL, "test-key") - - // Test CreateTask - ctx := context.Background() - task, err := client.CreateTask(ctx, "Test Task", "project-123", nil, 0) - - if err != nil { - t.Fatalf("CreateTask failed: %v", err) - } - - // Verify response - if task.ID != "task-456" { - t.Errorf("Expected task ID task-456, got %s", task.ID) - } - if task.Content != "Test Task" { - t.Errorf("Expected task content Test Task, got %s", task.Content) - } - if task.ProjectID != "project-123" { - t.Errorf("Expected project ID project-123, got %s", task.ProjectID) - } -} - -func TestTodoistClient_CreateTask_WithDueDate(t *testing.T) { - dueDate := time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC) - - // Mock server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Parse JSON body - var payload map[string]interface{} - _ = json.NewDecoder(r.Body).Decode(&payload) - - // Verify due_date - if payload["due_date"] != "2026-01-15" { - t.Errorf("Expected due_date=2026-01-15, got %v", payload["due_date"]) - } - - // Return mock response with due date - response := todoistTaskResponse{ - ID: "task-789", - Content: "Task with Due Date", - ProjectID: "project-456", - URL: "https://todoist.com/task/789", - CreatedAt: time.Now().Format(time.RFC3339), - Due: &dueInfo{ - Date: "2026-01-15", - Datetime: "", - }, - } - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - // Create client - client := newTestTodoistClient(server.URL, "test-key") - - // Test CreateTask with due date - ctx := context.Background() - task, err := client.CreateTask(ctx, "Task with Due Date", "project-456", &dueDate, 0) - - if err != nil { - t.Fatalf("CreateTask failed: %v", err) - } - - // Verify due date is set - if task.DueDate == nil { - t.Error("Expected due date to be set") - } else { - expectedDate := time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC) - if !task.DueDate.Equal(expectedDate) { - t.Errorf("Expected due date %v, got %v", expectedDate, *task.DueDate) - } - } -} - -func TestParseDueDate_LocalDatetimeInDateField(t *testing.T) { - // Todoist REST API v1 puts local datetime (no tz offset) in the "date" field - // when datetime is not in UTC (e.g. recurring tasks with a set time) - // e.g. due={date="2026-03-22T19:00:00" datetime="" is_recurring=true} - due := &dueInfo{ - Date: "2026-03-22T19:00:00", - Datetime: "", - IsRecurring: true, - } - result := parseDueDate(due) - if result == nil { - t.Fatal("parseDueDate returned nil for date field containing local datetime — must parse YYYY-MM-DDTHH:MM:SS format") - } - if result.Hour() != 19 || result.Minute() != 0 { - t.Errorf("Expected 19:00, got %02d:%02d", result.Hour(), result.Minute()) - } -} - -func TestParseDueDate_MicrosecondDatetime(t *testing.T) { - // Todoist REST API v1 returns datetime with microseconds: "2023-01-15T10:00:00.000000Z" - // time.RFC3339 cannot parse fractional seconds — parseDueDate must use RFC3339Nano - due := &dueInfo{ - Date: "2023-01-15", - Datetime: "2023-01-15T10:00:00.000000Z", - IsRecurring: true, - } - result := parseDueDate(due) - if result == nil { - t.Fatal("parseDueDate returned nil for datetime with microseconds — RFC3339Nano required") - } - if result.Hour() != 10 || result.Minute() != 0 { - t.Errorf("Expected 10:00, got %02d:%02d", result.Hour(), result.Minute()) - } -} - -func TestParseDueDate_RFC3339Datetime(t *testing.T) { - // Standard RFC3339 without fractional seconds should also work - due := &dueInfo{ - Date: "2023-01-15", - Datetime: "2023-01-15T10:00:00Z", - } - result := parseDueDate(due) - if result == nil { - t.Fatal("parseDueDate returned nil for standard RFC3339 datetime") - } -} - -func TestTodoistClient_UsesAPIv1BaseURL(t *testing.T) { - client := NewTodoistClient("test-key") - const want = "https://api.todoist.com/api/v1" - if client.BaseURL != want { - t.Errorf("expected base URL %q, got %q — Todoist REST v2 is deprecated (HTTP 410)", want, client.BaseURL) - } -} - -func TestTodoistClient_CompleteTask(t *testing.T) { - // Mock server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify method and path - if r.Method != "POST" { - t.Errorf("Expected POST request, got %s", r.Method) - } - expectedPath := "/tasks/task-123/close" - if r.URL.Path != expectedPath { - t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) - } - - // Verify Authorization header - if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { - t.Errorf("Expected Bearer token in Authorization header") - } - - // Return 204 No Content - w.WriteHeader(http.StatusNoContent) - })) - defer server.Close() - - // Create client - client := newTestTodoistClient(server.URL, "test-key") - - // Test CompleteTask - ctx := context.Background() - err := client.CompleteTask(ctx, "task-123") - - if err != nil { - t.Fatalf("CompleteTask failed: %v", err) - } -} - -func TestTodoistClient_CompleteTask_Error(t *testing.T) { - // Mock server that returns error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - _, _ = w.Write([]byte(`{"error":"Task not found"}`)) - })) - defer server.Close() - - // Create client - client := newTestTodoistClient(server.URL, "test-key") - - // Test CompleteTask with error - ctx := context.Background() - err := client.CompleteTask(ctx, "invalid-task") - - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "404") { - t.Errorf("Expected error to contain status 404, got: %v", err) - } -} - -func TestTodoistClient_GetProjects(t *testing.T) { - // Mock server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify method and path - if r.Method != "GET" { - t.Errorf("Expected GET request, got %s", r.Method) - } - if r.URL.Path != "/projects" { - t.Errorf("Expected path /projects, got %s", r.URL.Path) - } - - // Return mock response - response := todoistProjectsPage{ - Results: []todoistProjectResponse{ - {ID: "proj-1", Name: "Project 1"}, - {ID: "proj-2", Name: "Project 2"}, - }, - } - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(response) - })) - defer server.Close() - - // Create client - client := newTestTodoistClient(server.URL, "test-key") - - // Test GetProjects - ctx := context.Background() - projects, err := client.GetProjects(ctx) - - if err != nil { - t.Fatalf("GetProjects failed: %v", err) - } - - // Verify response - if len(projects) != 2 { - t.Errorf("Expected 2 projects, got %d", len(projects)) - } - - if projects[0].ID != "proj-1" || projects[0].Name != "Project 1" { - t.Errorf("Project 1 mismatch: got ID=%s Name=%s", projects[0].ID, projects[0].Name) - } - - if projects[1].ID != "proj-2" || projects[1].Name != "Project 2" { - t.Errorf("Project 2 mismatch: got ID=%s Name=%s", projects[1].ID, projects[1].Name) - } -} - -func TestTodoistClient_GetTasks(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - t.Errorf("Expected GET, got %s", r.Method) - } - - w.Header().Set("Content-Type", "application/json") - - // GetTasks also calls GetProjects internally - if r.URL.Path == "/projects" { - response := todoistProjectsPage{ - Results: []todoistProjectResponse{ - {ID: "proj-1", Name: "Project 1"}, - }, - } - json.NewEncoder(w).Encode(response) - return - } - - if r.URL.Path == "/tasks" { - response := todoistTasksPage{ - Results: []todoistTaskResponse{ - {ID: "task-1", Content: "Task 1", ProjectID: "proj-1", CreatedAt: time.Now().Format(time.RFC3339)}, - {ID: "task-2", Content: "Task 2", ProjectID: "proj-1", CreatedAt: time.Now().Format(time.RFC3339)}, - }, - } - json.NewEncoder(w).Encode(response) - return - } - - t.Errorf("Unexpected path: %s", r.URL.Path) - })) - defer server.Close() - - client := newTestTodoistClient(server.URL, "test-key") - tasks, err := client.GetTasks(context.Background()) - if err != nil { - t.Fatalf("GetTasks failed: %v", err) - } - - if len(tasks) != 2 { - t.Errorf("Expected 2 tasks, got %d", len(tasks)) - } -} - -func TestTodoistClient_ReopenTask(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("Expected POST, got %s", r.Method) - } - expectedPath := "/tasks/task-123/reopen" - if r.URL.Path != expectedPath { - t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) - } - - w.WriteHeader(http.StatusNoContent) - })) - defer server.Close() - - client := newTestTodoistClient(server.URL, "test-key") - err := client.ReopenTask(context.Background(), "task-123") - if err != nil { - t.Fatalf("ReopenTask failed: %v", err) - } -} - -func TestTodoistClient_UpdateTask(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("Expected POST, got %s", r.Method) - } - expectedPath := "/tasks/task-123" - if r.URL.Path != expectedPath { - t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) - } - - var payload map[string]interface{} - json.NewDecoder(r.Body).Decode(&payload) - if payload["content"] != "Updated Content" { - t.Errorf("Expected content 'Updated Content', got %v", payload["content"]) - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"id": "task-123"}) - })) - defer server.Close() - - client := newTestTodoistClient(server.URL, "test-key") - err := client.UpdateTask(context.Background(), "task-123", map[string]interface{}{"content": "Updated Content"}) - if err != nil { - t.Fatalf("UpdateTask failed: %v", err) - } -} - -- cgit v1.2.3