package memoriatest import ( "bytes" "fmt" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "regexp" "slices" "strconv" "strings" "testing" ) // The tests in this file cover drag-and-drop upload. Read this before // adding to them, because half of the feature is JavaScript and Go // cannot run it: // // What these tests DO prove is the half that decides whether the feature // is safe — that the scriptless form is untouched, that the script is // actually served and has no upload path of its own, and that the limits // and the words the browser refuses with are the handler's own, checked // by posting files either side of each advertised boundary. // // What they CANNOT prove is that the drop handler does what its copy // says. That was verified in a real browser instead, and here is the // recipe, because a verification nobody can repeat is a claim: // // go run ./tools/drive # serves on :8824, no sign-in // chromium --headless --remote-debugging-port=9222 about:blank // // (drive puts its throwaway database under the system temp directory. // -dir moves it, but MkdirTemp does not create the parent, so whatever // you pass has to exist already — an earlier version of this recipe said // `-dir /tmp/x` and panicked on any machine without a /tmp/x.) // // then over CDP, with the page open, send Input.dispatchDragEvent — // dragEnter, dragOver, drop — with real paths in data.files. Use that // rather than a synthetic DragEvent: it goes through the browser's own // drag machinery, so it is the only way to see that the page does not // navigate away, which is what a dropped file used to do. Then read // location.href, input.files.length and [rst-dropzone-note]. // // Checked that way on 2026-09-04, Chromium 151, BEFORE multi-file: one // file dropped on the textarea and on the document list both landed in // the input with the page still on "/" and the textarea untouched; two // files at once left input.files empty and said "That drop had 2 files"; // a folder was named as a folder; and pressing Store it after a drop // stored the document, which then came back from a search. // // Re-checked the same way on 2026-09-04, Chromium 151, for multi-file. // Five files dropped at once — three text, one binary, one empty — left // input.files holding exactly the three text files, the page still on // "/", and the note reading "Ready to store: alpha.md, beta.md, // gamma.md. Press Store it. 2 refused: scan.png is not text; blank.txt // is empty." Text typed into the paste box before the drop was still // there afterwards; pressing Store it then stored four documents (three // files and the paste) and the table went from three rows to seven. A // drop of 26 files left input.files EMPTY and said "That drop had 26 // files. memoria takes 25 at a time…". A drop of only the binary and // the empty file left the input empty and said "2 refused: …". Nothing // but a favicon 404 in the console. // // What that run does NOT cover, and nothing here does: the total-size // refusal in the browser, and a folder dropped alongside files. Both // are refused by the handler too, and the handler's half is tested. // dropZoneAttr pulls one attribute off the drop zone element. // // It matches inside the zone's own tag rather than anywhere in the page, // which matters more than it looks: the front page renders the search // results and the whole document list too, so "the string appears in the // HTML" is a false pass waiting to happen. func dropZoneAttr(t *testing.T, body, attr string) string { t.Helper() tag := dropZoneTag(t, body) m := regexp.MustCompile(`\b` + regexp.QuoteMeta(attr) + `="([^"]*)"`).FindStringSubmatch(tag) if m == nil { t.Fatalf("the drop zone carries no %s:\n%s", attr, tag) } return m[1] } // dropZoneTag returns the opening tag of the drop zone. func dropZoneTag(t *testing.T, body string) string { t.Helper() i := strings.Index(body, "
") if j < 0 { t.Fatal("the drop zone tag never closes") } return body[i : i+j+1] } // The whole feature is an enhancement, so the form underneath it has to // be the form it was. Three things make that true and each is checked: // the file input still exists inside the drop zone (wrapped, not // replaced), it accepts every file a person chose, and nothing the // script controls is visible before the script has run. // // `multiple` is the one to watch, and the assertion on it is INVERTED // from the one that stood here. While createDocument read one // r.FormFile("file"), `multiple` was forbidden: it would have let the // chooser hand over five files and had four vanish without a word. Now // the handler iterates r.MultipartForm.File["file"] and `multiple` is // required, because without it the chooser offers one file and four of // a person's five never become parts at all. The two states are one // edit apart in each direction, so both reasons are recorded. // // MUTATION-TESTED 2026-09-04, in index.html, each reverted: // // (a) deleting the → "the upload form has no file // input; the drop zone replaced it instead of wrapping it" // (a2) moving that input above `{{with .Drop}}`, so it is on the page // but outside the zone → "the file input is outside the drop zone" // (b) removing `multiple` from it → "the file input is not `multiple`" // (c) deleting `hidden` from the hint → "rst-dropzone-hint is visible // before any script has run" // (c2) replacing `hidden` on the hint with `data-was-hidden="yes"` → // the same failure. Recorded because it did NOT fail before: the // check was strings.Contains(tag, "hidden"), which that attribute // satisfies, so the guard passed over a hint that was visible on a // scriptless page. It is a boundary match now. // (d) writing rst-dropzone="ready" in the template → "the drop zone is // pre-marked in the template" func TestTheUploadFormStillWorksWithoutScript(t *testing.T) { body := get(t, newApp(t), "/add").Body.String() // Isolate the form that posts to /documents. Everything below is // asserted inside it, never against the whole page. i := strings.Index(body, `action="/documents"`) if i < 0 { t.Fatalf("no upload form on the page:\n%s", body) } j := strings.Index(body[i:], "") if j < 0 { t.Fatal("the upload form never closes") } form := body[i : i+j] if !strings.Contains(form, ``) { t.Errorf("the scriptless submit button is gone from the upload form:\n%s", form) } input := regexp.MustCompile(`]*type="file"[^>]*>`).FindString(form) if input == "" { t.Fatalf("the upload form has no file input; the drop zone replaced it "+ "instead of wrapping it, and the form no longer works without script:\n%s", form) } if !strings.Contains(input, `name="file"`) { t.Errorf("the file input is not named `file`, so createDocument's "+ "MultipartForm.File[\"file\"] will not see it: %s", input) } if !regexp.MustCompile(`\bmultiple\b`).MatchString(input) { t.Errorf("the file input is not `multiple`. It is the whole scriptless half of "+ "multi-file upload: without it the chooser takes one file, four of five never "+ "leave the browser, and the handler happily reports one document stored: %s", input) } // Containment: the input must sit between the drop zone's opening // tag and the end of the zone. zone := strings.Index(form, "
") if end < 0 { t.Fatal("the drop zone never closes") } if !strings.Contains(form[zone:zone+end], input) { t.Error("the file input is outside the drop zone; dropping onto the zone would " + "have nowhere to put the file") } // Nothing the script drives may show before the script has run: a // hint promising a drop target, on a page with no drop target, is a // promise the page cannot keep. // `hidden` is matched at its boundaries, not as a substring: the // substring form was satisfied by any attribute with those six // letters anywhere in it — data-was-hidden="yes" passed — which is // the same shape of false pass as "result" matching "0 results". hiddenAttr := regexp.MustCompile(`(^|\s)hidden(\s|>|=)`) for _, el := range []string{"rst-dropzone-hint", "rst-dropzone-note"} { tag := regexp.MustCompile(`

]*>`).FindString(form) if tag == "" { t.Errorf("no %s element in the form:\n%s", el, form) continue } if !hiddenAttr.MatchString(tag) { t.Errorf("%s is visible before any script has run, so a scriptless page "+ "promises a drop zone it does not have: %s", el, tag) } } // And the zone itself is unstyled until the script marks it ready. if v := dropZoneTag(t, body); strings.Contains(v, `rst-dropzone="`) { t.Errorf("the drop zone is pre-marked in the template: %s\n"+ "The styling keys off rst-dropzone=\"ready\", which only the script may "+ "write — otherwise a page whose script failed draws a dashed box that "+ "eats files.", v) } } // The script has to actually be served, and it may send a file to // exactly one place: the form's own action, read off the form. // // THE PROPERTY HERE WAS NARROWED, on 2026-09-04, on the owner's explicit // approval of the asynchronous-upload design. What stood here was "the // drop fills the form's own input and does nothing else", enforced by // forbidding XMLHttpRequest outright alongside fetch( and both spellings // of submit. That was the right property while a drop meant filling an // input; it is not the property of a design where each dropped file is // posted on its own, in the background, with a progress tray beside it. // // What is emphatically NOT narrowed is the reason that rule existed: a // second way in is a way in that the caps, the NUL sniff, the whole-body // limit that keeps parts off the disk and the same-origin check were // never asked about. So this pins the ADDRESS instead of the mechanism, // and it is stricter about the address than the old one was about // anything: // // - fetch(, sendBeacon, .submit( and requestSubmit( stay forbidden. // One posting mechanism, so there is one thing to check. // - exactly TWO .open( call sites exist, and each is one of exactly // two permitted shapes — never a literal, never an expression built // out of one. // - `endpoint` is assigned exactly once, from the form's own action // attribute, and the form is the file input's own form. // - no address literal appears anywhere in the file. A second endpoint // has to be spelled somewhere, and this is where. // // WIDENED BY ONE REQUEST, on 2026-09-05, on the owner's approval of the // refreshing document list. A drop that never navigates leaves the list // below it as the server drew it before the drop, and the tray said so // and offered a link. It now asks the server for the page again and puts // the freshly rendered regions in place of the old ones — which needs a // GET, and this test permitted exactly one request of any kind. // // It is widened by ONE SHAPE and no more, because narrowing it once // already proved insufficient: an earlier version was walked straight // past with `endpoint += '/../upload'`. So the second shape is not "a // GET" and not "a same-origin GET" — it is the page the browser is // already on, spelled as location.href or as location.pathname + // location.search, and nothing else parses. A constructed path, a // literal, a concatenation onto either of those, or a third call site of // any kind fails, and so does a POST that is not the form's action. // // Why that address is safe to permit when no other is: it is a GET, of a // page this person was already served, at an address the browser chose // rather than this file. It stores nothing, and it can reach nothing the // reader could not reach by pressing reload. // // Note for whoever edits drop.js next: this scans the file whole, // comments included, so prose in that file may not spell the forbidden // names either, nor quote a path. That is the deliberate direction of // error — a comment that trips this fails loudly, where stripping // comments first would risk over-stripping and passing something real. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `mv static/drop.js static/drop2.js` → "GET /static/drop.js: // status 404, want 200" // (b) `xhr.open("POST", endpoint, true)` → `xhr.open("POST", // "/upload", true)`, the second endpoint this whole test exists // for → "the drop script opens a request on something other than // the endpoint read off the form" AND "the drop script contains // the address literal "/upload"" // (c) a second call site added (`var probe = new XMLHttpRequest(); // probe.open("POST", endpoint, true);`) → "the drop script opens 2 // requests; exactly one call site may exist" // (d) `endpoint = form.getAttribute("action")` → `endpoint = // "/documents"` — the plausible "it is right there in the // template" edit, and the one that would let the template's action // and the script's address drift apart → "the drop script does not // read its address off the form's own action attribute", plus the // literal failure // (e) `xhr.send(body)` → `form.requestSubmit()` → "the drop script // uses requestSubmit(" // (f) adding `console.log("dropped", file.name)` → "the drop script // writes to the console; a dropped file's name is content" // // MUTATION-TESTED 2026-09-04, second round, after a code review walked // past this test twice on a build it had not been able to fail. Both were // replayed here and both were green before the two regexes above were // widened; both are red now, and each was reverted: // // (g) `endpoint += '/../upload';` on the line after the read — the // reviewer's own mutation, and the reason "assigned once" is now // spelled to include compound assignment → "the drop script assigns // `endpoint` 2 times" AND "the drop script contains the address // literal '/../upload'" // (h) ``var alt = `/upload`;`` — the same address in the third quoting // style, which the old double-quotes-only pattern could not see → // "the drop script contains the address literal `/upload`" // // MUTATION-TESTED 2026-09-05, third round, after the second request was // permitted. Every one of these was run against the narrowed test, and // each was reverted: // // (i) `req.open("GET", "/documents", true)` — the re-read pointed at // another address → "the drop script opens a request that is // neither of the two things it is allowed to open", "0 re-reads of // this page", and "the address literal "/documents"" // (j) the reviewer's own shape, moved to the new request: `var where = // location.pathname + location.search; where += '/../upload'; // req.open("GET", where, true)` → the same three failures, the // third naming '/../upload' // (k) `req.open("GET", location.pathname + location.search + endpoint, // true)` — the sharp one, because it writes NO literal down and // would slip past the literal ban entirely → red on the shape // alone: the trailing anchor is what stops an address that merely // begins with the permitted one // (l) a second re-read call site added → "the drop script opens 3 // requests; exactly two call sites may exist" // (m) `req.open("POST", location.pathname + location.search, true)` — // the permitted address with the verb changed, which would be a // second way in wearing the re-read's clothes → "neither of the two // things it is allowed to open" and "0 re-reads of this page" // (n) the ORIGINAL property, replayed to prove widening did not blunt // it: `xhr.open("POST", "/upload", true)` → "0 posts to the form's // action" and "the address literal "/upload"" // (o) and the code review's compound assignment, replayed for the same // reason: `endpoint += '/../upload'` after the read → "the drop // script assigns `endpoint` 2 times" and "the address literal // '/../upload'" // // And two that do NOT fail, recorded because a reader will try them: // changing the form's action in index.html (the script reads it, so both // sides move together — the point of reading it rather than writing it // down twice), and spelling the re-read `location.href` instead of // `location.pathname + location.search`. The second is deliberate: both // name the page the browser is already on, neither is written down here, // and pinning one spelling would be pinning a style rather than the // property. func TestTheDropScriptPostsOnlyToTheFormsOwnAction(t *testing.T) { src := servedDropScript(t, newApp(t)) // requestSubmit( is listed separately and deliberately: it is not a // suffix of ".submit(" and the whole point of the rule is that the // script never submits the form, by any spelling. for _, forbidden := range []string{"fetch(", "navigator.sendBeacon", ".submit(", "requestSubmit("} { if strings.Contains(src, forbidden) { t.Errorf("the drop script uses %s. There is one posting mechanism and one address: "+ "a second way in is a way in that the server's caps, its NUL check, its "+ "whole-body limit and its same-origin check were not asked about.", forbidden) } } // Exactly two requests are opened, and each is one of exactly two // shapes: the POST to the address read off the form, and the GET of // the page the browser is already on. Neither address may be a // literal, and neither may be an expression built out of one. opens := regexp.MustCompile(`\.open\s*\(([^)]*)\)`).FindAllStringSubmatch(src, -1) if len(opens) != 2 { t.Fatalf("the drop script opens %d requests; exactly two call sites may exist — the "+ "upload and the page it re-reads — or this test cannot say where all of them go", len(opens)) } // The upload. Unchanged: the variable, and only the variable. toTheForm := regexp.MustCompile(`^\s*"POST"\s*,\s*endpoint\s*(,[^,]*)?$`) // The re-read. Both spellings of "where the browser already is", and // nothing that merely starts with one: the trailing anchor is what // stops `location.href + "/../upload"` from parsing as this shape. toThisPage := regexp.MustCompile( `^\s*"GET"\s*,\s*(?:location\.href|location\.pathname\s*\+\s*location\.search)\s*(,[^,]*)?$`) posts, rereads := 0, 0 for _, o := range opens { switch { case toTheForm.MatchString(o[1]): posts++ case toThisPage.MatchString(o[1]): rereads++ default: t.Errorf("the drop script opens a request that is neither of the two things it is "+ "allowed to open: .open(%s). It may POST to the endpoint read off the form, "+ "and it may GET the page the reader is already on (location.href, or "+ "location.pathname + location.search). An address that is a literal, or built "+ "out of one, or anything else at all, is an address nothing on the server "+ "side agreed to.", o[1]) } } if posts != 1 || rereads != 1 { t.Errorf("the drop script opens %d posts to the form's action and %d re-reads of this "+ "page; it may open exactly one of each. Two posts is a second way in; two "+ "re-reads is a race for which answer lands last.", posts, rereads) } // And that variable is the form's action, assigned once. A second // assignment could be anything at all. // // COMPOUND assignment counts. `endpoint = …` was all this matched // until a code review wrote `endpoint += '/../upload'` on the line // after the read and watched the whole test stay green: `+=` is not // `=`, and the address that request would have gone to is /upload. // The alternation covers every JavaScript assignment operator, // longest first, and the trailing [^=] keeps `==` and `===` out. assign := regexp.MustCompile(`\bendpoint\s*(?:>>>|\*\*|<<|>>|\|\||&&|\?\?|[-+*/%&|^])?=[^=]`) if n := len(assign.FindAllString(src, -1)); n != 1 { t.Errorf("the drop script assigns `endpoint` %d times; the address is read once, from "+ "one place, or the value opened above is not the value read below", n) } if !strings.Contains(src, `endpoint = form.getAttribute("action")`) { t.Error("the drop script does not read its address off the form's own action " + "attribute. Anything else — a literal, a constructed path, another element's " + "action — is an address that can drift from the one the scriptless submit uses.") } if !strings.Contains(src, "var form = input.form") { t.Error("the drop script does not take its form from the file input. It has to be THAT " + "form: the one the person would have posted by pressing Store it.") } // No address is written down in here at all — in ANY of the three // ways JavaScript spells a string. This used to inspect double-quoted // strings only, so '/upload' and `/upload` were both invisible to it; // the same code review that found the compound assignment above wrote // its second endpoint in single quotes, which is the spelling half // this file's own selectors already use. var quoted []string for _, q := range []string{`"`, `'`, "`"} { quoted = append(quoted, q+`(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?//[^`+q+`]*`+q, q+`/[^`+q+`]*`+q) } if lit := regexp.MustCompile(strings.Join(quoted, "|")).FindAllString(src, -1); len(lit) > 0 { t.Errorf("the drop script contains the address literal %s. Every address it uses comes "+ "off the page; one written down in here is one the server never agreed to.", strings.Join(lit, ", ")) } // A filename is somebody's content. It never goes to a log, and the // browser console is close enough to one to keep the rule. if strings.Contains(src, "console.") { t.Error("the drop script writes to the console; a dropped file's name is content") } } // The cap the page advertises to the browser is the cap the handler // enforces. Not "roughly": the same integer. // // This is the check that makes the client-side size refusal honest. A // drifted cap is worse than none — the page would refuse a file the // server would have taken, and the person would have no way to find that // out. So the number is read off the rendered page and files of exactly // that size and exactly one byte more are posted at the real handler. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) data-max-bytes="100000" in index.html → "the page advertises a // cap of 100000 bytes but the handler answered "/?stored=1" for a // file 1 byte over it", plus "the refusal does not name the file // it refused" and "2 documents are stored ... want 1" // (b) `io.LimitReader(f, maxUpload+1)` → `io.LimitReader(f, maxUpload)` // in readUpload, which truncates instead of refusing → the same // failure, at the real cap. This is the mutation worth knowing: an // over-cap upload came back "/?stored=1" having quietly lost its // last byte. // // And one that does NOT fail, recorded because a reader will try it: // changing maxUpload itself. The page renders that constant, so both // sides move together and the test stays green — which is the whole // point of publishing the number rather than writing it twice. What // this test can catch is the two halves disagreeing, and (a) and (b) are // each of those halves. func TestTheAdvertisedCapIsTheHandlersCap(t *testing.T) { h := newApp(t) cap := atoi(t, dropZoneAttr(t, get(t, h, "/add").Body.String(), "data-max-bytes")) if cap < 1024 { t.Fatalf("data-max-bytes is %d, which no real cap is; the tests below would prove nothing", cap) } // Text, not one long line: the chunker's job is not what is being // tested, but a document it cannot split is not a fair sample. filler := "The quarterly figure was recorded in full and then again.\n\n" at := strings.Repeat(filler, cap/len(filler)+1)[:cap] if rec := upload(t, h, "at-the-cap.txt", at); rec.Header().Get("Location") != "/add?stored=1" { t.Errorf("a file of exactly the advertised cap (%d bytes) was refused with %q; "+ "the page tells the browser it will be taken", cap, rec.Header().Get("Location")) } // One byte over: refused, and refused BY NAME. The assertion is on // the outcome sentence rather than on a URL code, because that is // the whole of what a person is told and it is the half that can // silently become "stored". rec := upload(t, h, "over-the-cap.txt", at+"x") if loc := rec.Header().Get("Location"); loc != "/add?stored=0&refused=1" { t.Errorf("the page advertises a cap of %d bytes but the handler answered %q for a "+ "file 1 byte over it; the browser would refuse a file this server accepts, "+ "or the other way round", cap, loc) } said := outcome(t, h, rec) if !strings.Contains(said, "over-the-cap.txt") { t.Errorf("the refusal does not name the file it refused: %q", said) } if n := storedCount(t, h); n != 1 { t.Errorf("%d documents are stored after one at the cap and one over it, want 1", n) } } // Same again for the sniff window. looksLikeText reads a prefix and the // drop zone reads the same prefix, so the page publishes its length and // this proves the handler agrees: a NUL at the last byte inside the // window is refused, and a NUL one byte past it is stored. // // The second half is the one that catches drift. If the handler's window // were larger than the page's, the browser would wave through a file the // server then rejects — and the person would be told "binary" by a page // that had just said the file was fine. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) data-sniff-bytes="64" in index.html → "a NUL at byte 64 — one // past the advertised 64-byte window — was answered // "/?stored=0&refused=1", want /?stored=1" // (b) looksLikeText's window hard-coded back to 4096 → "a NUL at byte // 8191 — the last byte inside the advertised 8192-byte window — // was answered "/?stored=1", want a refusal" // // As with the cap: changing sniffBytes itself moves both sides and // stays green on purpose. func TestTheAdvertisedSniffWindowIsTheHandlersWindow(t *testing.T) { h := newApp(t) window := atoi(t, dropZoneAttr(t, get(t, h, "/add").Body.String(), "data-sniff-bytes")) if window < 16 { t.Fatalf("data-sniff-bytes is %d; the tests below would prove nothing", window) } // A NUL at the last byte the window covers: refused, by name. inside := strings.Repeat("a", window-1) + "\x00" + strings.Repeat("b", 64) rec := upload(t, h, "inside.txt", inside) if loc := rec.Header().Get("Location"); loc != "/add?stored=0&refused=1" { t.Errorf("a NUL at byte %d — the last byte inside the advertised %d-byte window — "+ "was answered %q, want a refusal", window-1, window, loc) } else if said := outcome(t, h, rec); !strings.Contains(said, "inside.txt") { t.Errorf("the refusal does not name the file it refused: %q", said) } // A NUL one byte past the window: stored, because neither side // looks that far. If this now fails, the handler reads further than // the page says it does and the browser's check has gone blind. outside := strings.Repeat("a", window) + "\x00" + strings.Repeat("b", 64) if loc := upload(t, h, "outside.txt", outside).Header().Get("Location"); loc != "/add?stored=1" { t.Errorf("a NUL at byte %d — one past the advertised %d-byte window — was answered "+ "%q, want /add?stored=1; the handler inspects more than the page tells the "+ "browser it does", window, window, loc) } } // The file-count cap the page advertises is the one the handler // enforces, and a drop over it is refused WHOLE — nothing stored, and // the sentence says nothing was stored. // // The number is read off the rendered page, so this cannot pass by // agreeing with itself: exactly that many files are stored and exactly // one more is refused. The half that matters is the second — a cap // applied part-way through a drop would store the first twenty-five of // twenty-six and report success, which is this codebase's worst failure // shape wearing a limit as a costume. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `len(files) > maxDropFiles` → `len(files) > maxDropFiles*2` in // createDocument, so the advertised cap is not the enforced one → // "26 files — one over the advertised cap of 25 — stored 26 // documents" // (b) truncating instead of refusing (`files = files[:maxDropFiles]` // in place of the refusal) → "26 files ... stored 25 documents", // which is the silent truncation itself // (c) data-max-files="4" in index.html → "5 files — one over the // advertised cap of 4 — stored 5 documents": the handler still // refuses at 25, so page and handler have drifted and the test // says so from the page's side func TestTheAdvertisedFileCountCapIsTheHandlersCap(t *testing.T) { h := newApp(t) maxFiles := atoi(t, dropZoneAttr(t, get(t, h, "/add").Body.String(), "data-max-files")) if maxFiles < 2 || maxFiles > 200 { t.Fatalf("data-max-files is %d; the drop built below would prove nothing", maxFiles) } atCap := make([]file, 0, maxFiles) for i := 0; i < maxFiles; i++ { atCap = append(atCap, file{fmt.Sprintf("at-%02d.txt", i), fmt.Sprintf("Passage %d about quarterly revenue and hosting.", i)}) } submit(t, h, "", "", atCap...) if n := storedCount(t, h); n != maxFiles { t.Fatalf("%d files, the advertised cap, stored %d documents", maxFiles, n) } h2 := newApp(t) overCap := append(atCap, file{"one-too-many.txt", "One more passage about revenue."}) rec := submit(t, h2, "", "", overCap...) if n := storedCount(t, h2); n != 0 { t.Errorf("%d files — one over the advertised cap of %d — stored %d documents. A "+ "submission cap must refuse the drop whole: storing some of it and reporting "+ "success is the silent truncation this app treats as its worst failure", len(overCap), maxFiles, n) } said := outcome(t, h2, rec) if !strings.Contains(said, strconv.Itoa(len(overCap))) || !strings.Contains(said, strconv.Itoa(maxFiles)) { t.Errorf("the refusal names neither how many were dropped (%d) nor how many are "+ "taken at a time (%d): %q", len(overCap), maxFiles, said) } } // Same again for the total-size cap. A drop over it is refused before // anything is opened, so the answer is "none of it was stored". // // This one is enforced twice on purpose and the test does not care // which half caught it: http.MaxBytesReader caps the BODY (framing // included) and the handler sums the parts' own sizes against the // advertised total. A drop landing between the two would otherwise be // taken, which is why the second check exists at all. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) removing the `total > maxDropBytes` check AND raising the // MaxBytesReader to `maxDropBytes*4` → "a drop of 23068672 bytes, // over the advertised 20971520, stored 11 documents" // (b) data-max-total-bytes="{{.MaxBytes}}" in index.html, so the page // advertises the per-file cap as the total → "the advertised // total (2097152) is not larger than the advertised per-file cap // (2097152); a drop over the total could not be built out of // legal files". A page that advertises a total it does not have // fails here rather than passing quietly. func TestTheAdvertisedTotalCapIsTheHandlersCap(t *testing.T) { h := newApp(t) body := get(t, h, "/add").Body.String() total := atoi(t, dropZoneAttr(t, body, "data-max-total-bytes")) per := atoi(t, dropZoneAttr(t, body, "data-max-bytes")) if total <= per { t.Fatalf("the advertised total (%d) is not larger than the advertised per-file cap "+ "(%d); a drop over the total could not be built out of legal files", total, per) } // Every file legal on its own — at the per-file cap — and one more // of them than the total allows. Nothing here is refusable except // the total. filler := "The quarterly figure was recorded in full and then again.\n\n" one := strings.Repeat(filler, per/len(filler)+1)[:per] files := make([]file, 0, total/per+1) for i := 0; len(files)*per <= total; i++ { files = append(files, file{fmt.Sprintf("big-%02d.txt", i), one}) } rec := submit(t, h, "", "", files...) if n := storedCount(t, h); n != 0 { t.Errorf("a drop of %d bytes, over the advertised %d, stored %d documents. The total "+ "cap must refuse the drop whole, before anything is stored", len(files)*per, total, n) } if said := outcome(t, h, rec); !strings.Contains(said, "20 MiB") { t.Errorf("the refusal does not name the total it enforces: %q", said) } } // The browser refuses in the handler's own words. A person who drops a // PDF and a person who picks one from the file chooser are told the same // thing about it, because it is the same sentence with the same // filename in it — the clause on the drop zone and the clause the // handler composes come from one field of newDropZone. // // The server half is produced by actually posting the file, not by // reading a string back off a page: a template that renders the clause // correctly and a handler that composes a different one would otherwise // agree perfectly. // // MUTATION-TESTED 2026-09-04, each reverted: (a) data-reason-binary set // to a literal "{name} is a binary file." in index.html → the binary // mismatch, printing both sentences; (b) ReasonEmpty used in place of // ReasonBinary in readUpload → the same failure from the handler's // side. Editing the field in newDropZone moves both at once and stays // green, which is what sharing the sentence is for. func TestTheBrowserRefusesInTheHandlersWords(t *testing.T) { h := newApp(t) page := get(t, h, "/add").Body.String() for _, c := range []struct { attr, name, body string }{ {"data-reason-binary", "scan.png", "PNG\x00\x00 not text at all"}, {"data-reason-empty", "blank.txt", " \n "}, } { // The browser's clause, with the filename in it. browser := strings.ReplaceAll(dropZoneAttr(t, page, c.attr), "{name}", c.name) // The handler's clause, produced by refusing the real file. It // arrives inside the outcome sentence, so this looks for the // clause within it rather than for equality. said := outcome(t, h, submit(t, h, "", "", file{c.name, c.body})) if !strings.Contains(said, browser) { t.Errorf("the drop zone and the handler refuse %s differently.\n browser: %q\n server: %q\n"+ "Two sentences for one rule means one of them is out of date and nobody can tell which.", c.attr, browser, said) } } } // servedDropScript fetches the script the page actually links, and // proves it is that script before anything is asserted about its // contents. Fetched through the handler rather than read off disk: // what ships is what the asset route serves. func servedDropScript(t *testing.T, h http.Handler) string { t.Helper() body := get(t, h, "/add").Body.String() m := regexp.MustCompile(`]+src="([^"]*drop[^"]*\.js[^"]*)"`).FindStringSubmatch(body) if m == nil { t.Fatalf("the page links no drop script:\n%s", body) } rec := get(t, h, m[1]) if rec.Code != http.StatusOK { t.Fatalf("GET %s: status %d, want 200 — the page links a script that is not served, "+ "so the drop zone never appears and nobody finds out from a test", m[1], rec.Code) } src := rec.Body.String() if len(src) < 500 { t.Fatalf("GET %s served %d bytes; that is not the drop script, and every "+ "assertion against it would pass against an empty file", m[1], len(src)) } // The vacuity guard: prove this is really the file, so an absence // asserted against it means something. if !strings.Contains(src, "rst-dropzone") { t.Fatalf("GET %s did not serve the drop script (no rst-dropzone in it)", m[1]) } return src } // The note is a live region, and a live region has to be in the // accessibility tree BEFORE the text it announces is written into it. // Unhiding it and filling it in the same tick is the common way to build // one that never announces anything — and this particular region carries // the refusals: "that drop had five files", "that is a folder". A // refusal a blind person cannot hear is exactly the silent failure this // codebase treats as its worst, so the empty paragraph is unhidden at // the ready step and stays there. // // Go cannot run the script, so this pins the shape instead: exactly one // place unhides the note, and it is the ready step at the end of the // file rather than say(). The CSS half is checked too — an empty // paragraph that still takes a half-line of margin is a visible cost for // an invisible element, and the obvious way to "fix" that is to hide the // note again, which is the bug coming back. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) moving `note.hidden = false` back into say() → "drop.js unhides // the note before the ready step" // (b) deleting it entirely → "drop.js never unhides the note" // (c) deleting the [rst-dropzone-note]:empty rule from memoria.css → // "memoria.css has no :empty rule for the note" func TestTheLiveRegionExistsBeforeItHasAnythingToSay(t *testing.T) { h := newApp(t) src := servedDropScript(t, h) // The anchor is the ready step's own marker comment. Asserted // first, because without it the position check below compares // against -1 and passes for the wrong reason. const anchor = "and only now does the zone exist" at := strings.Index(src, anchor) if at < 0 { t.Fatalf("drop.js no longer carries the %q marker, so this test cannot tell the "+ "ready step from the rest of the file and proves nothing", anchor) } unhide := regexp.MustCompile(`note\.hidden\s*=`).FindAllStringIndex(src, -1) if len(unhide) == 0 { t.Fatal("drop.js never unhides the note; the live region stays hidden and every " + "refusal a drop makes is silent to a screen reader") } if len(unhide) > 1 { t.Errorf("drop.js assigns note.hidden in %d places. One of them is the ready step; "+ "any other can only be unhiding it later, beside the text — which is the "+ "pattern that does not announce.", len(unhide)) } for _, u := range unhide { if u[0] < at { t.Errorf("drop.js unhides the note before the ready step (byte %d, marker at %d). "+ "The live region must enter the accessibility tree empty and early: unhidden "+ "in the same tick as its first message, it is commonly not announced at all.", u[0], at) } } // The rendered note is still a live region at all. tag := regexp.MustCompile(`

]*>`).FindString(get(t, h, "/add").Body.String()) if !strings.Contains(tag, `aria-live="polite"`) || !strings.Contains(tag, `role="status"`) { t.Errorf("the note is not a polite live region, so unhiding it early buys nothing: %s", tag) } // And an empty note costs no space. css, err := os.ReadFile(filepath.Join("..", "memoria", "static", "memoria.css")) if err != nil { t.Fatal(err) } if !regexp.MustCompile(`\[rst-dropzone-note\]:empty\s*\{[^}]*margin\s*:\s*0`).Match(css) { t.Error("memoria.css has no :empty rule for the note zeroing its margin. The note is " + "unhidden from the moment the script runs, so an empty paragraph with a top " + "margin pushes the Store it button down for no visible reason — and the " + "obvious fix for that is to hide the note again, which is the bug.") } } // Every refusal names the cap it enforces, and the hint above the input // names all three. The per-file cap, the total and the file count are // each one number said in several places, and each is written down once. // // Not pedantry: the refusal used to carry "2 MiB" as a literal while the // hint beside it derived its number from maxUpload. Raising the cap // would have left the page refusing a 3 MiB file with the words "that is // larger than 2 MiB" — and the person, who had just read a hint saying // 4 MiB, would have no way to tell which of the two lines was lying. // With three caps there are three ways to make that mistake. // // The per-file refusal comes from a real over-cap upload rather than // from a rendered attribute, so a handler composing its own sentence // cannot pass by agreeing with a template. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `ReasonTooBig: "{name} is larger than 2 MiB"` — the literal put // back — with maxUpload raised to `4 << 20` → "the refusal names // 2 MiB but the per-file cap it enforces is 4 MiB" // (b) `dropLimit()` swapped for `uploadLimit()` in the Hint → "the // hint does not name the total cap (20 MiB)" // (c) `fileLimit()` replaced with `"25"` in the Hint, with maxDropFiles // raised to 30 → "the hint does not name the file-count cap (30)" // // Changing any of the three constants ALONE keeps this green, which is // the point: one number, said everywhere from one place. func TestTheRefusalsNameTheCapsTheyEnforce(t *testing.T) { h := newApp(t) body := get(t, h, "/add").Body.String() capBytes := atoi(t, dropZoneAttr(t, body, "data-max-bytes")) totalBytes := atoi(t, dropZoneAttr(t, body, "data-max-total-bytes")) maxFiles := atoi(t, dropZoneAttr(t, body, "data-max-files")) if capBytes>>20 < 1 || totalBytes>>20 < 1 { t.Fatalf("the advertised caps are %d and %d bytes; this test compares whole MiB and "+ "would prove nothing", capBytes, totalBytes) } want := fmt.Sprintf("%d MiB", capBytes>>20) wantTotal := fmt.Sprintf("%d MiB", totalBytes>>20) // The refusal the handler actually composes, for a real file over // the real cap. filler := "The quarterly figure was recorded in full and then again.\n\n" over := strings.Repeat(filler, capBytes/len(filler)+2)[:capBytes+1] refusal := outcome(t, h, upload(t, h, "over.txt", over)) said := regexp.MustCompile(`\d+\s*MiB`).FindAllString(refusal, -1) if len(said) == 0 { t.Fatalf("the too-big refusal names no size at all, so it can neither agree nor "+ "disagree with the cap: %q", refusal) } for _, got := range said { if got != want { t.Errorf("the refusal names %s but the per-file cap it enforces is %s (%d bytes, "+ "read off the same page the browser reads). One of the two moved and the "+ "other did not: %q", got, want, capBytes, refusal) } } // And the hint above the input says all three, because they are the // same numbers from the same place. hint := regexp.MustCompile(`

]*>([^<]*)

