diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 21:41:38 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 21:41:38 +0200 |
| commit | b423759bf017d8fa1e45e5d1ce978f1b8d0a6a9f (patch) | |
| tree | fd0fc02516ca8ac4d201082d7cad146d81f888c8 /internal/web/apifeed_test.go | |
| parent | f8c92f253693e269edbc76da9a99e65df7d6f699 (diff) | |
| download | lectio-b423759bf017d8fa1e45e5d1ce978f1b8d0a6a9f.tar.gz lectio-b423759bf017d8fa1e45e5d1ce978f1b8d0a6a9f.zip | |
feat(web): /api/calendar.json + /calendar.ics endpoints (capped, nosniff, validated)
Diffstat (limited to 'internal/web/apifeed_test.go')
| -rw-r--r-- | internal/web/apifeed_test.go | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/internal/web/apifeed_test.go b/internal/web/apifeed_test.go new file mode 100644 index 0000000..97b657e --- /dev/null +++ b/internal/web/apifeed_test.go @@ -0,0 +1,110 @@ +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) + } +} |
