package slidestest import ( "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "amadan.net/rastrillo/rastrillo" slides "amadan.net/oficina/slides/internal/slides" ) // productionHandler is a behind rastrillo's serving stack, built from the // same ServeOptions main hands to rastrillo.Serve: security headers, CSP, // locales and all. a.Handler alone carries no CSP, so a test that serves // only the mux cannot see what a browser is told. func productionHandler(t *testing.T, a *testApp) http.Handler { t.Helper() opts := a.app.ServeOptions(rastrillo.Options{Logger: slog.New(slog.NewTextHandler(io.Discard, nil))}, a.mux) h, closeFn, err := rastrillo.Handler(opts) if err != nil { t.Fatalf("rastrillo.Handler: %v", err) } t.Cleanup(func() { _ = closeFn() }) return h } // directives splits a policy into directive name → source list. func directives(policy string) map[string]string { out := map[string]string{} for _, d := range strings.Split(policy, ";") { name, value, _ := strings.Cut(strings.TrimSpace(d), " ") if name != "" { out[name] = strings.TrimSpace(value) } } return out } // TestProductionCSPIsTheDefaultBarStyles: Slides' policy is rastrillo's // default with style-src alone widened to 'unsafe-inline' // (slides.ContentSecurityPolicy says why). Every other directive must // match the framework's, read off a handler built with no CSP of its // own, so a rastrillo bump that tightens anything else fails here rather // than being silently undone by our constant. func TestProductionCSPIsTheDefaultBarStyles(t *testing.T) { bare, closeFn, err := rastrillo.Handler(rastrillo.Options{Mux: http.NewServeMux(), Logger: slog.New(slog.NewTextHandler(io.Discard, nil))}) if err != nil { t.Fatal(err) } defer closeFn() rec := httptest.NewRecorder() bare.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil)) def := directives(rec.Header().Get("Content-Security-Policy")) ours := directives(slides.ContentSecurityPolicy) if len(def) == 0 { t.Fatal("rastrillo set no default CSP") } for name, value := range def { if name == "style-src" { continue } if ours[name] != value { t.Errorf("%s: ours %q, rastrillo's default %q", name, ours[name], value) } } for name := range ours { if _, ok := def[name]; !ok { t.Errorf("ours adds %s, which rastrillo's default does not have", name) } } // A hash or nonce beside 'unsafe-inline' makes the browser ignore // 'unsafe-inline' (CSP3), which is exactly how the slides would break. if got := ours["style-src"]; got != "'self' 'unsafe-inline'" { t.Errorf("style-src = %q, want 'self' 'unsafe-inline' and nothing else", got) } } // TestStandaloneServesInlineStyles: through the production stack, the // viewer — whose slide carries its theme as custom properties on a style // attribute, and whose own layout is a