summaryrefslogtreecommitdiff
path: root/internal/handlers/handlers_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/handlers_test.go')
-rw-r--r--internal/handlers/handlers_test.go295
1 files changed, 295 insertions, 0 deletions
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index 6f7cc92..1cab29d 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -502,6 +502,18 @@ func TestHandleCompleteAtom_Todoist(t *testing.T) {
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)
@@ -514,6 +526,64 @@ func TestHandleCompleteAtom_Todoist(t *testing.T) {
}
}
+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)
@@ -1621,6 +1691,24 @@ func TestHandleUncompleteAtom_Todoist(t *testing.T) {
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)
}
@@ -2162,3 +2250,210 @@ func TestHandleTimeline_InvalidParams(t *testing.T) {
t.Errorf("Expected status 200 or 500, got %d", w.Code)
}
}
+
+// syncAwareMockTodoist records the sync token passed to Sync and returns a configurable response.
+type syncAwareMockTodoist struct {
+ mockTodoistClient
+ syncResponse *api.TodoistSyncResponse
+ receivedTokens []string // tracks tokens passed to Sync
+}
+
+func (m *syncAwareMockTodoist) Sync(ctx context.Context, syncToken string) (*api.TodoistSyncResponse, error) {
+ m.receivedTokens = append(m.receivedTokens, syncToken)
+ if m.err != nil {
+ return nil, m.err
+ }
+ return m.syncResponse, nil
+}
+
+func TestFetchTasks_IncrementalSync_UpsertsActiveTasks(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ // Seed DB with an existing task (simulating previous full sync)
+ existingTask := models.Task{ID: "existing-1", Content: "Old task", Priority: 1}
+ if err := h.store.SaveTasks([]models.Task{existingTask}); err != nil {
+ t.Fatalf("Failed to seed task: %v", err)
+ }
+ // Set a sync token so fetchTasks uses incremental sync
+ if err := h.store.SetSyncToken("todoist", "previous-token"); err != nil {
+ t.Fatalf("Failed to set sync token: %v", err)
+ }
+
+ mock := &syncAwareMockTodoist{
+ syncResponse: &api.TodoistSyncResponse{
+ SyncToken: "new-token-1",
+ FullSync: false,
+ Items: []api.SyncItemResponse{
+ {ID: "new-1", Content: "New task", Priority: 2},
+ {ID: "existing-1", Content: "Updated task", Priority: 3},
+ },
+ Projects: []api.SyncProjectResponse{},
+ },
+ }
+ h.todoistClient = mock
+
+ tasks, err := h.fetchTasks(context.Background(), false)
+ if err != nil {
+ t.Fatalf("fetchTasks returned error: %v", err)
+ }
+
+ // Should have 2 tasks: the upserted existing-1 (updated) and new-1
+ if len(tasks) != 2 {
+ t.Fatalf("Expected 2 tasks, got %d", len(tasks))
+ }
+
+ // Verify the existing task was updated
+ taskMap := make(map[string]models.Task)
+ for _, task := range tasks {
+ taskMap[task.ID] = task
+ }
+ if taskMap["existing-1"].Content != "Updated task" {
+ t.Errorf("Expected existing-1 content 'Updated task', got %q", taskMap["existing-1"].Content)
+ }
+ if taskMap["new-1"].Content != "New task" {
+ t.Errorf("Expected new-1 content 'New task', got %q", taskMap["new-1"].Content)
+ }
+}
+
+func TestFetchTasks_IncrementalSync_DeletesCompletedAndDeletedTasks(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ // Seed DB with tasks
+ seeds := []models.Task{
+ {ID: "keep-1", Content: "Keep me"},
+ {ID: "complete-1", Content: "Will be completed"},
+ {ID: "delete-1", Content: "Will be deleted"},
+ }
+ if err := h.store.SaveTasks(seeds); err != nil {
+ t.Fatalf("Failed to seed tasks: %v", err)
+ }
+ if err := h.store.SetSyncToken("todoist", "prev-token"); err != nil {
+ t.Fatalf("Failed to set sync token: %v", err)
+ }
+
+ mock := &syncAwareMockTodoist{
+ syncResponse: &api.TodoistSyncResponse{
+ SyncToken: "new-token-2",
+ FullSync: false,
+ Items: []api.SyncItemResponse{
+ {ID: "complete-1", Content: "Will be completed", IsCompleted: true},
+ {ID: "delete-1", Content: "Will be deleted", IsDeleted: true},
+ },
+ Projects: []api.SyncProjectResponse{},
+ },
+ }
+ h.todoistClient = mock
+
+ tasks, err := h.fetchTasks(context.Background(), false)
+ if err != nil {
+ t.Fatalf("fetchTasks returned error: %v", err)
+ }
+
+ // Only keep-1 should remain
+ if len(tasks) != 1 {
+ t.Fatalf("Expected 1 task, got %d: %+v", len(tasks), tasks)
+ }
+ if tasks[0].ID != "keep-1" {
+ t.Errorf("Expected remaining task ID 'keep-1', got %q", tasks[0].ID)
+ }
+}
+
+func TestFetchTasks_IncrementalSync_StoresNewSyncToken(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.SetSyncToken("todoist", "old-token"); err != nil {
+ t.Fatalf("Failed to set sync token: %v", err)
+ }
+
+ mock := &syncAwareMockTodoist{
+ syncResponse: &api.TodoistSyncResponse{
+ SyncToken: "brand-new-token",
+ FullSync: false,
+ Items: []api.SyncItemResponse{},
+ Projects: []api.SyncProjectResponse{},
+ },
+ }
+ h.todoistClient = mock
+
+ _, err := h.fetchTasks(context.Background(), false)
+ if err != nil {
+ t.Fatalf("fetchTasks returned error: %v", err)
+ }
+
+ // Verify the new sync token was stored
+ token, err := h.store.GetSyncToken("todoist")
+ if err != nil {
+ t.Fatalf("Failed to get sync token: %v", err)
+ }
+ if token != "brand-new-token" {
+ t.Errorf("Expected sync token 'brand-new-token', got %q", token)
+ }
+}
+
+func TestFetchTasks_IncrementalSync_UsesSavedSyncToken(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ // Set a known sync token
+ if err := h.store.SetSyncToken("todoist", "my-saved-token"); err != nil {
+ t.Fatalf("Failed to set sync token: %v", err)
+ }
+
+ mock := &syncAwareMockTodoist{
+ syncResponse: &api.TodoistSyncResponse{
+ SyncToken: "next-token",
+ FullSync: false,
+ Items: []api.SyncItemResponse{},
+ Projects: []api.SyncProjectResponse{},
+ },
+ }
+ h.todoistClient = mock
+
+ _, err := h.fetchTasks(context.Background(), false)
+ if err != nil {
+ t.Fatalf("fetchTasks returned error: %v", err)
+ }
+
+ // Verify the saved token was passed to Sync
+ if len(mock.receivedTokens) != 1 {
+ t.Fatalf("Expected 1 Sync call, got %d", len(mock.receivedTokens))
+ }
+ if mock.receivedTokens[0] != "my-saved-token" {
+ t.Errorf("Expected Sync to receive token 'my-saved-token', got %q", mock.receivedTokens[0])
+ }
+}
+
+func TestFetchTasks_ForceRefresh_ClearsSyncToken(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.SetSyncToken("todoist", "existing-token"); err != nil {
+ t.Fatalf("Failed to set sync token: %v", err)
+ }
+
+ mock := &syncAwareMockTodoist{
+ syncResponse: &api.TodoistSyncResponse{
+ SyncToken: "fresh-token",
+ FullSync: true,
+ Items: []api.SyncItemResponse{},
+ Projects: []api.SyncProjectResponse{},
+ },
+ }
+ h.todoistClient = mock
+
+ _, err := h.fetchTasks(context.Background(), true)
+ if err != nil {
+ t.Fatalf("fetchTasks returned error: %v", err)
+ }
+
+ // forceRefresh should send empty token (full sync)
+ if len(mock.receivedTokens) != 1 {
+ t.Fatalf("Expected 1 Sync call, got %d", len(mock.receivedTokens))
+ }
+ if mock.receivedTokens[0] != "" {
+ t.Errorf("Expected empty sync token for forceRefresh, got %q", mock.receivedTokens[0])
+ }
+}