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, "
]*>`).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(`