oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "bytes"
5 "encoding/xml"
6 "io"
7 "io/fs"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12)
13
14// The bundle is committed, not built at boot. This asserts exactly one
15// is present and that the path the template emits resolves to it — a
16// mismatch would serve a 404 and leave the editor silently absent,
17// which looks identical to "editing is not implemented yet".
18func TestExactlyOneBundlePerKindIsEmbedded(t *testing.T) {
19 entries, err := fs.ReadDir(staticFS, "static")
20 if err != nil {
21 t.Fatalf("read static: %v", err)
22 }
23
24 // One committed file per bundle kind. This was "exactly one editor
25 // bundle" until the jump-to palette became a second one: it ships
26 // separately so the document list does not download 300KB of
27 // ProseMirror in order to offer a keyboard shortcut (assets.go's
28 // bundleNames). The invariant has not weakened — it is still
29 // exactly one file per name, and bundleKinds is a written list
30 // rather than whatever happens to be on disk, so a build that
31 // silently stopped emitting one panics at init instead of starting
32 // without it.
33 paths := map[string]string{
34 "editor": editorScriptPath(),
35 "jumpto": jumpToScriptPath(),
36 "share": shareScriptPath(),
37 }
38 accounted := map[string]bool{}
39 for _, kind := range bundleKinds {
40 var found []string
41 for _, e := range entries {
42 if strings.HasPrefix(e.Name(), kind+".") && strings.HasSuffix(e.Name(), ".js") {
43 found = append(found, e.Name())
44 }
45 }
46 if len(found) != 1 {
47 t.Fatalf("want exactly one %q bundle, got %v", kind, found)
48 }
49 accounted[found[0]] = true
50 want, ok := paths[kind]
51 if !ok {
52 t.Fatalf("bundleKinds names %q but this test knows no path accessor for it", kind)
53 }
54 if want != "/static/"+found[0] {
55 t.Errorf("%s path = %q, want %q", kind, want, "/static/"+found[0])
56 }
57 }
58
59 // And nothing else in static/ at all. That directory is the bundle
60 // build's output: .amadan/ci.d/10-gate rebuilds into it and fails
61 // on any dirt whatsoever there, then restores it with git checkout
62 // and git clean — so an extra file both reddens the gate and is
63 // deleted by it. The tab icon was written here first and lost
64 // exactly that way, which is why this is a test and not a note.
65 // Hand-authored assets go in brand/ (brandFS).
66 //
67 // Kept as "everything present is accounted for" rather than as a
68 // count, so it still catches a stray file now that the expected
69 // number is no longer one. assets.go's own registry cannot do this
70 // job: it looks up the names it knows and would never notice a
71 // third file nobody named.
72 for _, e := range entries {
73 if !accounted[e.Name()] {
74 t.Errorf("static/ holds %q, which no bundle kind accounts for — it must hold only the bundle build's output; put hand-authored assets in brand/", e.Name())
75 }
76 }
77}
78
79// The bundle must be served without a session, or a signed-out reader
80// gets a broken page: a script tag pointing at a 302-to-signin looks
81// exactly like an unimplemented editor. Built directly with
82// httptest.NewRequest/app.Handler().ServeHTTP rather than
83// app.TestRequest, which always signs someone in — there is
84// deliberately no signed-in identity anywhere in this request.
85func TestEditorBundleIsServed(t *testing.T) {
86 app, err := NewForTest(t)
87 if err != nil {
88 t.Fatalf("NewForTest: %v", err)
89 }
90 req := httptest.NewRequest(http.MethodGet, editorScriptPath(), nil)
91 rec := httptest.NewRecorder()
92 app.Handler().ServeHTTP(rec, req)
93
94 if rec.Code != http.StatusOK {
95 t.Fatalf("GET %s = %d, want 200", editorScriptPath(), rec.Code)
96 }
97 if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "javascript") {
98 t.Errorf("Content-Type = %q, want javascript", ct)
99 }
100 if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "immutable") {
101 t.Errorf("Cache-Control = %q, want immutable (the filename carries the content hash)", cc)
102 }
103}
104
105// The tab icon must be served, unauthenticated, with an image content
106// type — and the markup that links it must point at the constant this
107// package actually routes on. Those two drifting apart is the exact
108// failure this pins: a wrong href does not break a page, it quietly
109// restores the blank-page tab mark the icon exists to replace, which
110// nobody files a bug about and no other test here would notice.
111func TestIconIsServedAndLinkedConsistently(t *testing.T) {
112 app, err := NewForTest(t)
113 if err != nil {
114 t.Fatalf("NewForTest: %v", err)
115 }
116 // No session on this request, on purpose: the sign-in page needs
117 // the icon as much as any signed-in screen does, and it is
118 // reached without one.
119 req := httptest.NewRequest(http.MethodGet, iconPath, nil)
120 rec := httptest.NewRecorder()
121 app.Handler().ServeHTTP(rec, req)
122
123 if rec.Code != http.StatusOK {
124 t.Fatalf("GET %s = %d, want 200", iconPath, rec.Code)
125 }
126 if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "image/svg+xml") {
127 t.Errorf("Content-Type = %q, want image/svg+xml", ct)
128 }
129 if body := rec.Body.String(); !strings.Contains(body, "<svg") {
130 t.Errorf("body does not look like an SVG: %.80q", body)
131 }
132
133 // And the link must reach a real page. Asserted on rendered output
134 // rather than on views/head.html's bytes: the file containing the
135 // href proves nothing on its own, because "head" only reaches a
136 // page if newRenderer parses it into the base template ahead of the
137 // per-page clones (render.go). Were that wiring dropped, a
138 // bytes-of-the-file check would stay green while every page lost
139 // its icon — the icon would still serve, the template would still
140 // contain the href, and nothing would be able to tell. Rendering
141 // the page subsumes the file check and closes that gap.
142 //
143 // The sign-in page, because it needs no session and so exercises
144 // the same unauthenticated path the icon itself is served on.
145 pageReq := httptest.NewRequest(http.MethodGet, "/signin", nil)
146 pageRec := httptest.NewRecorder()
147 app.Handler().ServeHTTP(pageRec, pageReq)
148 if pageRec.Code != http.StatusOK {
149 t.Fatalf("GET /signin = %d, want 200", pageRec.Code)
150 }
151 if wantHref := `href="` + iconPath + `"`; !strings.Contains(pageRec.Body.String(), wantHref) {
152 t.Errorf("the rendered sign-in page does not contain %s — the shell's head block is not carrying the icon link", wantHref)
153 }
154}
155
156// The icon must be well-formed XML, because SVG is XML and a browser
157// will not render one that is not — it fails closed, showing the same
158// blank tab mark as no icon at all.
159//
160// This exists because the first version of icon.svg was not: its
161// comment named a CSS custom property with the two leading hyphens a
162// custom property has, and two consecutive hyphens are illegal inside
163// an XML comment. Everything a request-level check can see was
164// correct — 200, image/svg+xml, a plausible Content-Length, the
165// expected bytes — and the file was still unparseable. Only rendering
166// it showed anything wrong, which is exactly the failure shape
167// AGENTS.md warns about: a verification that cannot tell "no" from
168// "I can't see it".
169//
170// Decoding every token, rather than checking the root element, is the
171// point: a malformed comment or attribute halfway down is precisely
172// what a shallower check misses.
173func TestIconIsWellFormedXML(t *testing.T) {
174 b, err := fs.ReadFile(brandFS, "brand/icon.svg")
175 if err != nil {
176 t.Fatalf("read icon: %v", err)
177 }
178 dec := xml.NewDecoder(bytes.NewReader(b))
179 var root string
180 for {
181 tok, err := dec.Token()
182 if err == io.EOF {
183 break
184 }
185 if err != nil {
186 t.Fatalf("icon.svg is not well-formed XML, so no browser will render it: %v", err)
187 }
188 if se, ok := tok.(xml.StartElement); ok && root == "" {
189 root = se.Name.Local
190 }
191 }
192 if root != "svg" {
193 t.Errorf("icon.svg root element = %q, want svg", root)
194 }
195}
196