oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs_test
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "regexp"
7 "strings"
8 "testing"
9
10 "amadan.net/rastrillo/rastrillo"
11 "github.com/carlosframework/docs/internal/docs"
12)
13
14var inlineStyle = regexp.MustCompile(`<style[\s>]|\sstyle="`)
15
16// Docs' pages are drawn with <style> blocks and a style attribute.
17// rastrillo's default CSP stopped allowing either on 2026-09-13, and a
18// blocked style fails silently: the page renders unstyled and nothing
19// errors. Unit tests call app.Handler() and never see the policy, so
20// this serves real pages through rastrillo's production middleware,
21// built by the same ServeOptions cmd/docs uses, and checks that every
22// page with an inline style is served a policy that runs it.
23func TestServedPolicyAllowsThePagesInlineStyles(t *testing.T) {
24 app := members(t, "alice@example.com,bob@example.com")
25 served, done, err := rastrillo.Handler(docs.ServeOptions(rastrillo.Options{}, app))
26 if err != nil {
27 t.Fatal(err)
28 }
29 if done != nil {
30 defer done()
31 }
32 alice := signInCookies(t, app, "alice@example.com")
33 sqid := createDoc(t, app, alice, "Minutes")
34 link, err := app.TestSignIn(t, "bob@example.com")
35 if err != nil {
36 t.Fatal(err)
37 }
38
39 pages := []struct {
40 path string
41 cookies []*http.Cookie
42 }{
43 {"/signin", nil},
44 {link, nil}, // the confirm door
45 {"/signed-out", nil},
46 {"/", alice},
47 {"/d/" + sqid, alice},
48 {"/d/" + sqid + "/share", alice},
49 }
50 styled := 0
51 for _, p := range pages {
52 req := httptest.NewRequest(http.MethodGet, p.path, nil)
53 req.Header.Set("Accept", "text/html")
54 for _, c := range p.cookies {
55 req.AddCookie(c)
56 }
57 rec := httptest.NewRecorder()
58 served.ServeHTTP(rec, req)
59 if rec.Code != http.StatusOK {
60 t.Errorf("GET %s through the served handler: %d, want 200", p.path, rec.Code)
61 continue
62 }
63 if !inlineStyle.MatchString(rec.Body.String()) {
64 continue
65 }
66 styled++
67 for _, policy := range rec.Header().Values("Content-Security-Policy") {
68 if !styleSrcAllowsInline(policy) {
69 t.Errorf("GET %s has inline styles but is served %q, which blocks them: the page renders unstyled", p.path, policy)
70 }
71 }
72 }
73 // Without a styled page this test would pass on any policy at all.
74 if styled == 0 {
75 t.Fatal("no page had an inline style; this test checks nothing — delete it, and ContentSecurityPolicy's 'unsafe-inline' with it")
76 }
77}
78
79// styleSrcAllowsInline reports whether policy lets a <style> block and a
80// style attribute apply. With no style-src the browser uses default-src.
81func styleSrcAllowsInline(policy string) bool {
82 fallback := ""
83 for _, d := range strings.Split(policy, ";") {
84 f := strings.Fields(d)
85 if len(f) == 0 {
86 continue
87 }
88 switch f[0] {
89 case "style-src":
90 return contains(f[1:], "'unsafe-inline'")
91 case "default-src":
92 fallback = strings.Join(f[1:], " ")
93 }
94 }
95 return contains(strings.Fields(fallback), "'unsafe-inline'")
96}
97
98func contains(list []string, want string) bool {
99 for _, s := range list {
100 if s == want {
101 return true
102 }
103 }
104 return false
105}
106