oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1package memoriatest
2
3import (
4 "net/http"
5 "net/http/cookiejar"
6 "net/http/httptest"
7 "net/url"
8 "regexp"
9 "strings"
10 "testing"
11
12 "golang.org/x/net/html"
13)
14
15// browser carries cookies from one request to the next the way a browser
16// does, deletions included, so a test can visit pages in an order and see
17// what each visit spent.
18type browser struct {
19 t *testing.T
20 h http.Handler
21 jar *cookiejar.Jar
22}
23
24func newBrowser(t *testing.T, h http.Handler) *browser {
25 jar, err := cookiejar.New(nil)
26 if err != nil {
27 t.Fatal(err)
28 }
29 return &browser{t: t, h: h, jar: jar}
30}
31
32var browserBase = &url.URL{Scheme: "http", Host: "example.com", Path: "/"}
33
34func (b *browser) keep(rec *httptest.ResponseRecorder) *httptest.ResponseRecorder {
35 b.jar.SetCookies(browserBase, rec.Result().Cookies())
36 return rec
37}
38
39// visit GETs path with the jar's cookies. A non-empty header marks it as
40// drop.js's background re-fetch.
41func (b *browser) visit(path, header string) string {
42 b.t.Helper()
43 req := httptest.NewRequest(http.MethodGet, path, nil)
44 for _, c := range b.jar.Cookies(browserBase) {
45 req.AddCookie(c)
46 }
47 if header != "" {
48 req.Header.Set(header, "1")
49 }
50 rec := httptest.NewRecorder()
51 b.h.ServeHTTP(rec, req)
52 if rec.Code != http.StatusOK {
53 b.t.Fatalf("GET %s answered %d, want 200", path, rec.Code)
54 }
55 return b.keep(rec).Body.String()
56}
57
58// An upload's notice is spent on /add and nowhere else.
59//
60// The sentence naming a refused file travels in a cookie, because a
61// filename must not go in a URL. With one cookie shared by every page, a
62// visit to the index spent it, and so did /add's own background refresh.
63// Either way /add was left with the bare counts in its URL, and the
64// refresh case showed the sentence to nobody.
65//
66// MUTATION-TESTED 2026-09-29, each reverted:
67//
68// (a) index() takes addNotice instead of indexNotice → "a visit to the
69// index showed the upload's notice, which belongs on /add"
70// (b) the refresh-header check deleted from notice.take → "drop.js's
71// background refresh showed the upload's notice, which belongs on
72// /add"
73// (c) the drop zone's data-refresh-header hard-coded to a name notice.go
74// does not check → the same line as (b). The refresh is sent with the
75// header the PAGE names, which is what drop.js sends.
76func TestAnUploadNoticeWaitsForTheAddPage(t *testing.T) {
77 for _, first := range []struct {
78 what string
79 path string
80 refresh bool
81 }{
82 {"a visit to the index", "/", false},
83 {"drop.js's background refresh", "/add", true},
84 } {
85 h := newApp(t)
86 b := newBrowser(t, h)
87 rec := b.keep(submit(t, h, "", "",
88 file{"notes.txt", "Revenue was 100 across the first region we serve."},
89 file{"scan.png", "\x89PNG\x00\x00\x00 binary payload that is not text"}))
90 loc := rec.Header().Get("Location")
91 if !strings.HasPrefix(loc, "/add?") {
92 t.Fatalf("the submission redirected to %q, want /add", loc)
93 }
94
95 header := ""
96 if first.refresh {
97 header = refreshHeaderOn(t, get(t, h, "/add").Body.String())
98 }
99 if page := b.visit(first.path, header); strings.Contains(page, "scan.png") {
100 t.Errorf("%s showed the upload's notice, which belongs on /add", first.what)
101 }
102 if said := flashText(t, b.visit(loc, "")); !strings.Contains(said, "scan.png") {
103 t.Errorf("after %s, /add no longer names the refused file: %q", first.what, said)
104 }
105 if page := b.visit("/add", ""); strings.Contains(page, "scan.png") {
106 t.Error("the notice was shown twice; it is one-shot")
107 }
108 }
109}
110
111// refreshHeaderOn is the header name /add tells drop.js to send with its
112// re-fetch. Tests send what the page names, never a name of their own, so
113// a page and a server that disagree are caught here.
114func refreshHeaderOn(t *testing.T, addPage string) string {
115 t.Helper()
116 zones := findAll(parsePage(t, addPage), hasAttr("rst-dropzone"))
117 if len(zones) != 1 {
118 t.Fatalf("%d drop zones on /add, want 1", len(zones))
119 }
120 name, _ := attrOf(zones[0], "data-refresh-header")
121 if name == "" {
122 t.Fatal("the drop zone names no refresh header, so drop.js stops before it starts")
123 }
124 return name
125}
126
127// jsFunction returns the body of the named function declaration in src,
128// braces matched. drop.js writes no braces inside strings in the functions
129// this is used on; a body that does not close is a fatal, not a pass.
130func jsFunction(t *testing.T, src, name string) string {
131 t.Helper()
132 at := strings.Index(src, "function "+name+"(")
133 if at < 0 {
134 t.Fatalf("drop.js has no function %s", name)
135 }
136 open := strings.Index(src[at:], "{")
137 if open < 0 {
138 t.Fatalf("function %s has no body", name)
139 }
140 depth := 0
141 for i := at + open; i < len(src); i++ {
142 switch src[i] {
143 case '{':
144 depth++
145 case '}':
146 depth--
147 if depth == 0 {
148 return src[at+open : i+1]
149 }
150 }
151 }
152 t.Fatalf("function %s never closes", name)
153 return ""
154}
155
156// assignments counts every assignment to name, compound ones included —
157// the same rule drop_test.go applies to `endpoint`.
158func assignments(src, name string) int {
159 return len(regexp.MustCompile(`\b`+name+`\s*(?:>>>|\*\*|<<|>>|\|\||&&|\?\?|[-+*/%&|^])?=[^=]`).FindAllString(src, -1))
160}
161
162// drop.js sends the header the page names, and no other.
163//
164// Go cannot run the script, so this reads it, and what it can hold is
165// structure: there is no header name in drop.js to drift, the one
166// variable holding the page's name is assigned once — at the read — and
167// the re-fetch in refresh() is the one place it is sent. The behaviour
168// (the header a real re-fetch carries, and the notice surviving it) was
169// watched in Chromium by Astra on 2026-09-29; TestAnUploadNoticeWaitsForTheAddPage
170// pins that the server honours the name the page publishes.
171//
172// MUTATION-TESTED 2026-09-29, each reverted:
173//
174// (a) setRequestHeader(refreshHeader, "1") → setRequestHeader(
175// "X-Memoria-Refresh-Count", "1"), Astra's divergence → "drop.js
176// does not send the header it read off the page"
177// (b) the attribute read changed to data-refresh → "drop.js does not
178// read its refresh header off the drop zone"
179// (c) Astra's: `refreshHeader += "-Count";` after the read → "drop.js
180// assigns refreshHeader 2 times"
181// (d) the setRequestHeader line deleted from refresh() → "drop.js sends
182// refreshHeader in 0 places, want 1"
183// (e) Astra's: that line moved above req.open in refresh(). Chromium
184// throws InvalidStateError and the footer is never refreshed →
185// "req.setRequestHeader comes before req.open"
186func TestTheDropScriptSendsTheHeaderThePageNames(t *testing.T) {
187 h := newApp(t)
188 rec := get(t, h, "/static/drop.js")
189 if rec.Code != http.StatusOK {
190 t.Fatalf("GET /static/drop.js: status %d, want 200", rec.Code)
191 }
192 js := rec.Body.String()
193 if !strings.Contains(js, `var refreshHeader = zone.getAttribute("data-refresh-header");`) {
194 t.Error("drop.js does not read its refresh header off the drop zone")
195 }
196 if n := assignments(js, "refreshHeader"); n != 1 {
197 t.Errorf("drop.js assigns refreshHeader %d times; once, at the read, or what it sends "+
198 "is not what the page named", n)
199 }
200 if n := strings.Count(js, "setRequestHeader(refreshHeader"); n != 1 {
201 t.Errorf("drop.js sends refreshHeader in %d places, want 1", n)
202 }
203 if !strings.Contains(js, `req.setRequestHeader(refreshHeader, "1");`) {
204 t.Error("drop.js does not send the header it read off the page")
205 }
206 if !strings.Contains(jsFunction(t, js, "refresh"), `req.setRequestHeader(refreshHeader, "1");`) {
207 t.Error("refresh() does not send the header; the re-fetch is the request that needs it")
208 }
209 if strings.Contains(strings.ToLower(js), "x-memoria") {
210 t.Error("drop.js spells a header name of its own; the page is where it reads one")
211 }
212
213 // A header set before open() throws, and after send() is too late.
214 // Every request in the file, not just this one: the upload's Accept
215 // header is what gets it a judgement instead of a redirect.
216 setters := regexp.MustCompile(`(\w+)\.setRequestHeader\(`).FindAllStringSubmatchIndex(js, -1)
217 if len(setters) == 0 {
218 t.Fatal("drop.js sets no request headers, so the order check below proves nothing")
219 }
220 for _, m := range setters {
221 v, at := js[m[2]:m[3]], m[0]
222 open, send := strings.Index(js, v+".open("), strings.Index(js, v+".send(")
223 if open < 0 || send < 0 {
224 t.Errorf("%s.setRequestHeader has no %s.open and %s.send around it", v, v, v)
225 continue
226 }
227 if at < open {
228 t.Errorf("%s.setRequestHeader comes before %s.open; browsers throw InvalidStateError", v, v)
229 }
230 if at > send {
231 t.Errorf("%s.setRequestHeader comes after %s.send; the request has already gone", v, v)
232 }
233 }
234}
235
236// The tray points at the document list, which is not on /add, and keeps
237// pointing there.
238//
239// Astra, 2026-09-29: the link went back to /add, which has no list, and
240// changing it back passed make ci. Staying visible after the refresh is
241// the script's, and Go cannot run it, so the script is read: `listed` is
242// touched in exactly four places — found, checked, shown, hidden — and the
243// hiding is in hideTray(), which is dismissal. Watched in Chromium by
244// Astra on 2026-09-29: the link stayed after a real refresh.
245//
246// MUTATION-TESTED 2026-09-29, each reverted:
247//
248// (a) href="/" → href="/add" → "the tray's link goes to "/add""
249// (b) `listed.hidden = true;` added to put() → "drop.js mentions
250// `listed` 5 times"
251// (c) Astra's: that line MOVED from hideTray() into put() → "hideTray()
252// does not hide the pointer" and "put() touches the pointer"
253// (d) Astra's: the condition cut to `if (batch.stored > 0)`, so a batch
254// with only an unconfirmed file hides the link to the one place its
255// fate can be checked → "the pointer is not shown for an unconfirmed
256// upload"
257func TestTheTrayPointsAtTheDocumentList(t *testing.T) {
258 h := newApp(t)
259 doc := parsePage(t, get(t, h, "/add").Body.String())
260 listed := findAll(doc, hasAttr("rst-tray-listed"))
261 if len(listed) != 1 {
262 t.Fatalf("%d tray pointers on /add, want 1", len(listed))
263 }
264 links := findAll(listed[0], func(n *html.Node) bool { return n.Data == "a" })
265 if len(links) != 1 {
266 t.Fatalf("the tray pointer holds %d links, want 1", len(links))
267 }
268 if href, _ := attrOf(links[0], "href"); href != "/" {
269 t.Errorf("the tray's link goes to %q; the document list is on \"/\"", href)
270 }
271
272 js := get(t, h, "/static/drop.js").Body.String()
273 if n := len(regexp.MustCompile(`(^|[^-\w])listed\b`).FindAllString(js, -1)); n != 4 {
274 t.Errorf("drop.js mentions `listed` %d times, want 4: found, checked, shown on "+
275 "announce, hidden on dismiss. Anything else can hide it after a refresh.", n)
276 }
277 if !strings.Contains(jsFunction(t, js, "hideTray"), "listed.hidden = true;") {
278 t.Error("hideTray() does not hide the pointer")
279 }
280 if strings.Contains(jsFunction(t, js, "put"), "listed") {
281 t.Error("put() touches the pointer; it is true whether or not the refresh arrives")
282 }
283 if strings.Count(js, "listed.hidden = false;") != 1 {
284 t.Error("drop.js does not show the pointer in exactly one place")
285 }
286 // Shown when a file was stored OR its fate is unknown. An unconfirmed
287 // file is the case that most needs the list: it is the only place to
288 // find out whether the file landed.
289 shown := regexp.MustCompile(`if \(([^)]*)\) \{\s*listed\.hidden = false;`).FindStringSubmatch(js)
290 if shown == nil {
291 t.Fatal("the pointer is not shown under an if, so its condition cannot be checked")
292 }
293 for _, want := range []string{"batch.stored > 0", "batch.unsure.length > 0"} {
294 if !strings.Contains(shown[1], want) {
295 t.Errorf("the pointer is not shown for %s: its condition is %q",
296 map[string]string{"batch.stored > 0": "a stored upload",
297 "batch.unsure.length > 0": "an unconfirmed upload"}[want], shown[1])
298 }
299 }
300 if !strings.Contains(shown[1], "||") || strings.Contains(shown[1], "&&") {
301 t.Errorf("the pointer's condition is %q; either case alone must show it", shown[1])
302 }
303}
304