Files
captoys/webapp/tests/post-form.test.ts
dckc ff2e7841f2 capsite/webapp: postForm, a multipart POST that can echo a cookie into a field
Some sites check a double-submit token: the session value sent as a
cookie and again in the form body. postForm(fields, echoCookies) builds
a multipart body and copies the named cookies (from the header store,
at send time) into fields, so the caller never sees the value.

It returns a Reply instead of throwing on non-OK: status, content type,
content-disposition, the redirect location with its query stripped, and
the body as bytes (the caller decodes, e.g. per a charset the body
declares). Redirects are not followed, so an expired session shows up
as a 3xx. Set-Cookie is not passed back. Errors name keys, never values.

Generated-by: Claude Opus 5.5
2026-10-11 14:22:09 -05:00

118 lines
4.4 KiB
TypeScript

import { env } from "cloudflare:workers";
import { afterEach, describe, expect, it, vi } from "vitest";
import worker, { type WebAppInit } from "../src/index.js";
type TestEnv = {
WEBAPP_DO: DurableObjectNamespace<import("../src/index.js").WebAppDo>;
MAP_STORE_DO: DurableObjectNamespace<import("../src/index.js").MapStoreDo>;
};
const testEnv = env as unknown as TestEnv;
const URL_ = "https://site.example/app/export/7/data";
type Seen = { url: string; redirect?: string; contentType: string | null; form: Record<string, string> };
/** each outbound fetch, its form fields read back; answers with `res()` */
function spyForms(res: () => Response): Seen[] {
const seen: Seen[] = [];
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
const req = new Request(input as string, init as RequestInit);
const form = Object.fromEntries([...(await req.formData()).entries()].map(([k, v]) => [k, String(v)]));
seen.push({ url: req.url, redirect: init?.redirect, contentType: req.headers.get("content-type"), form });
return res();
});
return seen;
}
afterEach(() => {
vi.restoreAllMocks();
});
const mint = async (init: WebAppInit) => {
const res = await worker.fetch(
new Request("https://webapp.example/", {
method: "POST",
body: JSON.stringify(init),
headers: { "content-type": "application/json" },
}),
testEnv,
);
const { id, storeId } = (await res.json()) as { id: string; storeId: string };
return { id, storeId, instance: testEnv.WEBAPP_DO.get(testEnv.WEBAPP_DO.idFromString(id)) };
};
/** the failure of a call: what it threw; "" if it succeeds */
async function errorOf(p: Promise<unknown>): Promise<string> {
try {
await p;
return "";
} catch (err) {
return String(err);
}
}
describe("postForm", () => {
it("echoes a cookie from the header store into a form field; the holder never sees it", async () => {
const seen = spyForms(
() =>
new Response("col1,col2", {
headers: {
"content-type": "text/csv",
"content-disposition": 'attachment;filename="x.csv"',
"set-cookie": "sess=next; HttpOnly",
},
}),
);
const { instance } = await mint({ url: URL_, headers: { cookie: "a=1; tok=s3cret; b=2" } });
using app = await instance.handle();
const reply = await app.query({ since: "2026-07-13\x1f" }).postForm({ kind: "all" }, { csrf: "tok" });
expect(seen).toEqual([
{
url: `${URL_}?since=2026-07-13%1F`,
redirect: "manual",
contentType: expect.stringMatching(/^multipart\/form-data; boundary=/),
form: { kind: "all", csrf: "s3cret" },
},
]);
expect(reply).toEqual({
status: 200,
contentType: "text/csv",
contentDisposition: 'attachment;filename="x.csv"',
body: new TextEncoder().encode("col1,col2"),
});
});
it("does not follow a redirect: the 3xx comes back, location query-stripped", async () => {
spyForms(() => new Response(null, { status: 302, headers: { location: "/login?next=s3cret#x" } }));
const { instance } = await mint({ url: URL_, headers: { cookie: "tok=t" } });
using app = await instance.handle();
const reply = await app.postForm({}, { csrf: "tok" });
expect(reply).toEqual({ status: 302, contentType: "", location: "https://site.example/login", body: new Uint8Array() });
});
it("refuses, naming no values: a missing cookie, a field set twice, a content-type header", async () => {
spyForms(() => new Response("ok"));
const { instance } = await mint({ url: URL_, headers: { cookie: "tok=s3cret" } });
using app = await instance.handle();
expect(await errorOf(app.postForm({}, { csrf: "nope" }))).toMatch(/no nope cookie to echo/);
const twice = await errorOf(app.postForm({ csrf: "mine" }, { csrf: "tok" }));
expect(twice).toMatch(/contradicts on csrf/);
expect(twice).not.toMatch(/s3cret|mine/);
const typed = app.withHeader("content-type", "text/plain");
expect(await errorOf(typed.postForm())).toMatch(/content-type is set by the form/);
});
it("rotating the store's cookie changes what is echoed", async () => {
const seen = spyForms(() => new Response("ok"));
const { storeId, instance } = await mint({ url: URL_, headers: { cookie: "tok=one" } });
using app = await instance.handle();
await app.postForm({}, { csrf: "tok" });
await worker.fetch(
new Request(`https://webapp.example/store/${storeId}/cookie`, { method: "PUT", body: JSON.stringify("tok=two") }),
testEnv,
);
await app.postForm({}, { csrf: "tok" });
expect(seen.map((s) => s.form.csrf)).toEqual(["one", "two"]);
});
});