oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3package calendartest
4
5import (
6 "context"
7 "io"
8 "log/slog"
9 "net/http"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "amadan.net/rastrillo/rastrillo"
15 "amadan.net/rastrillo/rastrillo/harness"
16 "github.com/chromedp/cdproto/network"
17 "github.com/chromedp/chromedp"
18
19 calendar "amadan.net/oficina/calendar/internal/calendar"
20)
21
22// TestPlacedChipsSurviveTheProductionCSP: Calendar in Chromium behind the
23// production serving stack (App.ServeOptions through rastrillo.Handler:
24// the security headers and Calendar's CSP), standalone. Two events on
25// one day, at 09:00 for an hour and at 14:00 for two, paint where the
26// packer put them: the later one lower, twice as tall, and a team
27// calendar's chip in a different colour from a personal one.
28//
29// Every one of those is a style attribute. Under rastrillo fb806a2's
30// default style-src the browser drops them all — both chips stack at the
31// top of the column at their content height, and every chip falls back
32// to the stylesheet's default hue — while the other browser checks,
33// which serve App.Handler() with no CSP at all, stay green.
34func TestPlacedChipsSurviveTheProductionCSP(t *testing.T) {
35 var app *calendar.App
36 rig := harness.New(t, func(origin string) http.Handler {
37 a, err := calendar.New(calendar.Config{
38 DBPath: filepath.Join(t.TempDir(), "calendar.db"),
39 Env: "development",
40 Origin: origin,
41 Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
42 })
43 if err != nil {
44 t.Fatalf("calendar.New: %v", err)
45 }
46 t.Cleanup(func() { a.Close() })
47 app = a
48 h, closeFn, err := rastrillo.Handler(a.ServeOptions(rastrillo.Options{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))}))
49 if err != nil {
50 t.Fatalf("rastrillo.Handler: %v", err)
51 }
52 if closeFn != nil {
53 t.Cleanup(func() { _ = closeFn() })
54 }
55 return h
56 })
57 session := signInOverHTTP(t, app, rig.Origin, "host@example.com")
58
59 ctx := context.Background()
60 st := app.StoreForTest()
61 host := app.PersonByAddressForTest(t, "host@example.com")
62 personal, err := st.DefaultCalendarID(ctx, host.ID)
63 if err != nil {
64 t.Fatal(err)
65 }
66 cals, err := st.Calendars(ctx, host.ID)
67 if err != nil {
68 t.Fatal(err)
69 }
70 var team int64
71 for _, c := range cals {
72 if c.Kind == "team" {
73 team = c.ID
74 }
75 }
76 if team == 0 || team == personal {
77 t.Fatalf("no team calendar beside the personal one: %+v", cals)
78 }
79 for _, e := range []calendar.EventInput{
80 {CalendarID: personal, Title: "Morning", StartLocal: "2026-09-10T09:00", EndLocal: "2026-09-10T10:00", TZ: "UTC"},
81 {CalendarID: team, Title: "Afternoon", StartLocal: "2026-09-10T14:00", EndLocal: "2026-09-10T16:00", TZ: "UTC"},
82 } {
83 if _, err := st.CreateEvent(ctx, host.ID, e); err != nil {
84 t.Fatal(err)
85 }
86 }
87
88 run := func(what string, actions ...chromedp.Action) {
89 t.Helper()
90 c, cancel := context.WithTimeout(rig.Context(), 20*time.Second)
91 defer cancel()
92 if err := chromedp.Run(c, actions...); err != nil {
93 t.Fatalf("%s: %v", what, err)
94 }
95 }
96 run("sign in", network.SetCookie(session.Name, session.Value).WithURL(rig.Origin+"/").
97 WithHTTPOnly(session.HttpOnly).WithSameSite(network.CookieSameSiteLax))
98
99 var day struct {
100 MorningTop, MorningHeight, AfternoonTop, AfternoonHeight float64
101 MorningBorder, AfternoonBorder string
102 }
103 run("day view",
104 chromedp.Navigate(rig.Origin+"/day/2026-09-10"),
105 chromedp.WaitVisible(`[cal-placed]`, chromedp.ByQuery),
106 chromedp.Evaluate(`(() => {
107 const placed = (title) => [...document.querySelectorAll('[cal-placed]')].find(p => p.textContent.includes(title));
108 const box = (title) => placed(title).getBoundingClientRect();
109 const chip = (title) => getComputedStyle(placed(title).querySelector('[cal-chip]'));
110 return {
111 MorningTop: box('Morning').top, MorningHeight: box('Morning').height,
112 AfternoonTop: box('Afternoon').top, AfternoonHeight: box('Afternoon').height,
113 MorningBorder: chip('Morning').borderInlineStartColor,
114 AfternoonBorder: chip('Afternoon').borderInlineStartColor,
115 };
116 })()`, &day),
117 )
118 if day.AfternoonTop-day.MorningTop < 100 {
119 t.Errorf("the 14:00 chip sits %.0fpx below the 09:00 one: its position was refused", day.AfternoonTop-day.MorningTop)
120 }
121 if ratio := day.AfternoonHeight / day.MorningHeight; ratio < 1.8 || ratio > 2.2 {
122 t.Errorf("a two-hour chip is %.0fpx and a one-hour chip %.0fpx: their heights were refused", day.AfternoonHeight, day.MorningHeight)
123 }
124 if day.MorningBorder == day.AfternoonBorder {
125 t.Errorf("the personal and team chips paint the same hue %s: --cal-hue was refused", day.MorningBorder)
126 }
127}
128