83 lines
4.7 KiB
Markdown
83 lines
4.7 KiB
Markdown
# QA procedure: serve_uploaded_images
|
|
|
|
Package: `test` (black-box, new file `test/serve_uploaded_images_test.go`).
|
|
|
|
## Setup shared by every case
|
|
1. Call `validator.InitValidator()` once (harmless even though this handler
|
|
does no validation).
|
|
2. Create a fresh `uploadDir := t.TempDir()` per test/subtest. This handler
|
|
has no domain repository to fake — it serves real files from a real
|
|
directory on disk, which is standard for filesystem-serving code and is
|
|
not the "real SQLite file" / "live server" / "real SMTP" the project
|
|
convention rules out.
|
|
3. Build `staticimages.NewHandler(uploadDir)` (`api/handlers/staticimages`).
|
|
4. Mount it on a fresh `chi.NewRouter()` mirroring `api/router.go`'s shape
|
|
after this slice:
|
|
- `r.Mount("/static/uploadedImages", staticHandler)` — no
|
|
`jwtauth.Verifier` / `jwtauth.Authenticator` wrapping.
|
|
- `r.Mount("/", stubFrontend)` where `stubFrontend` is a trivial
|
|
`http.HandlerFunc` that writes a recognizable marker string, e.g.
|
|
`"SPA-INDEX-MARKER"`, with status 200 — standing in for the real SPA
|
|
fallback, just to prove routing order without needing the embedded
|
|
`dist/` build.
|
|
5. Helper to seed a file: `os.WriteFile(filepath.Join(uploadDir, name),
|
|
content, 0o644)`, creating parent directories with `os.MkdirAll` when a
|
|
subdirectory case needs one.
|
|
|
|
## HTTP checks
|
|
6. Write `4b9e2f1a-1111-....png`, `.jpeg`, `.webp` files with distinct
|
|
non-empty byte content. For each, `httptest.NewRequest("GET",
|
|
"/static/uploadedImages/"+name, nil)`, no cookie. Assert status `200`;
|
|
`Content-Type` equals the expected value; `Cache-Control` equals
|
|
`"public, no-cache"`; `X-Content-Type-Options` equals
|
|
`"nosniff"`; `Content-Security-Policy` header is absent; body bytes equal
|
|
what was written.
|
|
7. Write a `.svg` file whose content contains an inline `onload` handler
|
|
(content doesn't matter functionally, just needs to be non-empty and
|
|
distinguishable). GET it. Assert status `200`, `Content-Type` is
|
|
`"image/svg+xml"`, `Content-Security-Policy` equals `"default-src
|
|
'none'; style-src 'unsafe-inline'; sandbox"`, `X-Content-Type-Options` is
|
|
`"nosniff"`, body equals the exact SVG bytes (not sanitized/altered).
|
|
8. Write `notes.txt`. GET it. Assert status `200`, `Content-Type` is
|
|
`"application/octet-stream"`, no `Content-Security-Policy` header.
|
|
9. Don't create `missing.png`. GET `/static/uploadedImages/missing.png`.
|
|
Assert status `404`; assert the body does **not** contain
|
|
`"SPA-INDEX-MARKER"` (proves the static mount, not the frontend
|
|
fallback, produced the 404 — i.e. mount ordering in the test router
|
|
matches the required ordering in `api/router.go`).
|
|
10. Write a file, then `os.Remove` it before the request. GET its old name.
|
|
Assert status `404`.
|
|
11. `os.MkdirAll(filepath.Join(uploadDir, "nested"), 0o755)` and write a
|
|
file inside it. GET `/static/uploadedImages/nested`. Assert status
|
|
`404` (a directory is never listed or served).
|
|
12. Write one real file inside `uploadDir`, and separately write a marker
|
|
file (e.g. `"sentinel-outside"`) to a location outside `uploadDir` (a
|
|
second `t.TempDir()`, or a path one level above `uploadDir`) so a
|
|
successful escape would be detectable. For each of the traversal paths
|
|
`"../"+sentinelName`, `"..%2f"+sentinelName` (raw in the request path,
|
|
since `httptest.NewRequest` takes a literal target string and Go's
|
|
`net/http` mux/handler must not decode-then-escape), and a
|
|
triple-`..%2f` variant: send `GET /static/uploadedImages/<path>`.
|
|
Assert status `404` for every case, and assert the response body never
|
|
equals the sentinel file's content.
|
|
13. Write a `.webp` file. Send `httptest.NewRequest("HEAD", ...)`. Assert
|
|
status `200`, the same `Content-Type` / `Cache-Control` /
|
|
`X-Content-Type-Options` headers as the GET case, and an empty response
|
|
body (`rec.Body.Len() == 0`).
|
|
14. Repeat step 6's PNG case with explicitly no `Cookie` header set on the
|
|
request (already true in step 6, but assert this once more as its own
|
|
subtest/comment-free assertion block) and confirm status `200`, not a
|
|
3xx redirect and not `401`.
|
|
|
|
## Manual verification (not exercised by the Go test)
|
|
- Real end-to-end mount order: run `task dev` or a built `task build`
|
|
binary, confirm `GET /static/uploadedImages/<unknown>` returns 404 from
|
|
the real router (with the real embedded `dist/`), not `index.html`, since
|
|
the Go test above stands in a stub for the frontend rather than the
|
|
embedded SPA.
|
|
- Browser-level CSP enforcement: open an uploaded SVG containing a script
|
|
directly in a browser and confirm the CSP header actually blocks
|
|
execution (header presence is asserted by the test; browser enforcement
|
|
isn't).
|
|
- No SMTP involvement.
|