From f06df5fd71fd52dfc2881396c39a21370b94ec93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 23 Oct 2026 11:25:36 +0200 Subject: [PATCH] test(api): count parseJsonBody refusals in the issue-list guard A route that reads its body through parseJsonBody has no `if (!parsed.success)` block, so the guard lost sight of every migrated site: its floor of 200 failed at 139 and the token inventory reported the coach message feedback entry as stale while that route still answers `feedback.body.invalid` in `error`, now through the `message` option. Each parseJsonBody call is now a site of its own, carrying the issue list by construction unless it names a status other than 422, and its `message` option is read as the refused string by the token sweep. --- .../zod-refusal-issue-list-guard.test.ts | 70 +++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/src/__tests__/zod-refusal-issue-list-guard.test.ts b/src/__tests__/zod-refusal-issue-list-guard.test.ts index 242353000..96fce35f2 100644 --- a/src/__tests__/zod-refusal-issue-list-guard.test.ts +++ b/src/__tests__/zod-refusal-issue-list-guard.test.ts @@ -16,7 +16,10 @@ * every `if (!.success)` block whose identifier came from a `safeParse` * in that file is read on its own, and only the blocks that answer 422 are * judged — a 400, a 401 or a 403 raised off a parse result is a different - * refusal and is left alone. + * refusal and is left alone. A route that reads its body through + * `parseJsonBody` has no such block: each call is a site of its own, carrying + * the issue list by construction, and its `message` option is read as the + * refused string by the token sweep below. * * The second half of the same audit item is the dotted token in `error`, and * it is checked by its own sweep at the bottom of this file rather than by the @@ -94,11 +97,58 @@ interface RefusalSite { carriesIssues: boolean; } +/** + * Every `parseJsonBody(…)` call in a file, as its paren-balanced source text + * with the line it starts on. The helper reads, validates and refuses in one + * call (`src/lib/api/body.ts`), so a route that uses it has no + * `if (!x.success)` block for the matcher above to find; its refusal is the + * helper's envelope, which always carries `details.issues`. + */ +function parseJsonBodyCalls(src: string): { text: string; line: number }[] { + const calls: { text: string; line: number }[] = []; + for (const m of src.matchAll(/\bparseJsonBody\s*\(/g)) { + const start = m.index ?? 0; + let depth = 0; + let end = src.length; + for (let j = start + m[0].length - 1; j < src.length; j++) { + if (src[j] === "(") depth++; + else if (src[j] === ")") { + depth--; + if (depth === 0) { + end = j + 1; + break; + } + } + } + calls.push({ + text: src.slice(start, end), + line: src.slice(0, start).split("\n").length, + }); + } + return calls; +} + +/** The status a `parseJsonBody` call refuses with: its `status` option, else 422. */ +function parseJsonBodyStatus(call: string): number { + const m = /\bstatus:\s*(\d{3})\b/.exec(call); + return m ? Number(m[1]) : 422; +} + /** Every 422 refusal decided by a `safeParse` result, across the route tree. */ function scanRefusalSites(): RefusalSite[] { const sites: RefusalSite[] = []; for (const file of routeFiles(API_ROOT)) { const src = readFileSync(file, "utf8"); + // A `parseJsonBody` refusal is a 422 with the issue list unless the call + // names another status, which is then a different refusal, as below. + for (const call of parseJsonBodyCalls(src)) { + if (parseJsonBodyStatus(call.text) !== 422) continue; + sites.push({ + route: relative(process.cwd(), file).split(sep).join("/"), + line: call.line, + carriesIssues: true, + }); + } // Identifiers bound to a parse result. Both call styles are in the tree: // `schema.safeParse(body)` and the standalone `z.safeParse(schema, body)`. const parsed = new Set(); @@ -228,12 +278,20 @@ function tokenRefusals(): string[] { /apiValidationError\(\s*"([^"]+)"/g, /new HttpError\(\s*422\s*,\s*"([^"]+)"/g, ]; + const refused: string[] = []; for (const shape of shapes) { - for (const m of src.matchAll(shape)) { - if (!DOTTED_TOKEN.test(m[1])) continue; - const key = `${route} ${m[1]}`; - if (!found.includes(key)) found.push(key); - } + for (const m of src.matchAll(shape)) refused.push(m[1]); + } + // `parseJsonBody`'s `message` option is the 422's `error` string. + for (const call of parseJsonBodyCalls(src)) { + if (parseJsonBodyStatus(call.text) !== 422) continue; + const m = /\bmessage:\s*"([^"]+)"/.exec(call.text); + if (m) refused.push(m[1]); + } + for (const text of refused) { + if (!DOTTED_TOKEN.test(text)) continue; + const key = `${route} ${text}`; + if (!found.includes(key)) found.push(key); } } return found;