package web import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/lukaszkasprzak/lectio/internal/config" ) // TestAPICalendarJSON exercises GET /api/calendar.json's success path: a // single ?date= resolves to exactly one day, with the correct Content-Type, // nosniff, and a body that parses as the lectio.calendar/1 envelope. func TestAPICalendarJSON(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/api/calendar.json?date=2026-01-06", nil)) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } if ct := rec.Header().Get("Content-Type"); ct != "application/json; charset=utf-8" { t.Errorf("Content-Type = %q", ct) } if ns := rec.Header().Get("X-Content-Type-Options"); ns != "nosniff" { t.Errorf("X-Content-Type-Options = %q, want nosniff", ns) } var out struct { Schema string `json:"schema"` Form string `json:"form"` Days []struct { Date string `json:"date"` } `json:"days"` } if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { t.Fatalf("body did not parse as JSON: %v; body=%s", err, rec.Body.String()) } if len(out.Days) != 1 { t.Fatalf("got %d days, want 1", len(out.Days)) } } // TestCalendarICS exercises GET /calendar.ics's success path: a whole ?year= // resolves to a VCALENDAR body, with the correct Content-Type and nosniff. func TestCalendarICS(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/calendar.ics?year=2026", nil)) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } if ct := rec.Header().Get("Content-Type"); ct != "text/calendar; charset=utf-8" { t.Errorf("Content-Type = %q", ct) } if ns := rec.Header().Get("X-Content-Type-Options"); ns != "nosniff" { t.Errorf("X-Content-Type-Options = %q, want nosniff", ns) } if !strings.Contains(rec.Body.String(), "BEGIN:VCALENDAR") { t.Errorf("body missing BEGIN:VCALENDAR") } } // TestAPICalendarJSONBadDate: an unparsable date is rejected with 400 (D3). func TestAPICalendarJSONBadDate(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/api/calendar.json?date=not-a-date", nil)) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } } // TestAPICalendarJSONBadForm: an out-of-enum form is rejected with 400, not // silently defaulted (D3). func TestAPICalendarJSONBadForm(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/api/calendar.json?from=2026-01-01&to=2026-01-02&form=bogus", nil)) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } } // TestCalendarICSOverCap: a range exceeding the 1830-day web cap (D1) is // rejected with 400 BEFORE any day is built -- the body must not contain a // VEVENT, proving the request never reached compute. func TestCalendarICSOverCap(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/calendar.ics?from=2000-01-01&to=2100-01-01", nil)) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } if strings.Contains(rec.Body.String(), "BEGIN:VEVENT") { t.Errorf("over-cap request produced a VEVENT: %s", rec.Body.String()) } } // TestAPICalendarJSONInverted: from after to is rejected with 400. func TestAPICalendarJSONInverted(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/api/calendar.json?from=2026-02-01&to=2026-01-01", nil)) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } }