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 client.syncClient.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 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 := []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) } }