diff options
Diffstat (limited to 'internal/handlers/handlers_test.go')
| -rw-r--r-- | internal/handlers/handlers_test.go | 802 |
1 files changed, 50 insertions, 752 deletions
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index b640c78..1f66378 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -84,40 +84,6 @@ func setupTestHandler(t *testing.T) (*Handler, func()) { return h, cleanup } -// mockTodoistClient creates a mock Todoist client for testing -type mockTodoistClient struct { - tasks []models.Task - err error -} - -func (m *mockTodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { - if m.err != nil { - return nil, m.err - } - return m.tasks, nil -} - -func (m *mockTodoistClient) GetProjects(ctx context.Context) ([]models.Project, error) { - return []models.Project{}, nil -} - -func (m *mockTodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) { - return nil, nil -} - -func (m *mockTodoistClient) UpdateTask(ctx context.Context, taskID string, updates map[string]interface{}) error { - return m.err -} - -func (m *mockTodoistClient) CompleteTask(ctx context.Context, taskID string) error { - return nil -} - -func (m *mockTodoistClient) ReopenTask(ctx context.Context, taskID string) error { - return nil -} - - // mockTrelloClient creates a mock Trello client for testing type mockTrelloClient struct { boards []models.Board @@ -154,79 +120,6 @@ func (m *mockTrelloClient) UpdateCard(ctx context.Context, cardID string, update return nil } -// TestHandleGetTasks tests the HandleGetTasks handler -func TestHandleGetTasks(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Create test tasks - testTasks := []models.Task{ - { - ID: "1", - Content: "Test task 1", - Description: "Description 1", - ProjectID: "proj1", - ProjectName: "Project 1", - Priority: 1, - Completed: false, - Labels: []string{"label1"}, - URL: "https://todoist.com/task/1", - CreatedAt: time.Now(), - }, - { - ID: "2", - Content: "Test task 2", - Description: "Description 2", - ProjectID: "proj2", - ProjectName: "Project 2", - Priority: 2, - Completed: true, - Labels: []string{"label2"}, - URL: "https://todoist.com/task/2", - CreatedAt: time.Now(), - }, - } - - // Save tasks to database - if err := db.SaveTasks(testTasks); err != nil { - t.Fatalf("Failed to save test tasks: %v", err) - } - - // Create handler with mock client - cfg := &config.Config{ - CacheTTLMinutes: 5, - } - mockTodoist := &mockTodoistClient{} - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: cfg, - } - - // Create test request - req := httptest.NewRequest("GET", "/api/tasks", nil) - w := httptest.NewRecorder() - - // Execute handler - h.HandleGetTasks(w, req) - - // Check response - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Parse response - var tasks []models.Task - if err := json.NewDecoder(w.Body).Decode(&tasks); err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - // Verify tasks - if len(tasks) != 2 { - t.Errorf("Expected 2 tasks, got %d", len(tasks)) - } -} - // TestHandleGetBoards tests the HandleGetBoards handler func TestHandleGetBoards(t *testing.T) { db, cleanup := setupTestDB(t) @@ -309,16 +202,6 @@ func TestHandleRefresh(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - // Create mock clients - mockTodoist := &mockTodoistClient{ - tasks: []models.Task{ - { - ID: "1", - Content: "Test task", - }, - }, - } - mockTrello := &mockTrelloClient{ boards: []models.Board{ { @@ -333,10 +216,9 @@ func TestHandleRefresh(t *testing.T) { CacheTTLMinutes: 5, } h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: cfg, + store: db, + trelloClient: mockTrello, + config: cfg, } // Create test request @@ -397,21 +279,6 @@ func TestHandleGetMeals(t *testing.T) { } } -// mockTodoistClientWithComplete tracks CompleteTask calls -type mockTodoistClientWithComplete struct { - mockTodoistClient - completedTaskIDs []string - completeErr error -} - -func (m *mockTodoistClientWithComplete) CompleteTask(ctx context.Context, taskID string) error { - if m.completeErr != nil { - return m.completeErr - } - m.completedTaskIDs = append(m.completedTaskIDs, taskID) - return nil -} - // mockTrelloClientWithUpdate tracks UpdateCard calls type mockTrelloClientWithUpdate struct { mockTrelloClient @@ -427,138 +294,6 @@ func (m *mockTrelloClientWithUpdate) UpdateCard(ctx context.Context, cardID stri return nil } -// TestHandleCompleteAtom_Todoist tests completing a Todoist task -func TestHandleCompleteAtom_Todoist(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Save a task to the cache - tasks := []models.Task{ - { - ID: "task123", - Content: "Test task", - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save test task: %v", err) - } - - // Verify task exists in cache - cachedTasks, _ := db.GetTasks() - if len(cachedTasks) != 1 { - t.Fatalf("Expected 1 task in cache, got %d", len(cachedTasks)) - } - - // Create handler with mock client - mockTodoist := &mockTodoistClientWithComplete{} - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, - renderer: newTestRenderer(), - } - - // Create request - req := httptest.NewRequest("POST", "/complete-atom", nil) - req.Form = map[string][]string{ - "id": {"task123"}, - "source": {"todoist"}, - } - w := httptest.NewRecorder() - - // Execute handler - h.HandleCompleteAtom(w, req) - - // Check response - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Verify response body contains rendered completed-atom template - body := w.Body.String() - if !strings.Contains(body, "rendered:completed-atom") { - t.Errorf("Expected response body to contain 'rendered:completed-atom', got %q", body) - } - - // Verify Content-Type header - ct := w.Header().Get("Content-Type") - if ct != "text/html; charset=utf-8" { - t.Errorf("Expected Content-Type 'text/html; charset=utf-8', got %q", ct) - } - - // Verify CompleteTask was called on the API - if len(mockTodoist.completedTaskIDs) != 1 || mockTodoist.completedTaskIDs[0] != "task123" { - t.Errorf("Expected CompleteTask to be called with 'task123', got %v", mockTodoist.completedTaskIDs) - } - - // Verify task was deleted from cache - cachedTasks, _ = db.GetTasks() - if len(cachedTasks) != 0 { - t.Errorf("Expected task to be deleted from cache, but found %d tasks", len(cachedTasks)) - } -} - -func TestHandleCompleteAtom_RendersCorrectTemplateData(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Seed a task with known title - tasks := []models.Task{ - {ID: "task-data-1", Content: "Buy groceries", Labels: []string{}, CreatedAt: time.Now()}, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to seed: %v", err) - } - - renderer := NewMockRenderer() - h := &Handler{ - store: db, - todoistClient: &mockTodoistClientWithComplete{}, - config: &config.Config{}, - renderer: renderer, - } - - req := httptest.NewRequest("POST", "/complete-atom", nil) - req.Form = map[string][]string{"id": {"task-data-1"}, "source": {"todoist"}} - w := httptest.NewRecorder() - - h.HandleCompleteAtom(w, req) - - // Verify renderer was called with "completed-atom" and correct data - if len(renderer.Calls) != 1 { - t.Fatalf("Expected 1 render call, got %d", len(renderer.Calls)) - } - call := renderer.Calls[0] - if call.Name != "completed-atom" { - t.Errorf("Expected template 'completed-atom', got %q", call.Name) - } - - // The data should contain the task ID, source, and title - type atomData struct { - ID string - Source string - Title string - } - // Use JSON round-trip to extract fields from the anonymous struct - jsonBytes, _ := json.Marshal(call.Data) - var got atomData - if err := json.Unmarshal(jsonBytes, &got); err != nil { - t.Fatalf("Failed to unmarshal render data: %v", err) - } - if got.ID != "task-data-1" { - t.Errorf("Expected data.ID 'task-data-1', got %q", got.ID) - } - if got.Source != "todoist" { - t.Errorf("Expected data.Source 'todoist', got %q", got.Source) - } - if got.Title != "Buy groceries" { - t.Errorf("Expected data.Title 'Buy groceries', got %q", got.Title) - } -} - // TestHandleCompleteAtom_Trello tests completing a Trello card func TestHandleCompleteAtom_Trello(t *testing.T) { db, cleanup := setupTestDB(t) @@ -641,7 +376,7 @@ func TestHandleCompleteAtom_MissingParams(t *testing.T) { id string source string }{ - {"missing id", "", "todoist"}, + {"missing id", "", "doot"}, {"missing source", "task123", ""}, {"both missing", "", ""}, } @@ -688,56 +423,6 @@ func TestHandleCompleteAtom_UnknownSource(t *testing.T) { } } -// TestHandleCompleteAtom_APIError tests that API errors are handled correctly -func TestHandleCompleteAtom_APIError(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Save a task to the cache - tasks := []models.Task{ - { - ID: "task123", - Content: "Test task", - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save test task: %v", err) - } - - // Create handler with mock client that returns an error - mockTodoist := &mockTodoistClientWithComplete{ - completeErr: context.DeadlineExceeded, - } - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, - } - - req := httptest.NewRequest("POST", "/complete-atom", nil) - req.Form = map[string][]string{ - "id": {"task123"}, - "source": {"todoist"}, - } - w := httptest.NewRecorder() - - h.HandleCompleteAtom(w, req) - - // Should return 500 on API error - if w.Code != http.StatusInternalServerError { - t.Errorf("Expected status 500 on API error, got %d", w.Code) - } - - // Verify task was NOT deleted from cache (rollback behavior) - cachedTasks, _ := db.GetTasks() - if len(cachedTasks) != 1 { - t.Errorf("Task should NOT be deleted from cache on API error, found %d tasks", len(cachedTasks)) - } -} - // TestHandleShoppingModeComplete tests completing (removing) shopping items func TestHandleShoppingModeComplete(t *testing.T) { db, cleanup := setupTestDB(t) @@ -1354,12 +1039,6 @@ func TestHandleTabTasks(t *testing.T) { config: &config.Config{CacheTTLMinutes: 5}, } - // Add some tasks - tasks := []models.Task{ - {ID: "1", Content: "Task 1", Labels: []string{}, CreatedAt: time.Now()}, - } - _ = db.SaveTasks(tasks) - req := httptest.NewRequest("GET", "/tabs/tasks", nil) w := httptest.NewRecorder() @@ -1599,69 +1278,6 @@ func TestHTMLString(t *testing.T) { // Uncomplete Atom Tests // ============================================================================= -// mockTodoistClientWithReopen tracks ReopenTask calls -type mockTodoistClientWithReopen struct { - mockTodoistClient - reopenedTaskIDs []string - reopenErr error -} - -func (m *mockTodoistClientWithReopen) ReopenTask(ctx context.Context, taskID string) error { - if m.reopenErr != nil { - return m.reopenErr - } - m.reopenedTaskIDs = append(m.reopenedTaskIDs, taskID) - return nil -} - -func TestHandleUncompleteAtom_Todoist(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - mockTodoist := &mockTodoistClientWithReopen{} - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, - renderer: newTestRenderer(), - } - - req := httptest.NewRequest("POST", "/uncomplete-atom", nil) - req.Form = map[string][]string{ - "id": {"task123"}, - "source": {"todoist"}, - } - w := httptest.NewRecorder() - - h.HandleUncompleteAtom(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Verify HX-Reswap header set to "none" - reswap := w.Header().Get("HX-Reswap") - if reswap != "none" { - t.Errorf("Expected HX-Reswap header 'none', got %q", reswap) - } - - // Verify HX-Trigger header set to "refresh-tasks" - trigger := w.Header().Get("HX-Trigger") - if trigger != "refresh-tasks" { - t.Errorf("Expected HX-Trigger header 'refresh-tasks', got %q", trigger) - } - - // Verify response body is empty (no template rendered) - body := w.Body.String() - if body != "" { - t.Errorf("Expected empty response body for uncomplete, got %q", body) - } - - if len(mockTodoist.reopenedTaskIDs) != 1 || mockTodoist.reopenedTaskIDs[0] != "task123" { - t.Errorf("Expected ReopenTask to be called with 'task123', got %v", mockTodoist.reopenedTaskIDs) - } -} - // ============================================================================= // Settings Handler Tests // ============================================================================= @@ -1769,16 +1385,6 @@ func TestHandleGetSourceOptions_MissingSource(t *testing.T) { } } -// mockTodoistClientWithProjects returns mock projects -type mockTodoistClientWithProjects struct { - mockTodoistClient - projects []models.Project -} - -func (m *mockTodoistClientWithProjects) GetProjects(ctx context.Context) ([]models.Project, error) { - return m.projects, nil -} - // mockTrelloClientWithBoards returns mock boards type mockTrelloClientWithBoards struct { mockTrelloClient @@ -1788,14 +1394,6 @@ func TestHandleSyncSources(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed tasks table with project info (projects now come from store, not REST API) - tasks := []models.Task{ - {ID: "t1", Content: "Task 1", ProjectID: "proj1", ProjectName: "Project 1", Labels: []string{}}, - {ID: "t2", Content: "Task 2", ProjectID: "proj2", ProjectName: "Project 2", Labels: []string{}}, - } - _ = h.store.SaveTasks(tasks) - - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClientWithBoards{ mockTrelloClient: mockTrelloClient{ boards: []models.Board{ @@ -1804,7 +1402,6 @@ func TestHandleSyncSources(t *testing.T) { }, } - h.todoistClient = mockTodoist h.trelloClient = mockTrello req := httptest.NewRequest("POST", "/settings/sync", nil) @@ -1816,10 +1413,10 @@ func TestHandleSyncSources(t *testing.T) { t.Errorf("Expected status 200, got %d", w.Code) } - // Verify configs were synced from store (not REST API) - configs, _ := h.store.GetSourceConfigsBySource("todoist") - if len(configs) != 2 { - t.Errorf("Expected 2 todoist configs, got %d", len(configs)) + // Verify Trello configs were synced + configs, _ := h.store.GetSourceConfigsBySource("trello") + if len(configs) != 1 { + t.Errorf("Expected 1 trello config, got %d", len(configs)) } } @@ -1921,14 +1518,6 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed tasks with due dates in range - now := config.Now() - todayNoon := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location()) - tasks := []models.Task{ - {ID: "t1", Content: "Today task", DueDate: &todayNoon, Labels: []string{}, CreatedAt: now}, - } - _ = h.store.SaveTasks(tasks) - req := httptest.NewRequest("GET", "/tabs/timeline", nil) w := httptest.NewRecorder() @@ -1949,14 +1538,10 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) { t.Errorf("Expected template 'timeline-tab', got '%s'", lastCall.Name) } - data, ok := lastCall.Data.(TimelineData) + _, ok := lastCall.Data.(TimelineData) if !ok { t.Fatalf("Expected TimelineData, got %T", lastCall.Data) } - - if len(data.TodayItems) == 0 { - t.Error("Expected TodayItems to contain at least one item, got 0") - } } // assertTemplateContains reads a template file and asserts it contains the expected string. @@ -1979,7 +1564,6 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClient{} // Use a renderer that outputs actual content so we can check for settings link @@ -1994,11 +1578,10 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) { } h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: &config.Config{CacheTTLMinutes: 5}, - renderer: renderer, + store: db, + trelloClient: mockTrello, + config: &config.Config{CacheTTLMinutes: 5}, + renderer: renderer, } req := httptest.NewRequest("GET", "/", nil) @@ -2112,17 +1695,15 @@ func TestHandleDashboard_PassesBuildVersion(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClient{} renderer := NewMockRenderer() h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: &config.Config{CacheTTLMinutes: 5}, - renderer: renderer, - BuildVersion: "abc123", + store: db, + trelloClient: mockTrello, + config: &config.Config{CacheTTLMinutes: 5}, + renderer: renderer, + BuildVersion: "abc123", } req := httptest.NewRequest("GET", "/", nil) @@ -2210,123 +1791,20 @@ func TestHandleTimeline_InvalidParams(t *testing.T) { } } -func TestFetchTasks_FetchesAndSavesTasks(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - h.todoistClient = &mockTodoistClient{ - tasks: []models.Task{ - {ID: "t1", Content: "Task one", Priority: 1}, - {ID: "t2", Content: "Task two", Priority: 2}, - }, - } - - tasks, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - if len(tasks) != 2 { - t.Fatalf("Expected 2 tasks, got %d", len(tasks)) - } - - // Verify tasks are persisted in the store - stored, err := h.store.GetTasks() - if err != nil { - t.Fatalf("Failed to get tasks from store: %v", err) - } - if len(stored) != 2 { - t.Errorf("Expected 2 stored tasks, got %d", len(stored)) - } -} - -func TestFetchTasks_ReturnsCachedTasksWhenValid(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed cache with tasks and mark it valid - cached := []models.Task{{ID: "cached-1", Content: "Cached task"}} - if err := h.store.SaveTasks(cached); err != nil { - t.Fatalf("Failed to seed tasks: %v", err) - } - if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil { - t.Fatalf("Failed to set cache metadata: %v", err) - } - - // API would return different tasks — should not be called - h.todoistClient = &mockTodoistClient{ - tasks: []models.Task{{ID: "api-1", Content: "API task"}}, - } - - tasks, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - if len(tasks) != 1 || tasks[0].ID != "cached-1" { - t.Errorf("Expected cached task, got %+v", tasks) - } -} - -func TestFetchTasks_ForceRefresh_BypassesCache(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed cache and mark it valid - if err := h.store.SaveTasks([]models.Task{{ID: "old-1", Content: "Old"}}); err != nil { - t.Fatalf("Failed to seed tasks: %v", err) - } - if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil { - t.Fatalf("Failed to set cache metadata: %v", err) - } - - h.todoistClient = &mockTodoistClient{ - tasks: []models.Task{{ID: "fresh-1", Content: "Fresh"}}, - } - - tasks, err := h.fetchTasks(context.Background(), true) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - if len(tasks) != 1 || tasks[0].ID != "fresh-1" { - t.Errorf("Expected fresh task from API, got %+v", tasks) - } -} - -func TestFetchTasks_FallsBackToCacheOnAPIError(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed stale cache - if err := h.store.SaveTasks([]models.Task{{ID: "stale-1", Content: "Stale"}}); err != nil { - t.Fatalf("Failed to seed tasks: %v", err) - } - - h.todoistClient = &mockTodoistClient{err: fmt.Errorf("API error")} - - tasks, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("Expected fallback to cache, got error: %v", err) - } - if len(tasks) != 1 || tasks[0].ID != "stale-1" { - t.Errorf("Expected stale cached task as fallback, got %+v", tasks) - } -} - // ============================================================================= -// Clear Cache + GetProjects removal tests +// Clear Cache tests // ============================================================================= // TestHandleClearCache verifies that POST /settings/clear-cache invalidates -// all cache keys, clears the todoist sync token, and returns 200 with HX-Trigger. +// all cache keys and returns 200 with HX-Trigger. func TestHandleClearCache(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed cache metadata and sync token so we can verify they get cleared - _ = h.store.UpdateCacheMetadata("todoist_tasks", 5) + // Seed cache metadata so we can verify it gets cleared _ = h.store.UpdateCacheMetadata("trello_boards", 5) _ = h.store.UpdateCacheMetadata("plantoeat_meals", 5) _ = h.store.UpdateCacheMetadata("google_calendar", 5) - _ = h.store.SetSyncToken("todoist", "some-sync-token") req := httptest.NewRequest("POST", "/settings/clear-cache", nil) w := httptest.NewRecorder() @@ -2343,62 +1821,20 @@ func TestHandleClearCache(t *testing.T) { } // Verify all cache keys invalidated - for _, key := range []string{"todoist_tasks", "trello_boards", "plantoeat_meals", "google_calendar"} { + for _, key := range []string{"trello_boards", "plantoeat_meals", "google_calendar"} { valid, _ := h.store.IsCacheValid(key) if valid { t.Errorf("Cache key %q should be invalidated but is still valid", key) } } - - // Verify sync token cleared - token, _ := h.store.GetSyncToken("todoist") - if token != "" { - t.Errorf("Expected empty sync token after clear cache, got %q", token) - } } -// TestGetProjectsFromTasks verifies that distinct project pairs are extracted -// from the tasks table. -func TestGetProjectsFromTasks(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed tasks with different projects (including duplicates and empty) - tasks := []models.Task{ - {ID: "1", Content: "Task 1", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - {ID: "2", Content: "Task 2", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - {ID: "3", Content: "Task 3", ProjectID: "p2", ProjectName: "Work", Labels: []string{}}, - {ID: "4", Content: "Task 4", ProjectID: "", ProjectName: "", Labels: []string{}}, - } - if err := h.store.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - - projects, err := h.store.GetProjectsFromTasks() - if err != nil { - t.Fatalf("GetProjectsFromTasks returned error: %v", err) - } - - if len(projects) != 2 { - t.Fatalf("Expected 2 distinct projects, got %d: %v", len(projects), projects) - } - - // Verify both projects are present - found := map[string]bool{} - for _, p := range projects { - found[p.ID] = true - } - if !found["p1"] || !found["p2"] { - t.Errorf("Expected projects p1 and p2, got %v", projects) - } -} - -// TestInvalidateAllCaches verifies the convenience method clears all 4 keys. +// TestInvalidateAllCaches verifies the convenience method clears all cache keys. func TestInvalidateAllCaches(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - keys := []string{"todoist_tasks", "trello_boards", "plantoeat_meals", "google_calendar"} + keys := []string{"trello_boards", "plantoeat_meals", "google_calendar"} for _, key := range keys { _ = h.store.UpdateCacheMetadata(key, 5) } @@ -2423,116 +1859,6 @@ func TestSettingsTemplate_HasClearCacheButton(t *testing.T) { "settings.html must contain a clear-cache button/endpoint") } -// TestAggregateData_DoesNotCallGetProjects verifies that aggregateData no longer -// calls the deprecated GetProjects REST API. -func TestAggregateData_DoesNotCallGetProjects(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - getProjectsCalled := false - mock := &mockTodoistClientTracksGetProjects{ - mockTodoistClient: mockTodoistClient{ - tasks: []models.Task{ - {ID: "1", Content: "Test", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - }, - }, - onGetProjects: func() { getProjectsCalled = true }, - } - h.todoistClient = mock - h.trelloClient = &mockTrelloClient{boards: []models.Board{}} - - _, err := h.aggregateData(context.Background(), false) - if err != nil { - t.Fatalf("aggregateData returned error: %v", err) - } - - if getProjectsCalled { - t.Error("aggregateData should NOT call GetProjects (deprecated REST API)") - } -} - -// TestHandleCreateTask_DoesNotCallGetProjects verifies that HandleCreateTask -// populates projects from the store, not the REST API. -func TestHandleCreateTask_DoesNotCallGetProjects(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed tasks with project info - tasks := []models.Task{ - {ID: "1", Content: "Existing", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - } - _ = h.store.SaveTasks(tasks) - - getProjectsCalled := false - mock := &mockTodoistClientTracksGetProjects{ - mockTodoistClient: mockTodoistClient{ - tasks: tasks, - }, - onGetProjects: func() { getProjectsCalled = true }, - } - h.todoistClient = mock - - body := strings.NewReader("content=New+Task&project_id=p1") - req := httptest.NewRequest("POST", "/tasks", body) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - h.HandleCreateTask(w, req) - - if getProjectsCalled { - t.Error("HandleCreateTask should NOT call GetProjects (deprecated REST API)") - } -} - -// TestHandleSyncSources_UsesStoreForProjects verifies that HandleSyncSources -// reads projects from the tasks table instead of the REST API. -func TestHandleSyncSources_UsesStoreForProjects(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed tasks with project info - tasks := []models.Task{ - {ID: "1", Content: "Task1", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - {ID: "2", Content: "Task2", ProjectID: "p2", ProjectName: "Work", Labels: []string{}}, - } - _ = h.store.SaveTasks(tasks) - - getProjectsCalled := false - mock := &mockTodoistClientTracksGetProjects{ - mockTodoistClient: mockTodoistClient{tasks: tasks}, - onGetProjects: func() { getProjectsCalled = true }, - } - h.todoistClient = mock - - req := httptest.NewRequest("POST", "/settings/sync", nil) - w := httptest.NewRecorder() - - h.HandleSyncSources(w, req) - - if getProjectsCalled { - t.Error("HandleSyncSources should NOT call GetProjects (deprecated REST API)") - } - - // Verify todoist project configs were still synced from store - configs, _ := h.store.GetSourceConfigsBySource("todoist") - if len(configs) != 2 { - t.Errorf("Expected 2 todoist configs from store, got %d", len(configs)) - } -} - -// mockTodoistClientTracksGetProjects wraps mockTodoistClient and tracks GetProjects calls. -type mockTodoistClientTracksGetProjects struct { - mockTodoistClient - onGetProjects func() -} - -func (m *mockTodoistClientTracksGetProjects) GetProjects(ctx context.Context) ([]models.Project, error) { - if m.onGetProjects != nil { - m.onGetProjects() - } - return []models.Project{}, nil -} - // --- Sync Log Tests --- // TestStore_AddAndGetSyncLog verifies that sync log entries can be added and retrieved. @@ -2678,7 +2004,7 @@ func TestHandleGetTaskDetail_RendersTemplate(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=todoist", nil) + req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=doot", nil) w := httptest.NewRecorder() h.HandleGetTaskDetail(w, req) @@ -2749,7 +2075,7 @@ func TestHandleSyncSources_AddsLogEntry(t *testing.T) { // HandleTabPlanning data tests // ============================================================================= -// TestHandleTabPlanning_HappyPath verifies that tasks, events, and cards are +// TestHandleTabPlanning_HappyPath verifies that cards are // placed into the correct planning sections (scheduled/unscheduled/upcoming). func TestHandleTabPlanning_HappyPath(t *testing.T) { db, cleanup := setupTestDB(t) @@ -2757,29 +2083,16 @@ func TestHandleTabPlanning_HappyPath(t *testing.T) { today := config.Today() - // Task at 10am today (has time) → goes to Scheduled - taskWithTime := today.Add(10 * time.Hour) - // Task at midnight today (no time component) → goes to Unscheduled - taskNoTime := today - // Task 2 days from now → goes to Upcoming (today+4 is the upper bound) - taskUpcoming := today.AddDate(0, 0, 2) - - tasks := []models.Task{ - {ID: "t-sched", Content: "Scheduled Task", DueDate: &taskWithTime, Labels: []string{}, CreatedAt: time.Now()}, - {ID: "t-unsched", Content: "Unscheduled Task", DueDate: &taskNoTime, Labels: []string{}, CreatedAt: time.Now()}, - {ID: "t-upcoming", Content: "Upcoming Task", DueDate: &taskUpcoming, Labels: []string{}, CreatedAt: time.Now()}, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - // Card at 2pm today → goes to Scheduled cardWithTime := today.Add(14 * time.Hour) + // Card 2 days from now → goes to Upcoming + cardUpcoming := today.AddDate(0, 0, 2) boards := []models.Board{{ ID: "board1", Name: "Board 1", Cards: []models.Card{ {ID: "c-sched", Name: "Scheduled Card", DueDate: &cardWithTime, URL: "http://trello.com/c1"}, + {ID: "c-upcoming", Name: "Upcoming Card", DueDate: &cardUpcoming, URL: "http://trello.com/c2"}, }, }} if err := db.SaveBoards(boards); err != nil { @@ -2818,65 +2131,51 @@ func TestHandleTabPlanning_HappyPath(t *testing.T) { Unscheduled []models.Atom Upcoming []ScheduledItem Boards []models.Board - Projects []models.Project Today string }) if !ok { t.Fatalf("Expected planning data struct, got %T", planningCall.Data) } - // t-sched (10am) and c-sched (2pm) should be in Scheduled - var foundTaskSched, foundCardSched bool + // c-sched (2pm) should be in Scheduled + var foundCardSched bool for _, s := range data.Scheduled { - if s.ID == "t-sched" { - foundTaskSched = true - } if s.ID == "c-sched" { foundCardSched = true } } - if !foundTaskSched { - t.Error("Expected t-sched (task with time today) in Scheduled") - } if !foundCardSched { t.Error("Expected c-sched (card with time today) in Scheduled") } - // t-unsched (midnight, no time) should be in Unscheduled - var foundUnsched bool - for _, u := range data.Unscheduled { - if u.ID == "t-unsched" { - foundUnsched = true - } - } - if !foundUnsched { - t.Error("Expected t-unsched (midnight task) in Unscheduled") - } - - // t-upcoming (2 days out) should be in Upcoming + // c-upcoming (2 days out) should be in Upcoming var foundUpcoming bool for _, u := range data.Upcoming { - if u.ID == "t-upcoming" { + if u.ID == "c-upcoming" { foundUpcoming = true } } if !foundUpcoming { - t.Error("Expected t-upcoming (task in 2 days) in Upcoming") + t.Error("Expected c-upcoming (card in 2 days) in Upcoming") } } -// TestHandleTabPlanning_TomorrowBoundary verifies that a task due exactly at +// TestHandleTabPlanning_TomorrowBoundary verifies that a card due exactly at // midnight of "tomorrow" is NOT before tomorrow, so it lands in Upcoming. func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() tomorrow := config.Today().AddDate(0, 0, 1) // midnight tomorrow - tasks := []models.Task{ - {ID: "t-boundary", Content: "Midnight Tomorrow", DueDate: &tomorrow, Labels: []string{}, CreatedAt: time.Now()}, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) + boards := []models.Board{{ + ID: "board1", + Name: "Board 1", + Cards: []models.Card{ + {ID: "c-boundary", Name: "Midnight Tomorrow", DueDate: &tomorrow, URL: "http://trello.com/c1"}, + }, + }} + if err := db.SaveBoards(boards); err != nil { + t.Fatalf("Failed to save boards: %v", err) } renderer := NewMockRenderer() @@ -2911,7 +2210,6 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { Unscheduled []models.Atom Upcoming []ScheduledItem Boards []models.Board - Projects []models.Project Today string }) if !ok { @@ -2920,24 +2218,24 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { // midnight-tomorrow: !dueDate.Before(tomorrow) → not in scheduled/unscheduled for _, s := range data.Scheduled { - if s.ID == "t-boundary" { - t.Error("Midnight-tomorrow task must not appear in Scheduled") + if s.ID == "c-boundary" { + t.Error("Midnight-tomorrow card must not appear in Scheduled") } } for _, u := range data.Unscheduled { - if u.ID == "t-boundary" { - t.Error("Midnight-tomorrow task must not appear in Unscheduled") + if u.ID == "c-boundary" { + t.Error("Midnight-tomorrow card must not appear in Unscheduled") } } // dueDate.Before(in3Days=today+4) → lands in Upcoming var foundUpcoming bool for _, u := range data.Upcoming { - if u.ID == "t-boundary" { + if u.ID == "c-boundary" { foundUpcoming = true } } if !foundUpcoming { - t.Error("Expected midnight-tomorrow task in Upcoming") + t.Error("Expected midnight-tomorrow card in Upcoming") } } |