`).FindStringSubmatch(body) if hint == nil { t.Fatalf("no drop zone hint on the page:\n%s", body) } for _, w := range []struct{ what, value string }{ {"per-file cap", want}, {"total cap", wantTotal}, {"file-count cap", strconv.Itoa(maxFiles)}, } { if !strings.Contains(hint[1], w.value) { t.Errorf("the hint does not name the %s (%s): %q — a person is told one set of "+ "limits before the upload and a different one after it", w.what, w.value, strings.TrimSpace(hint[1])) } } } // flashText pulls the rendered flash message out of a page. The callout // body is rastrillo's shape,

…

(shared // chrome §4); it was a

, whose own margins doubled the // callout's height. func flashText(t *testing.T, body string) string { t.Helper() m := regexp.MustCompile(`

([^<]*)

`).FindStringSubmatch(body) if m == nil { t.Fatalf("no flash on the page:\n%s", body) } return strings.TrimSpace(unescape(m[1])) } // unescape undoes html/template's attribute and text escaping for the // few entities this app's copy can produce. Both sides of the comparison // go through it, so a difference is a difference in the copy. func unescape(s string) string { return strings.NewReplacer( """, `"`, """, `"`, "'", "'", "'", "'", "&", "&", "<", "<", ">", ">", "+", "+", ).Replace(s) } func atoi(t *testing.T, s string) int { t.Helper() n := 0 if s == "" { t.Fatal("empty number") } for _, c := range s { if c < '0' || c > '9' { t.Fatalf("%q is not a number", s) } n = n*10 + int(c-'0') } return n } // ---- one submission, several documents -------------------------------- // Five files in, five documents out — each with its own title, its own // chunks and its own row. // // The assertions are on the document TABLE, never on the page: the front // page renders the search results, the flash and the list, and the flash // alone contains the words "5 documents". A filename appearing somewhere // in the HTML is the false pass this whole file is written against. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `for _, hdr := range files` → `for _, hdr := range files[:1]` in // createDocument → "5 files stored 1 documents: [alpha]" — the // old single-file behaviour, caught // (b) `stored++` moved outside the loop (a plain `stored = 1` after // it) → "the outcome sentence does not say how many documents // were stored: "Stored 1 document."" func TestEveryFileInADropBecomesItsOwnDocument(t *testing.T) { h := newApp(t) files := []file{ {"alpha.md", "## Revenue\nAlpha revenue was 100 in the quarter just gone."}, {"beta.md", "## Revenue\nBeta revenue was 200 in the quarter just gone."}, {"gamma.md", "## Costs\nGamma hosting fell after the migration completed."}, {"delta.md", "## Risks\nDelta concentration is the single largest risk."}, {"epsilon.md", "## Notes\nEpsilon minutes from the August meeting."}, } rec := submit(t, h, "", "", files...) // The redirect lands on /add, which carries the outcome; the document // list stayed on the index when adding moved to its own page. follow(t, h, rec) titles := storedTitles(t, get(t, h, "/").Body.String()) if len(titles) != len(files) { t.Fatalf("%d files stored %d documents: %v", len(files), len(titles), titles) } for _, f := range files { want := strings.TrimSuffix(f.name, ".md") if !slices.Contains(titles, want) { t.Errorf("%q is not in the document list: %v", want, titles) } } // Each is separately indexed, not one document with five sections. for _, term := range []string{"epsilon", "gamma"} { if n := strings.Count(get(t, h, "/?q="+term).Body.String(), "rst-result-title"); n != 1 { t.Errorf("searching %q returns %d results, want 1; the files did not become "+ "separate documents", term, n) } } if said := outcome(t, h, rec); !strings.Contains(said, "Stored 5 documents") { t.Errorf("the outcome sentence does not say how many documents were stored: %q", said) } } // The half that matters: some of a drop is refused and the rest is // stored anyway, and the person is told BOTH — how many landed, and // every refusal by name with the reason for that file. // // Storing four of five while saying "Stored 4 documents." and nothing // else is the silent truncation this codebase treats as its worst // failure. So is refusing one without saying which one. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `refused = append(...)` in the binary branch of readUpload // replaced with a bare `continue` (refuse it, say nothing) → // "nothing in the outcome names the refused file: "Stored 4 // documents."" // (b) the per-file refusal replaced by a whole-drop refusal (`a.refuse` // and return, instead of appending) → "4 good files among 5 // stored 2 documents: [two one]" — the two that happened to come // before the bad one, which is the half-taken drop itself // (c) `fill(c.ReasonBinary, ...)` → `fill(c.ReasonEmpty, ...)` for the // binary case → "the refusal gives the wrong reason for // scan.png": it said "is empty" for a file that is not text func TestAPartialDropStoresTheGoodOnesAndNamesTheBad(t *testing.T) { h := newApp(t) rec := submit(t, h, "", "", file{"one.txt", "Revenue was 100 for the first region we serve."}, file{"two.txt", "Revenue was 200 for the second region we serve."}, file{"scan.png", "\x89PNG\x00\x00\x00 binary payload that is not text"}, file{"three.txt", "Revenue was 300 for the third region we serve."}, file{"four.txt", "Revenue was 400 for the fourth region we serve."}, ) page := follow(t, h, rec).Body.String() // The outcome is on /add, where the redirect lands; the list stayed // on the index when adding moved to its own page. list := get(t, h, "/").Body.String() titles := storedTitles(t, list) if len(titles) != 4 { t.Fatalf("4 good files among 5 stored %d documents: %v", len(titles), titles) } if slices.Contains(titles, "scan") { t.Errorf("the binary file was stored anyway: %v", titles) } said := flashText(t, page) if !strings.Contains(said, "Stored 4 documents") { t.Errorf("the outcome does not say how many landed: %q", said) } if !strings.Contains(said, "scan.png") { t.Errorf("nothing in the outcome names the refused file: %q", said) } if !strings.Contains(said, "1 refused") { t.Errorf("the outcome does not say how many were refused: %q", said) } if !strings.Contains(said, "scan.png is not text") { t.Errorf("the refusal gives the wrong reason for scan.png: %q", said) } // And the good ones are not named as refusals. The clause list is // everything after the count, so this is where a reason attached to // the wrong file would show up. Guarded on the count being there at // all: a missing "1 refused" is already reported above, and slicing // from -1 would panic rather than fail. if at := strings.Index(said, "1 refused"); at >= 0 { for _, good := range []string{"one.txt", "two.txt", "three.txt", "four.txt"} { if strings.Contains(said[at:], good) { t.Errorf("%s was stored but appears in the refusal list: %q", good, said) } } } } // The count in the sentence is the number of documents that exist. // // "A count that fails is not a count of zero" is already an AGENTS.md // rule; this is its neighbour — a count that is written by hand beside // the loop that does the work drifts from it the first time the loop // gains a branch. So the number is read out of the sentence and compared // with the rows in the table. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `stored++` → `stored += 2` in the file loop → "the outcome says // 7 documents were stored but the list holds 4" // (b) `c.storedPart(stored)` → `c.storedPart(stored+len(refused))` in // outcome(), the plausible "say how many we looked at" edit → // "the outcome says 6 documents were stored but the list holds 4" // (c) `stored++` deleted from the paste branch → "the outcome says 3 // documents were stored but the list holds 4" func TestTheStoredCountIsTheNumberActuallyStored(t *testing.T) { h := newApp(t) rec := submit(t, h, "Minutes", "The August meeting agreed the migration date.", file{"good-one.txt", "Revenue was 100 across the first region."}, file{"bad-one.png", "\x00\x00 binary"}, file{"good-two.txt", "Revenue was 200 across the second region."}, file{"bad-two.png", "\x00\x00 binary"}, file{"good-three.txt", "Revenue was 300 across the third region."}, ) page := follow(t, h, rec).Body.String() // The outcome is on /add, where the redirect lands; the list stayed // on the index when adding moved to its own page. list := get(t, h, "/").Body.String() rows := len(storedTitles(t, list)) said := flashText(t, page) m := regexp.MustCompile(`Stored (\d+) documents?\.`).FindStringSubmatch(said) if m == nil { t.Fatalf("the outcome sentence states no stored count, so nothing here can be "+ "compared: %q", said) } if got := atoi(t, m[1]); got != rows { t.Errorf("the outcome says %d documents were stored but the list holds %d.\n %q\n %v", got, rows, said, storedTitles(t, list)) } // Three files and the paste box: four. Stated so a change to the // fixture cannot quietly make the comparison above trivial. if rows != 4 { t.Errorf("3 good files and a paste stored %d documents, want 4: %v", rows, storedTitles(t, list)) } } // Dropping an edited file again UPDATES its document. The filename is // the stable key, exactly as it is for a single upload, so a second drop // of the same five files is five updates and not five more rows. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `a.store.Ingest(ctx, "upload", hdr.Filename, …)` → `Ingest(ctx, // "upload", "", …)` in createDocument, so the key falls back to // the title → still green (the title is derived from the filename, // so it is the same key by another route). Recorded because it is // the mutation a reader will try first and it proves nothing. // (b) the key changed back to the body — `Ingest(ctx, "upload", // string(raw), …)`, the old content-hash scheme → "a re-drop of 2 // files made 3 documents" and "the superseded passage is still // searchable", which is the duplication this test exists for // (c) the stale-passage half: `s.ix.RemoveDocument(docID)` commented // out in Store.Ingest → "the superseded passage is still // searchable" func TestRedroppingFilesUpdatesThemRatherThanDuplicating(t *testing.T) { h := newApp(t) submit(t, h, "", "", file{"board.md", "## Revenue\nThe zebrafinch figure was 100 for the quarter."}, file{"minutes.md", "## Present\nEveryone was at the August meeting."}, ) submit(t, h, "", "", file{"board.md", "## Revenue\nThe wildebeest figure was 200 for the quarter."}, file{"minutes.md", "## Present\nEveryone was at the August meeting."}, ) if n := storedCount(t, h); n != 2 { t.Errorf("a re-drop of 2 files made %d documents", n) } if got := get(t, h, "/?q=zebrafinch").Body.String(); strings.Contains(got, "") { t.Error("the superseded passage is still searchable; a citation would point at text " + "that is no longer there") } if got := get(t, h, "/?q=wildebeest").Body.String(); !strings.Contains(got, "") { t.Error("the edited passage was not indexed") } } // The paste box is one more document, and files no longer overwrite it. // // This was real data loss: a file in the same submission silently // replaced whatever had been typed, and the page said "Stored, chunked // and indexed." With one document per file the conflict is gone — // everything submitted is stored, and the title field titles the paste. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) `if body != ""` → `if body != "" && len(files) == 0`, which is // what the old overwrite amounted to → "a paste submitted // alongside 2 files stored 2 documents, want 3: [costs revenue]" // (b) `Ingest(ctx, "paste", title, title, body)` → `Ingest(ctx, // "paste", title, "", body)`, so the title field stops titling // the paste → "the paste did not become a document under its // title: [The August meeting agreed the pangolin timetable. costs // revenue]" func TestThePasteBoxIsOneMoreDocumentAndIsNeverDiscarded(t *testing.T) { h := newApp(t) rec := submit(t, h, "Board minutes", "The August meeting agreed the pangolin timetable.", file{"revenue.txt", "Revenue was 100 across the first region we serve."}, file{"costs.txt", "Hosting costs fell after the migration completed."}, ) follow(t, h, rec) // The outcome is on /add, where the redirect lands; the list stayed // on the index when adding moved to its own page. list := get(t, h, "/").Body.String() titles := storedTitles(t, list) if len(titles) != 3 { t.Fatalf("a paste submitted alongside 2 files stored %d documents, want 3: %v", len(titles), titles) } if !slices.Contains(titles, "Board minutes") { t.Errorf("the paste did not become a document under its title: %v", titles) } if n := strings.Count(get(t, h, "/?q=pangolin").Body.String(), "rst-result-title"); n != 1 { t.Errorf("the pasted text is not searchable: it was discarded. %d results for a term "+ "only it contains, want 1", n) } // The files kept their own names rather than being titled by the // paste's title field. for _, want := range []string{"revenue", "costs"} { if !slices.Contains(titles, want) { t.Errorf("%q is not in the document list; the title field took a file's name: %v", want, titles) } } } // The scriptless path. AGENTS.md: every affordance works scriptless, and // multi-file upload is one — `multiple` is plain HTML and the handler // change is server-side, so a browser with JavaScript switched off must // store all five. // // Go cannot switch JavaScript off, so this asserts the two things that // make the claim true and then exercises the request they produce: the // rendered form is a plain multipart form with a `multiple` input named // "file", and a POST of several parts under that one name — which is // exactly what such a form submits — stores several documents. No part // of the request comes from drop.js; the harness builds it from the // attributes read off the page. // // MUTATION-TESTED 2026-09-04, each reverted: // // (a) removing `multiple` from index.html → "the rendered file input // is not `multiple`, so a scriptless chooser can offer only one // file" // (b) `enctype="multipart/form-data"` deleted from the form → "the // upload form is not multipart, so a scriptless browser would post // filenames and no file contents" // (c) `for _, hdr := range files` → `files[:1]` in createDocument → // "a scriptless multipart POST of 3 parts named "file" stored 1 // document" func TestTheScriptlessFormStoresEveryFileItSubmits(t *testing.T) { h := newApp(t) body := get(t, h, "/add").Body.String() i := strings.Index(body, `action="/documents"`) if i < 0 { t.Fatalf("no upload form on the page:\n%s", body) } j := strings.Index(body[i:], "") if j < 0 { t.Fatal("the upload form never closes") } form := body[i : i+j] if !strings.Contains(body[max(i-200, 0):i+j], `enctype="multipart/form-data"`) { t.Errorf("the upload form is not multipart, so a scriptless browser would post "+ "filenames and no file contents:\n%s", form) } input := regexp.MustCompile(`]*type="file"[^>]*>`).FindString(form) if !regexp.MustCompile(`\bmultiple\b`).MatchString(input) { t.Errorf("the rendered file input is not `multiple`, so a scriptless chooser can "+ "offer only one file: %s", input) } name := regexp.MustCompile(`name="([^"]*)"`).FindStringSubmatch(input) if name == nil { t.Fatalf("the file input has no name: %s", input) } // The request that form makes: several parts, one name, no script. var buf bytes.Buffer mw := multipart.NewWriter(&buf) parts := []file{ {"first.txt", "Revenue was 100 across the first region we serve."}, {"second.txt", "Revenue was 200 across the second region we serve."}, {"third.txt", "Revenue was 300 across the third region we serve."}, } for _, p := range parts { w, err := mw.CreateFormFile(name[1], p.name) if err != nil { t.Fatal(err) } if _, err := w.Write([]byte(p.body)); err != nil { t.Fatal(err) } } mw.Close() req := httptest.NewRequest(http.MethodPost, "/documents", &buf) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Origin", testOrigin) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if n := storedCount(t, h); n != len(parts) { t.Errorf("a scriptless multipart POST of %d parts named %q stored %d documents", len(parts), name[1], n) } if said := outcome(t, h, rec); !strings.Contains(said, "Stored 3 documents") { t.Errorf("the scriptless path does not report what it stored: %q", said) } }