//go:build browser package calendartest import ( "context" "io" "log/slog" "net/http" "path/filepath" "testing" "time" "amadan.net/rastrillo/rastrillo" "amadan.net/rastrillo/rastrillo/harness" "github.com/chromedp/cdproto/network" "github.com/chromedp/chromedp" calendar "amadan.net/oficina/calendar/internal/calendar" ) // TestPlacedChipsSurviveTheProductionCSP: Calendar in Chromium behind the // production serving stack (App.ServeOptions through rastrillo.Handler: // the security headers and Calendar's CSP), standalone. Two events on // one day, at 09:00 for an hour and at 14:00 for two, paint where the // packer put them: the later one lower, twice as tall, and a team // calendar's chip in a different colour from a personal one. // // Every one of those is a style attribute. Under rastrillo fb806a2's // default style-src the browser drops them all — both chips stack at the // top of the column at their content height, and every chip falls back // to the stylesheet's default hue — while the other browser checks, // which serve App.Handler() with no CSP at all, stay green. func TestPlacedChipsSurviveTheProductionCSP(t *testing.T) { var app *calendar.App rig := harness.New(t, func(origin string) http.Handler { a, err := calendar.New(calendar.Config{ DBPath: filepath.Join(t.TempDir(), "calendar.db"), Env: "development", Origin: origin, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatalf("calendar.New: %v", err) } t.Cleanup(func() { a.Close() }) app = a h, closeFn, err := rastrillo.Handler(a.ServeOptions(rastrillo.Options{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})) if err != nil { t.Fatalf("rastrillo.Handler: %v", err) } if closeFn != nil { t.Cleanup(func() { _ = closeFn() }) } return h }) session := signInOverHTTP(t, app, rig.Origin, "host@example.com") ctx := context.Background() st := app.StoreForTest() host := app.PersonByAddressForTest(t, "host@example.com") personal, err := st.DefaultCalendarID(ctx, host.ID) if err != nil { t.Fatal(err) } cals, err := st.Calendars(ctx, host.ID) if err != nil { t.Fatal(err) } var team int64 for _, c := range cals { if c.Kind == "team" { team = c.ID } } if team == 0 || team == personal { t.Fatalf("no team calendar beside the personal one: %+v", cals) } for _, e := range []calendar.EventInput{ {CalendarID: personal, Title: "Morning", StartLocal: "2026-09-10T09:00", EndLocal: "2026-09-10T10:00", TZ: "UTC"}, {CalendarID: team, Title: "Afternoon", StartLocal: "2026-09-10T14:00", EndLocal: "2026-09-10T16:00", TZ: "UTC"}, } { if _, err := st.CreateEvent(ctx, host.ID, e); err != nil { t.Fatal(err) } } run := func(what string, actions ...chromedp.Action) { t.Helper() c, cancel := context.WithTimeout(rig.Context(), 20*time.Second) defer cancel() if err := chromedp.Run(c, actions...); err != nil { t.Fatalf("%s: %v", what, err) } } run("sign in", network.SetCookie(session.Name, session.Value).WithURL(rig.Origin+"/"). WithHTTPOnly(session.HttpOnly).WithSameSite(network.CookieSameSiteLax)) var day struct { MorningTop, MorningHeight, AfternoonTop, AfternoonHeight float64 MorningBorder, AfternoonBorder string } run("day view", chromedp.Navigate(rig.Origin+"/day/2026-09-10"), chromedp.WaitVisible(`[cal-placed]`, chromedp.ByQuery), chromedp.Evaluate(`(() => { const placed = (title) => [...document.querySelectorAll('[cal-placed]')].find(p => p.textContent.includes(title)); const box = (title) => placed(title).getBoundingClientRect(); const chip = (title) => getComputedStyle(placed(title).querySelector('[cal-chip]')); return { MorningTop: box('Morning').top, MorningHeight: box('Morning').height, AfternoonTop: box('Afternoon').top, AfternoonHeight: box('Afternoon').height, MorningBorder: chip('Morning').borderInlineStartColor, AfternoonBorder: chip('Afternoon').borderInlineStartColor, }; })()`, &day), ) if day.AfternoonTop-day.MorningTop < 100 { t.Errorf("the 14:00 chip sits %.0fpx below the 09:00 one: its position was refused", day.AfternoonTop-day.MorningTop) } if ratio := day.AfternoonHeight / day.MorningHeight; ratio < 1.8 || ratio > 2.2 { t.Errorf("a two-hour chip is %.0fpx and a one-hour chip %.0fpx: their heights were refused", day.AfternoonHeight, day.MorningHeight) } if day.MorningBorder == day.AfternoonBorder { t.Errorf("the personal and team chips paint the same hue %s: --cal-hue was refused", day.MorningBorder) } }