diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-02-03 15:16:35 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-02-03 15:16:35 -1000 |
| commit | 25a5b7ecf9ddd31da54e91f87988b77aea857571 (patch) | |
| tree | 30654edbdd966cea316a5f54a99474aad337cf58 /internal/api | |
| parent | 9f35f7149d8fb790bbe8e4f0ee74f895aea1fc58 (diff) | |
Add comprehensive test coverage across packages
New test files:
- api/http_test.go: HTTP client and error handling tests
- config/config_test.go: Configuration loading and validation tests
- middleware/security_test.go: Security middleware tests
- models/atom_test.go: Atom model and conversion tests
Expanded test coverage:
- api/todoist_test.go: Todoist API client tests
- api/trello_test.go: Trello API client tests
- auth/auth_test.go: Authentication and CSRF tests
- handlers/timeline_logic_test.go: Timeline building logic tests
- store/sqlite_test.go: SQLite store operations tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/http_test.go | 207 | ||||
| -rw-r--r-- | internal/api/todoist_test.go | 183 | ||||
| -rw-r--r-- | internal/api/trello_test.go | 164 |
3 files changed, 554 insertions, 0 deletions
diff --git a/internal/api/http_test.go b/internal/api/http_test.go new file mode 100644 index 0000000..c2c32ee --- /dev/null +++ b/internal/api/http_test.go @@ -0,0 +1,207 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestNewBaseClient(t *testing.T) { + client := NewBaseClient("https://api.example.com") + if client.BaseURL != "https://api.example.com" { + t.Errorf("Expected BaseURL 'https://api.example.com', got '%s'", client.BaseURL) + } + if client.HTTPClient == nil { + t.Error("HTTPClient should not be nil") + } +} + +func TestBaseClient_Get(t *testing.T) { + expected := map[string]string{"message": "hello"} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET, got %s", r.Method) + } + if r.URL.Path != "/test" { + t.Errorf("Expected path /test, got %s", r.URL.Path) + } + if r.Header.Get("Authorization") != "Bearer token123" { + t.Errorf("Expected Authorization header") + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(expected) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]string + err := client.Get(context.Background(), "/test", map[string]string{"Authorization": "Bearer token123"}, &result) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if result["message"] != "hello" { + t.Errorf("Expected message 'hello', got '%s'", result["message"]) + } +} + +func TestBaseClient_Get_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]string + err := client.Get(context.Background(), "/test", nil, &result) + if err == nil { + t.Error("Expected error for 500 response") + } +} + +func TestBaseClient_Post(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) + } + if r.Header.Get("Content-Type") != "application/json" { + t.Error("Expected Content-Type application/json") + } + + var body map[string]string + json.NewDecoder(r.Body).Decode(&body) + if body["name"] != "test" { + t.Errorf("Expected name 'test', got '%s'", body["name"]) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]int{"id": 42}) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]int + err := client.Post(context.Background(), "/create", nil, map[string]string{"name": "test"}, &result) + if err != nil { + t.Fatalf("Post failed: %v", err) + } + if result["id"] != 42 { + t.Errorf("Expected id 42, got %d", result["id"]) + } +} + +func TestBaseClient_Post_NilBody(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Content-Type") == "application/json" { + t.Error("Content-Type should not be set for nil body") + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]string + err := client.Post(context.Background(), "/action", nil, nil, &result) + if err != nil { + t.Fatalf("Post with nil body failed: %v", err) + } +} + +func TestBaseClient_PostForm(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) + } + if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + t.Error("Expected form-urlencoded Content-Type") + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"success": true}) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]bool + err := client.PostForm(context.Background(), "/submit", nil, "key=value", &result) + if err != nil { + t.Fatalf("PostForm failed: %v", err) + } + if !result["success"] { + t.Error("Expected success to be true") + } +} + +func TestBaseClient_PostEmpty(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) + } + if r.Header.Get("X-Custom") != "custom-value" { + t.Error("Expected custom header") + } + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + err := client.PostEmpty(context.Background(), "/trigger", map[string]string{"X-Custom": "custom-value"}) + if err != nil { + t.Fatalf("PostEmpty failed: %v", err) + } +} + +func TestBaseClient_PostEmpty_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("bad request")) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + err := client.PostEmpty(context.Background(), "/fail", nil) + if err == nil { + t.Error("Expected error for 400 response") + } +} + +func TestBaseClient_Put(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "PUT" { + t.Errorf("Expected PUT, got %s", r.Method) + } + if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { + t.Error("Expected form-urlencoded Content-Type") + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"updated": "yes"}) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]string + err := client.Put(context.Background(), "/update", nil, "field=value", &result) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + if result["updated"] != "yes" { + t.Error("Expected updated to be yes") + } +} + +func TestBaseClient_doJSON_DecodeError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("invalid json")) + })) + defer server.Close() + + client := BaseClient{HTTPClient: server.Client(), BaseURL: server.URL} + var result map[string]string + err := client.Get(context.Background(), "/bad-json", nil, &result) + if err == nil { + t.Error("Expected error for invalid JSON") + } +} diff --git a/internal/api/todoist_test.go b/internal/api/todoist_test.go index 7bbcc1e..f7ca719 100644 --- a/internal/api/todoist_test.go +++ b/internal/api/todoist_test.go @@ -246,3 +246,186 @@ func TestTodoistClient_GetProjects(t *testing.T) { 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 := []todoistProjectResponse{ + {ID: "proj-1", Name: "Project 1"}, + } + json.NewEncoder(w).Encode(response) + return + } + + if r.URL.Path == "/tasks" { + response := []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) + } +} + +func TestTodoistClient_Sync(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) + } + if r.URL.Path != "/sync" { + t.Errorf("Expected path /sync, got %s", r.URL.Path) + } + + response := TodoistSyncResponse{ + SyncToken: "new-sync-token", + FullSync: true, + Items: []SyncItemResponse{ + {ID: "item-1", Content: "Item 1", ProjectID: "proj-1"}, + }, + Projects: []SyncProjectResponse{ + {ID: "proj-1", Name: "Project 1"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + client := newTestTodoistClient(server.URL, "test-key") + resp, err := client.Sync(context.Background(), "*") + if err != nil { + t.Fatalf("Sync failed: %v", err) + } + + if resp.SyncToken != "new-sync-token" { + t.Errorf("Expected sync token 'new-sync-token', got '%s'", resp.SyncToken) + } + if len(resp.Items) != 1 { + t.Errorf("Expected 1 item, got %d", len(resp.Items)) + } +} + +func TestConvertSyncItemsToTasks(t *testing.T) { + projects := map[string]string{ + "proj-1": "Project 1", + } + + items := []SyncItemResponse{ + { + ID: "item-1", + Content: "Task 1", + Description: "Description 1", + ProjectID: "proj-1", + Priority: 3, + Labels: []string{"label1"}, + }, + { + ID: "item-2", + Content: "Completed Task", + ProjectID: "proj-1", + IsCompleted: true, + }, + } + + tasks := ConvertSyncItemsToTasks(items, projects) + + // Should skip completed task + if len(tasks) != 1 { + t.Errorf("Expected 1 task (excluding completed), got %d", len(tasks)) + } + + if tasks[0].ID != "item-1" { + t.Errorf("Expected task ID 'item-1', got '%s'", tasks[0].ID) + } + if tasks[0].ProjectName != "Project 1" { + t.Errorf("Expected project name 'Project 1', got '%s'", tasks[0].ProjectName) + } +} + +func TestBuildProjectMapFromSync(t *testing.T) { + projects := []SyncProjectResponse{ + {ID: "proj-1", Name: "Project 1"}, + {ID: "proj-2", Name: "Project 2"}, + } + + projectMap := BuildProjectMapFromSync(projects) + + if len(projectMap) != 2 { + t.Errorf("Expected 2 projects in map, got %d", len(projectMap)) + } + + if projectMap["proj-1"] != "Project 1" { + t.Errorf("Expected 'Project 1', got '%s'", projectMap["proj-1"]) + } +} diff --git a/internal/api/trello_test.go b/internal/api/trello_test.go index d677363..a209d01 100644 --- a/internal/api/trello_test.go +++ b/internal/api/trello_test.go @@ -239,3 +239,167 @@ func parseFormData(data string) (map[string]string, error) { } return result, nil } + +func TestTrelloClient_GetBoards(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) + } + if r.URL.Path != "/members/me/boards" { + t.Errorf("Expected path /members/me/boards, got %s", r.URL.Path) + } + + response := []trelloBoardResponse{ + {ID: "board-1", Name: "Board 1"}, + {ID: "board-2", Name: "Board 2"}, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + client := newTestTrelloClient(server.URL, "test-key", "test-token") + boards, err := client.GetBoards(context.Background()) + if err != nil { + t.Fatalf("GetBoards failed: %v", err) + } + + if len(boards) != 2 { + t.Errorf("Expected 2 boards, got %d", len(boards)) + } + if boards[0].ID != "board-1" { + t.Errorf("Expected board ID 'board-1', got '%s'", boards[0].ID) + } +} + +func TestTrelloClient_GetCards(t *testing.T) { + dueDate := "2024-01-15T12:00:00Z" + 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") + + // GetCards calls getLists internally + if strings.Contains(r.URL.Path, "/lists") { + response := []trelloListResponse{ + {ID: "list-1", Name: "To Do"}, + } + json.NewEncoder(w).Encode(response) + return + } + + if strings.Contains(r.URL.Path, "/cards") { + response := []trelloCardResponse{ + {ID: "card-1", Name: "Card 1", IDList: "list-1", IDBoard: "board-1", Due: &dueDate}, + {ID: "card-2", Name: "Card 2", IDList: "list-1", IDBoard: "board-1"}, + } + json.NewEncoder(w).Encode(response) + return + } + + t.Errorf("Unexpected path: %s", r.URL.Path) + })) + defer server.Close() + + client := newTestTrelloClient(server.URL, "test-key", "test-token") + cards, err := client.GetCards(context.Background(), "board-1") + if err != nil { + t.Fatalf("GetCards failed: %v", err) + } + + if len(cards) != 2 { + t.Errorf("Expected 2 cards, got %d", len(cards)) + } + if cards[0].DueDate == nil { + t.Error("Expected due date for card 1") + } +} + +func TestTrelloClient_GetLists(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) + } + if !strings.Contains(r.URL.Path, "/boards/board-1/lists") { + t.Errorf("Expected path to contain /boards/board-1/lists, got %s", r.URL.Path) + } + + response := []trelloListResponse{ + {ID: "list-1", Name: "To Do"}, + {ID: "list-2", Name: "Done"}, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + client := newTestTrelloClient(server.URL, "test-key", "test-token") + lists, err := client.GetLists(context.Background(), "board-1") + if err != nil { + t.Fatalf("GetLists failed: %v", err) + } + + if len(lists) != 2 { + t.Errorf("Expected 2 lists, got %d", len(lists)) + } + if lists[0].Name != "To Do" { + t.Errorf("Expected 'To Do', got '%s'", lists[0].Name) + } +} + +func TestTrelloClient_GetBoardsWithCards(t *testing.T) { + requestCount := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + + if strings.Contains(r.URL.Path, "/members/me/boards") { + response := []trelloBoardResponse{ + {ID: "board-1", Name: "Board 1"}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if strings.Contains(r.URL.Path, "/lists") { + response := []trelloListResponse{ + {ID: "list-1", Name: "To Do"}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + if strings.Contains(r.URL.Path, "/cards") { + response := []trelloCardResponse{ + {ID: "card-1", Name: "Card 1", IDList: "list-1", IDBoard: "board-1"}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + t.Errorf("Unexpected path: %s", r.URL.Path) + })) + defer server.Close() + + client := newTestTrelloClient(server.URL, "test-key", "test-token") + boards, err := client.GetBoardsWithCards(context.Background()) + if err != nil { + t.Fatalf("GetBoardsWithCards failed: %v", err) + } + + if len(boards) != 1 { + t.Errorf("Expected 1 board, got %d", len(boards)) + } + if len(boards[0].Cards) != 1 { + t.Errorf("Expected 1 card, got %d", len(boards[0].Cards)) + } + if boards[0].Cards[0].ListName != "To Do" { + t.Errorf("Expected list name 'To Do', got '%s'", boards[0].Cards[0].ListName) + } +} |
