summaryrefslogtreecommitdiff
path: root/internal/api/todoist_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-13 14:18:24 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-13 14:18:24 -1000
commite107192be5efb65807c7da3b6aa99ce3555944d0 (patch)
tree12f9a03a0586ec79c13b3461d960ccb27d0ae117 /internal/api/todoist_test.go
parent2fee76ea41f37e3a068273c05a98b892ab29228c (diff)
Implement Todoist write operations - API layer (Part 1)
Add CreateTask and CompleteTask methods to Todoist API client: Models: - Add Project struct (ID, Name) to types.go - Add Projects []Project field to DashboardData API Interface: - Change GetProjects signature to return []models.Project - Ensure CreateTask and CompleteTask are defined Todoist Client: - Add baseURL field for testability - Refactor GetProjects to return []models.Project - Update GetTasks to build project map from new GetProjects - Implement CreateTask with JSON payload support - Implement CompleteTask using POST to /tasks/{id}/close Tests: - Create comprehensive todoist_test.go - Test CreateTask, CreateTask with due date, CompleteTask - Test error handling and GetProjects - Update mock client in handlers tests All tests pass. Ready for handlers and UI integration. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/todoist_test.go')
-rw-r--r--internal/api/todoist_test.go264
1 files changed, 264 insertions, 0 deletions
diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go
new file mode 100644
index 0000000..56b1484
--- /dev/null
+++ b/internal/api/todoist_test.go
@@ -0,0 +1,264 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+)
+
+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 := &TodoistClient{
+ apiKey: "test-key",
+ baseURL: server.URL,
+ httpClient: &http.Client{},
+ }
+
+ // 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
+ dueStruct := struct {
+ Date string `json:"date"`
+ Datetime string `json:"datetime"`
+ }{
+ Date: "2026-01-15",
+ Datetime: "",
+ }
+ 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: &dueStruct,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+ }))
+ defer server.Close()
+
+ // Create client
+ client := &TodoistClient{
+ apiKey: "test-key",
+ baseURL: server.URL,
+ httpClient: &http.Client{},
+ }
+
+ // 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 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 := &TodoistClient{
+ apiKey: "test-key",
+ baseURL: server.URL,
+ httpClient: &http.Client{},
+ }
+
+ // 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 := &TodoistClient{
+ apiKey: "test-key",
+ baseURL: server.URL,
+ httpClient: &http.Client{},
+ }
+
+ // 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 := []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 := &TodoistClient{
+ apiKey: "test-key",
+ baseURL: server.URL,
+ httpClient: &http.Client{},
+ }
+
+ // 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)
+ }
+}