97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import {
|
|
configureStorage,
|
|
createSignedFileToken,
|
|
getStore,
|
|
safeObjectKey,
|
|
serveStoredFile,
|
|
upload,
|
|
verifySignedFileToken,
|
|
} from "../src/index.ts";
|
|
import { UPLOAD_RUNTIME } from "../src/runtime.ts";
|
|
|
|
test("browser upload response parse failures emit a diagnostic", () => {
|
|
expect(UPLOAD_RUNTIME).toContain("upload response was not valid JSON");
|
|
expect(UPLOAD_RUNTIME).not.toContain("catch (e2) {}");
|
|
});
|
|
|
|
function configure() {
|
|
configureStorage(
|
|
{ stores: { public: { driver: "local", access: "public", dir: "files" } } },
|
|
mkdtempSync(join(tmpdir(), "wrnexus-upload-")),
|
|
);
|
|
}
|
|
|
|
test("rejects unsafe upload prefixes before writing", async () => {
|
|
configure();
|
|
const form = new FormData();
|
|
form.set("file", new File(["hello"], "hello.txt", { type: "text/plain" }));
|
|
const request = new Request("http://local/upload", { method: "POST", body: form });
|
|
await expect(upload("public", request, { prefix: "../escape" })).rejects.toThrow(
|
|
"unsafe upload prefix",
|
|
);
|
|
});
|
|
|
|
test("public active content is attachment-only and cannot be MIME-sniffed", async () => {
|
|
configure();
|
|
await getStore("public").driver.put(
|
|
"safe/page.html",
|
|
new TextEncoder().encode("<script>x</script>"),
|
|
{
|
|
contentType: "text/html",
|
|
},
|
|
);
|
|
const response = await serveStoredFile("/__wrnexus/uploads/public/safe/page.html");
|
|
expect(response?.headers.get("x-content-type-options")).toBe("nosniff");
|
|
expect(response?.headers.get("content-disposition")).toStartWith("attachment");
|
|
});
|
|
|
|
test("safe object keys discard traversal-like prefixes", () => {
|
|
expect(safeObjectKey("report.pdf", "../../private/reports")).toMatch(/^private\/reports\//);
|
|
expect(safeObjectKey("report.pdf", "../..")).toMatch(/^uploads\//);
|
|
});
|
|
|
|
test("signed file tokens reject tampering and expired payloads", async () => {
|
|
const secret = "a-long-test-secret-for-files";
|
|
const token = await createSignedFileToken(
|
|
{ store: "private", key: "reports/a.pdf", expiresAt: 2_000 },
|
|
secret,
|
|
);
|
|
expect(await verifySignedFileToken(token, secret, 1_000)).toMatchObject({
|
|
store: "private",
|
|
key: "reports/a.pdf",
|
|
});
|
|
expect(await verifySignedFileToken(`${token}x`, secret, 1_000)).toBeNull();
|
|
expect(await verifySignedFileToken(token, secret, 2_000)).toBeNull();
|
|
});
|
|
|
|
test("upload scanning rejects unsafe bytes before storage", async () => {
|
|
const form = new FormData();
|
|
form.set("file", new File(["virus"], "bad.txt", { type: "text/plain" }));
|
|
await expect(
|
|
upload("public", new Request("http://test/upload", { method: "POST", body: form }), {
|
|
scan: async () => ({ safe: false, scanner: "test-av", reason: "signature" }),
|
|
}),
|
|
).rejects.toMatchObject({ status: 422 });
|
|
expect(await getStore("public").driver.get("bad.txt")).toBeNull();
|
|
});
|
|
|
|
test("post-storage processor failure rolls back the object", async () => {
|
|
const form = new FormData();
|
|
form.set("file", new File(["image"], "photo.png", { type: "image/png" }));
|
|
let key = "";
|
|
await expect(
|
|
upload("public", new Request("http://test/upload", { method: "POST", body: form }), {
|
|
afterStore(file) {
|
|
key = file.key;
|
|
throw new Error("transform failed");
|
|
},
|
|
}),
|
|
).rejects.toMatchObject({ status: 422 });
|
|
expect(key).not.toBe("");
|
|
expect(await getStore("public").driver.get(key)).toBeNull();
|
|
});
|