summaryrefslogtreecommitdiff
path: root/internal/handlers/timeline_logic_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-02-17 14:43:42 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-02-17 14:43:42 -1000
commitec7d895c00c571b37ad9255b99b2e1756776c9e1 (patch)
tree31f8a925375fd5b00ee5febfe5d83f35487b1dd3 /internal/handlers/timeline_logic_test.go
parent44fa97ce901bbfc5957e6d9ba90a53086bb5950b (diff)
Add calendar cache layer, incremental sync tests, completion assertions
- Google Calendar events now cached via CacheFetcher pattern with stale-cache fallback on API errors (new migration 015, store methods, fetchCalendarEvents handler, BuildTimeline reads from store) - Todoist incremental sync path covered by 5 new tests - Task completion tests assert response body, headers, and template data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/handlers/timeline_logic_test.go')
-rw-r--r--internal/handlers/timeline_logic_test.go157
1 files changed, 148 insertions, 9 deletions
diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go
index 11406b9..b42ad4c 100644
--- a/internal/handlers/timeline_logic_test.go
+++ b/internal/handlers/timeline_logic_test.go
@@ -2,11 +2,13 @@ package handlers
import (
"context"
+ "fmt"
"os"
"path/filepath"
"testing"
"time"
+ "task-dashboard/internal/config"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
@@ -79,6 +81,15 @@ func setupTestStore(t *testing.T) *store.Store {
url TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
+ CREATE TABLE IF NOT EXISTS calendar_events (
+ id TEXT PRIMARY KEY,
+ summary TEXT NOT NULL,
+ description TEXT,
+ start_time DATETIME NOT NULL,
+ end_time DATETIME NOT NULL,
+ html_link TEXT,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
`
if err := os.WriteFile(filepath.Join(migrationDir, "001_init.sql"), []byte(schema), 0644); err != nil {
t.Fatalf("Failed to write migration file: %v", err)
@@ -122,19 +133,17 @@ func TestBuildTimeline(t *testing.T) {
},
})
- // Calendar Event: 09:00
+ // Calendar Event: 09:00 (saved to store cache)
eventDate := baseTime.Add(1 * time.Hour)
- mockCal := &MockCalendarClient{
- Events: []models.CalendarEvent{
- {ID: "e1", Summary: "Event 1", Start: eventDate, End: eventDate.Add(1 * time.Hour)},
- },
- }
+ _ = s.SaveCalendarEvents([]models.CalendarEvent{
+ {ID: "e1", Summary: "Event 1", Start: eventDate, End: eventDate.Add(1 * time.Hour)},
+ })
// Test Range: Full Day
start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
- items, err := BuildTimeline(context.Background(), s, mockCal, nil, start, end)
+ items, err := BuildTimeline(context.Background(), s, nil, start, end)
if err != nil {
t.Fatalf("BuildTimeline failed: %v", err)
}
@@ -268,7 +277,7 @@ func TestBuildTimeline_IncludesOverdueItems(t *testing.T) {
// Query range: today through tomorrow
end := today.AddDate(0, 0, 1)
- items, err := BuildTimeline(context.Background(), s, nil, nil, today, end)
+ items, err := BuildTimeline(context.Background(), s, nil,today, end)
if err != nil {
t.Fatalf("BuildTimeline failed: %v", err)
}
@@ -311,7 +320,7 @@ func TestBuildTimeline_ExcludesCompletedOverdue(t *testing.T) {
{ID: "done1", Content: "Done overdue", DueDate: &yesterday, Completed: true},
})
- items, err := BuildTimeline(context.Background(), s, nil, nil, today, end)
+ items, err := BuildTimeline(context.Background(), s, nil,today, end)
if err != nil {
t.Fatalf("BuildTimeline failed: %v", err)
}
@@ -323,6 +332,136 @@ func TestBuildTimeline_ExcludesCompletedOverdue(t *testing.T) {
}
}
+func TestBuildTimeline_ReadsCalendarEventsFromStore(t *testing.T) {
+ s := setupTestStore(t)
+
+ start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
+ end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
+
+ // Save events to the store (simulating a prior cache)
+ eventTime := time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC)
+ err := s.SaveCalendarEvents([]models.CalendarEvent{
+ {ID: "cached-e1", Summary: "Cached Meeting", Start: eventTime, End: eventTime.Add(time.Hour)},
+ })
+ if err != nil {
+ t.Fatalf("Failed to save calendar events: %v", err)
+ }
+
+ // Call BuildTimeline with NO calendar client (nil) — events should come from store
+ items, err := BuildTimeline(context.Background(), s, nil,start, end)
+ if err != nil {
+ t.Fatalf("BuildTimeline failed: %v", err)
+ }
+
+ foundEvent := false
+ for _, item := range items {
+ if item.ID == "cached-e1" {
+ foundEvent = true
+ if item.Title != "Cached Meeting" {
+ t.Errorf("Expected title 'Cached Meeting', got %q", item.Title)
+ }
+ if item.Source != "calendar" {
+ t.Errorf("Expected source 'calendar', got %q", item.Source)
+ }
+ }
+ }
+ if !foundEvent {
+ t.Error("BuildTimeline should read calendar events from store, but cached event was not found")
+ }
+}
+
+func TestFetchCalendarEvents_CacheFallbackOnAPIError(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Pre-cache some calendar events
+ eventTime := time.Date(2023, 6, 15, 10, 0, 0, 0, time.UTC)
+ err := db.SaveCalendarEvents([]models.CalendarEvent{
+ {ID: "e-cached", Summary: "Cached Event", Start: eventTime, End: eventTime.Add(time.Hour)},
+ })
+ if err != nil {
+ t.Fatalf("Failed to seed calendar events: %v", err)
+ }
+ // Mark cache as valid
+ if err := db.UpdateCacheMetadata(store.CacheKeyGoogleCalendar, 60); err != nil {
+ t.Fatalf("Failed to update cache metadata: %v", err)
+ }
+
+ // Create handler with a failing calendar client
+ failingCal := &MockCalendarClient{Err: fmt.Errorf("API unavailable")}
+ h := &Handler{
+ store: db,
+ googleCalendarClient: failingCal,
+ config: &config.Config{CacheTTLMinutes: 5},
+ renderer: newTestRenderer(),
+ }
+
+ // Force refresh to hit the API (which fails), should fall back to cache
+ events, err := h.fetchCalendarEvents(context.Background(), true)
+ if err != nil {
+ t.Fatalf("fetchCalendarEvents should not return error on API failure with cached data, got: %v", err)
+ }
+
+ if len(events) != 1 {
+ t.Fatalf("Expected 1 cached event on fallback, got %d", len(events))
+ }
+ if events[0].ID != "e-cached" {
+ t.Errorf("Expected cached event ID 'e-cached', got %q", events[0].ID)
+ }
+}
+
+func TestSaveAndGetCalendarEvents(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ events := []models.CalendarEvent{
+ {
+ ID: "evt-1",
+ Summary: "Morning Standup",
+ Description: "Daily team sync",
+ Start: time.Date(2023, 6, 1, 9, 0, 0, 0, time.UTC),
+ End: time.Date(2023, 6, 1, 9, 30, 0, 0, time.UTC),
+ HTMLLink: "https://calendar.google.com/event/1",
+ },
+ {
+ ID: "evt-2",
+ Summary: "Lunch",
+ Start: time.Date(2023, 6, 1, 12, 0, 0, 0, time.UTC),
+ End: time.Date(2023, 6, 1, 13, 0, 0, 0, time.UTC),
+ },
+ }
+
+ if err := db.SaveCalendarEvents(events); err != nil {
+ t.Fatalf("SaveCalendarEvents failed: %v", err)
+ }
+
+ // Get all events
+ got, err := db.GetCalendarEvents()
+ if err != nil {
+ t.Fatalf("GetCalendarEvents failed: %v", err)
+ }
+ if len(got) != 2 {
+ t.Fatalf("Expected 2 events, got %d", len(got))
+ }
+ if got[0].Summary != "Morning Standup" {
+ t.Errorf("Expected first event 'Morning Standup', got %q", got[0].Summary)
+ }
+
+ // Get by date range (only morning)
+ rangeStart := time.Date(2023, 6, 1, 8, 0, 0, 0, time.UTC)
+ rangeEnd := time.Date(2023, 6, 1, 10, 0, 0, 0, time.UTC)
+ ranged, err := db.GetCalendarEventsByDateRange(rangeStart, rangeEnd)
+ if err != nil {
+ t.Fatalf("GetCalendarEventsByDateRange failed: %v", err)
+ }
+ if len(ranged) != 1 {
+ t.Fatalf("Expected 1 event in range, got %d", len(ranged))
+ }
+ if ranged[0].ID != "evt-1" {
+ t.Errorf("Expected event ID 'evt-1', got %q", ranged[0].ID)
+ }
+}
+
func timePtr(t time.Time) *time.Time {
return &t
}