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
`
}