Files
OmniRoute/tests/unit/route-body-validation-t06.test.ts
Diego Rodrigues de Sa e Souza 24bdae29ca fix(api): validate request bodies with Zod in 4 routes — restores the t06 gate (#9779)
The release-green verdict (#9737) lists check:route-validation:t06 as a HARD
failure and it is STILL red on the current tip: four routes call
request.json() and hand-roll `typeof x === "string"` checks instead of using
Zod, which Hard Rule #7 requires and the gate enforces (it scans source and
has no allowlist).

- src/app/api/plugins/marketplace/install (#9445): InstallBodySchema; the
  400 'Missing or invalid name field' response is preserved verbatim.
- src/app/api/services/dario/admin/accounts (#8523): DeleteAccountBodySchema
  for the optional { alias } DELETE body; query-param path untouched.
- src/app/api/services/dario/admin/login-start (#8523): LoginStartBodySchema;
  trimming now happens in the schema, so the forward body is unchanged.
- src/app/api/services/dario/admin/import-from-omniroute (#8523):
  ImportBodySchema for connectionId/alias; invalid shapes fall back to the
  same 'connectionId is required' 400 as before.

All four keep their exact status codes and messages — this is a validation
mechanism swap, not a contract change (plugins route suite still 33/33).

Adds tests/unit/route-body-validation-t06.test.ts, which runs the gate's own
rule inside the unit suite so the next such route fails on ITS OWN PR instead
of surfacing weeks later in a base-red sweep. Guard verified by mutation:
renaming .safeParse( in one route makes it fail (1 fail), restored from a
pre-probe copy.

Gates: route-validation:t06, file-size, test-discovery, mutation-test-coverage,
dead-code exit 0; typecheck:core clean; eslint clean.

Refs #9737

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-08 10:35:18 -03:00

55 lines
2.0 KiB
TypeScript

/**
* Guard for the t06:route-validation gate (Hard Rule #7 — always validate inputs
* with Zod schemas).
*
* The gate (scripts/check/check-route-validation.mjs) is a source scan: any
* `route.ts` under src/app/api that calls `request.json()` must also call
* `validateBody()` or `.safeParse()`. It has NO allowlist, so a route that
* hand-rolls `typeof x === "string"` checks passes review but fails CI — which
* is exactly how four routes (#9445 marketplace install, #8523's three Dario
* admin routes) landed on release/v3.8.50 and kept the branch out of
* release-green (#9737).
*
* This test runs the same rule inside the unit suite so the violation surfaces
* on the PR that introduces it, instead of on the next base-red sweep.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..");
const API_ROOT = path.join(REPO_ROOT, "src", "app", "api");
function collectRouteFiles(dir: string): string[] {
const files: string[] = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectRouteFiles(full));
} else if (entry.isFile() && entry.name === "route.ts") {
files.push(full);
}
}
return files;
}
test("every API route reading request.json() validates it with Zod (t06)", () => {
const offenders: string[] = [];
for (const file of collectRouteFiles(API_ROOT)) {
const source = fs.readFileSync(file, "utf8");
if (!/request\.json\s*\(/.test(source)) continue;
if (/\bvalidateBody\s*\(/.test(source) || /\.safeParse\s*\(/.test(source)) continue;
offenders.push(path.relative(REPO_ROOT, file));
}
assert.deepEqual(
offenders,
[],
`routes call request.json() without validateBody()/.safeParse() — hand-rolled ` +
`typeof checks do not satisfy Hard Rule #7 and fail the t06 CI gate:\n ` +
offenders.join("\n ")
);
});