package web_test import ( "context" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/m/projax/caldav" "github.com/m/projax/web" ) // TestDashboardEventsCardSurfacesUpcoming spins up a fake CalDAV server that // answers REPORT for /Work-events/, seeds a projax item with a caldav-list // link pointing at that fake calendar, and asserts /dashboard renders the // Events card with the right row. func TestDashboardEventsCardSurfacesUpcoming(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() // Build a fixed-time fake calendar with one tomorrow-event and one all-day // event the day after. now := time.Now().UTC() tomorrow := now.AddDate(0, 0, 1) dayAfter := now.AddDate(0, 0, 2) icsBody := buildFakeEventsReportBody(t, tomorrow, dayAfter) mux := http.NewServeMux() mux.HandleFunc("/dav/calendars/m/Work-evts/", func(w http.ResponseWriter, r *http.Request) { if r.Method != "REPORT" { http.Error(w, "method", http.StatusMethodNotAllowed) return } w.WriteHeader(207) _, _ = io.WriteString(w, icsBody) }) fake := httptest.NewServer(mux) defer fake.Close() srv.CalDAV = &web.CalDAVDeps{Client: caldav.New(fake.URL+"/dav/calendars/m/", "u", "p")} // Seed projax item under dev with a caldav-list link to the fake calendar. stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000000"), ".", "") slug := "evt-fix-" + stamp calURL := fake.URL + "/dav/calendars/m/Work-evts/" ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var dev, id string if err := pool.QueryRow(ctx, `select id from projax.items where slug='dev' and cardinality(parent_ids)=0`).Scan(&dev); err != nil { t.Fatalf("dev: %v", err) } if err := pool.QueryRow(ctx, `insert into projax.items (kind, title, slug, parent_ids) values (array['project']::text[], 'evt', $1, ARRAY[$2]::uuid[]) returning id`, slug, dev, ).Scan(&id); err != nil { t.Fatalf("seed item: %v", err) } defer pool.Exec(context.Background(), `delete from projax.items where id=$1`, id) if _, err := pool.Exec(ctx, `insert into projax.item_links (item_id, ref_type, ref_id, rel) values ($1, 'caldav-list', $2, 'tracks')`, id, calURL, ); err != nil { t.Fatalf("seed link: %v", err) } h := srv.Routes() // The card-events markup lives on the Tasks tab (Phase 5h). code, body := get(t, h, "/views/dashboard?view=tasks") if code != 200 { t.Fatalf("GET /views/dashboard?view=tasks → %d", code) } for _, want := range []string{ `card-events`, `Upcoming dinner`, `Munich`, `/i/dev.` + slug, } { if !strings.Contains(body, want) { t.Errorf("dashboard body missing %q", want) } } if !strings.Contains(body, "ganztägig") { t.Errorf("dashboard body should label the DATE-only event as 'ganztägig'") } } // TestDashboardEventsCardCollapsesWhenEmpty: with no items linked to any // CalDAV calendar and no filter active, the Events card collapses to the // one-line muted note. func TestDashboardEventsCardCollapsesWhenEmpty(t *testing.T) { srv, pool := mustServer(t) defer pool.Close() // Wire a CalDAV client that always returns 0 events. mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(207) _, _ = io.WriteString(w, ``) }) fake := httptest.NewServer(mux) defer fake.Close() srv.CalDAV = &web.CalDAVDeps{Client: caldav.New(fake.URL+"/", "u", "p")} h := srv.Routes() _, body := get(t, h, "/views/dashboard?view=tasks") if !strings.Contains(body, "No upcoming events") { t.Errorf("expected collapsed Events card with 'No upcoming events' note") } } // buildFakeEventsReportBody returns a CalDAV REPORT multistatus body with one // DATE-TIME event tomorrow at 18:00Z and one all-day DATE event the day // after. Inputs are real UTC times so the test reads against the actual // dashboard "next 7d" window from now. func buildFakeEventsReportBody(t *testing.T, tomorrow, dayAfter time.Time) string { t.Helper() startDT := tomorrow.UTC().Format("20060102T150405Z") endDT := tomorrow.Add(time.Hour).UTC().Format("20060102T150405Z") allDayDate := dayAfter.UTC().Format("20060102") dayAfterPlus1 := dayAfter.AddDate(0, 0, 1).UTC().Format("20060102") return ` /dav/calendars/m/Work-evts/ev-1.ics "e1" BEGIN:VCALENDAR BEGIN:VEVENT UID:ev-1@fake SUMMARY:Upcoming dinner DTSTART:` + startDT + ` DTEND:` + endDT + ` LOCATION:Munich END:VEVENT END:VCALENDAR HTTP/1.1 200 OK /dav/calendars/m/Work-evts/ev-2.ics "e2" BEGIN:VCALENDAR BEGIN:VEVENT UID:ev-2@fake SUMMARY:All-hands offsite DTSTART;VALUE=DATE:` + allDayDate + ` DTEND;VALUE=DATE:` + dayAfterPlus1 + ` END:VEVENT END:VCALENDAR HTTP/1.1 200 OK ` }