mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-11 01:13:02 +03:00
* test(infra): retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) Two shards on release/v3.8.51 went red in one day with the same signature — "ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only .github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass alone and on re-run: the cleanup races something still writing into the directory (SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner the window opens. 1154 test files do their own cleanup with fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries. One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record): every rm / rmSync / rmdirSync option object with `recursive: true` and no `maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292 files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included. Only the option object changes: no call site, assertion or import is touched. Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292 files; a random 20-file sample runs green (quota-redis-store hangs identically on the untouched tree — it needs a Redis on localhost, an environment matter). The four unit shards on this PR are the full run. * fix(quality): let check-forgotten-sibling-tests read a 1,000-file diff The gate shells out to `git diff` through execFileSync with Node's default 1 MB maxBuffer; the 1,292-file codemod in this PR is the first diff large enough to overflow it, and the gate died with `spawnSync git ENOBUFS` before comparing anything. 64 MB is far above any real PR and costs nothing when unused.
350 lines
14 KiB
TypeScript
350 lines
14 KiB
TypeScript
/**
|
|
* Regression guard for the T06 route-validation gate on the proxy-subscriptions
|
|
* management routes.
|
|
*
|
|
* `POST /api/v1/management/proxy-subscriptions` and
|
|
* `PATCH /api/v1/management/proxy-subscriptions/:id` used to hand-roll their body
|
|
* parsing (a local `parsePayload()` / inline field-by-field checks) instead of a
|
|
* Zod schema, which `scripts/check/check-route-validation.mjs` flags as an
|
|
* unvalidated `request.json()` usage. The fix swaps both routes to
|
|
* `proxySubscriptionCreateSchema` / `proxySubscriptionUpdateSchema`
|
|
* (`src/lib/proxySubscription/schema.ts`) applied via `.safeParse()`.
|
|
*
|
|
* These tests pin the EXACT pre-existing acceptance/rejection rules, error
|
|
* messages, and `{ error: string }` envelope shape — the schema swap must be a
|
|
* pure refactor, not a behavior change.
|
|
*
|
|
* DB/auth setup mirrors tests/unit/api-malformed-json-400.test.ts: a temp
|
|
* DATA_DIR with no configured password means requireManagementAuth() is a
|
|
* no-op, so the handlers run unauthenticated.
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-sub-route-"));
|
|
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
|
const ORIGINAL_API_KEY_SECRET = process.env.API_KEY_SECRET;
|
|
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
|
|
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "proxy-sub-route-test-secret";
|
|
delete process.env.INITIAL_PASSWORD; // ensure auth is NOT required
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const collectionRoute =
|
|
await import("../../src/app/api/v1/management/proxy-subscriptions/route.ts");
|
|
const itemRoute = await import("../../src/app/api/v1/management/proxy-subscriptions/[id]/route.ts");
|
|
|
|
function jsonRequest(url: string, body: unknown, method = "POST"): Request {
|
|
return new Request(url, {
|
|
method,
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
function malformedJsonRequest(url: string, method = "POST"): Request {
|
|
return new Request(url, {
|
|
method,
|
|
headers: { "content-type": "application/json" },
|
|
body: "not-json",
|
|
});
|
|
}
|
|
|
|
async function createValidSubscription(name: string) {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name,
|
|
url: `https://example.com/${name}`,
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
assert.equal(res.status, 201, "fixture creation must succeed");
|
|
return (await res.json()) as { id: string };
|
|
}
|
|
|
|
test.after(() => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
|
|
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
|
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
|
|
|
if (ORIGINAL_API_KEY_SECRET === undefined) delete process.env.API_KEY_SECRET;
|
|
else process.env.API_KEY_SECRET = ORIGINAL_API_KEY_SECRET;
|
|
|
|
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
|
|
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// POST /api/v1/management/proxy-subscriptions
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
test("POST proxy-subscriptions — valid body still creates (201), unregressed happy path", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "my-sub",
|
|
url: "https://example.com/sub.txt",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 201);
|
|
const body = (await res.json()) as { name?: string; url?: string; mode?: string };
|
|
assert.equal(body.name, "my-sub");
|
|
assert.equal(body.url, "https://example.com/sub.txt");
|
|
assert.equal(body.mode, "global", "mode defaults to 'global' when omitted");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — trims name/url and coerces unknown mode to 'global'", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: " padded-name ",
|
|
url: " https://example.com/padded ",
|
|
mode: "not-a-real-mode",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 201);
|
|
const body = (await res.json()) as { name?: string; url?: string; mode?: string };
|
|
assert.equal(body.name, "padded-name");
|
|
assert.equal(body.url, "https://example.com/padded");
|
|
assert.equal(body.mode, "global");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — malformed JSON body returns 400 with the original message", async () => {
|
|
const req = malformedJsonRequest("http://localhost/api/v1/management/proxy-subscriptions");
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "Invalid JSON body");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — valid-JSON non-object (string) body returns 400 'Invalid JSON body'", async () => {
|
|
// A bare JSON array is still `typeof === "object"` in JS (matching the original
|
|
// `typeof body !== "object"` guard), so it falls through to the missing-name
|
|
// check instead — see the array-body test below for that path. A primitive
|
|
// (string/number/boolean) is the one JSON shape that actually trips this guard.
|
|
const req = jsonRequest(
|
|
"http://localhost/api/v1/management/proxy-subscriptions",
|
|
"just-a-string"
|
|
);
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "Invalid JSON body");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — a JSON array body is an 'object' in JS, so it hits 'name is required' (not 'Invalid JSON body')", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", [1, 2, 3]);
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "name is required");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — missing name returns 400 'name is required'", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
url: "https://example.com/sub.txt",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "name is required");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — blank name (whitespace only) returns 400 'name is required'", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: " ",
|
|
url: "https://example.com/sub.txt",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "name is required");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — missing url returns 400 'url is required' (checked after name)", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "my-sub",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "url is required");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — mode 'rule' without ruleProviders returns 400", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "my-sub",
|
|
url: "https://example.com/sub.txt",
|
|
mode: "rule",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "ruleProviders is required when mode is 'rule'");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — mode 'rule' with an empty ruleProviders array returns 400", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "my-sub",
|
|
url: "https://example.com/sub.txt",
|
|
mode: "rule",
|
|
ruleProviders: [],
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "ruleProviders is required when mode is 'rule'");
|
|
});
|
|
|
|
test("POST proxy-subscriptions — mode 'rule' with ruleProviders succeeds (201)", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "rule-sub",
|
|
url: "https://example.com/rule-sub.txt",
|
|
mode: "rule",
|
|
ruleProviders: ["openai", 42, "anthropic"],
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 201);
|
|
const body = (await res.json()) as { mode?: string; ruleProviders?: string[] };
|
|
assert.equal(body.mode, "rule");
|
|
assert.deepEqual(
|
|
body.ruleProviders,
|
|
["openai", "anthropic"],
|
|
"non-string ruleProviders entries are filtered out, matching the original parser"
|
|
);
|
|
});
|
|
|
|
test("POST proxy-subscriptions — invalid updateIntervalMinutes silently falls back to 60", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "interval-sub",
|
|
url: "https://example.com/interval-sub.txt",
|
|
updateIntervalMinutes: "not-a-number",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 201);
|
|
const body = (await res.json()) as { updateIntervalMinutes?: number };
|
|
assert.equal(body.updateIntervalMinutes, 60);
|
|
});
|
|
|
|
test("POST proxy-subscriptions — enabled must be exactly `true`, not truthy", async () => {
|
|
const req = jsonRequest("http://localhost/api/v1/management/proxy-subscriptions", {
|
|
name: "enabled-sub",
|
|
url: "https://example.com/enabled-sub.txt",
|
|
enabled: "yes",
|
|
});
|
|
const res = await collectionRoute.POST(req);
|
|
|
|
assert.equal(res.status, 201);
|
|
const body = (await res.json()) as { enabled?: boolean };
|
|
assert.equal(body.enabled, false);
|
|
});
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
// PATCH /api/v1/management/proxy-subscriptions/:id
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
test("PATCH proxy-subscriptions/:id — valid partial body updates (200), unregressed happy path", async () => {
|
|
const fixture = await createValidSubscription("patch-target");
|
|
const req = jsonRequest(
|
|
`http://localhost/api/v1/management/proxy-subscriptions/${fixture.id}`,
|
|
{ name: "renamed" },
|
|
"PATCH"
|
|
);
|
|
const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: fixture.id }) });
|
|
|
|
assert.equal(res.status, 200);
|
|
const body = (await res.json()) as { name?: string };
|
|
assert.equal(body.name, "renamed");
|
|
});
|
|
|
|
test("PATCH proxy-subscriptions/:id — wrong-typed fields are ignored, not rejected", async () => {
|
|
const fixture = await createValidSubscription("patch-wrongtype");
|
|
const req = jsonRequest(
|
|
`http://localhost/api/v1/management/proxy-subscriptions/${fixture.id}`,
|
|
{ name: "kept", updateIntervalMinutes: "not-a-number", enabled: "yes" },
|
|
"PATCH"
|
|
);
|
|
const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: fixture.id }) });
|
|
|
|
assert.equal(res.status, 200, "unknown-typed fields must be silently dropped, not a 400");
|
|
const body = (await res.json()) as {
|
|
name?: string;
|
|
updateIntervalMinutes?: number;
|
|
enabled?: boolean;
|
|
};
|
|
assert.equal(body.name, "kept");
|
|
assert.equal(body.updateIntervalMinutes, 60, "untouched — non-number was ignored, not coerced");
|
|
assert.equal(body.enabled, false, "untouched — non-boolean was ignored");
|
|
});
|
|
|
|
test("PATCH proxy-subscriptions/:id — malformed JSON body returns 400 'Invalid JSON body'", async () => {
|
|
const fixture = await createValidSubscription("patch-malformed");
|
|
const req = malformedJsonRequest(
|
|
`http://localhost/api/v1/management/proxy-subscriptions/${fixture.id}`,
|
|
"PATCH"
|
|
);
|
|
const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: fixture.id }) });
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "Invalid JSON body");
|
|
});
|
|
|
|
test("PATCH proxy-subscriptions/:id — valid-JSON non-object body returns 400 'Invalid JSON body'", async () => {
|
|
const fixture = await createValidSubscription("patch-nonobject");
|
|
const req = jsonRequest(
|
|
`http://localhost/api/v1/management/proxy-subscriptions/${fixture.id}`,
|
|
"just-a-string",
|
|
"PATCH"
|
|
);
|
|
const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: fixture.id }) });
|
|
|
|
assert.equal(res.status, 400);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "Invalid JSON body");
|
|
});
|
|
|
|
test("PATCH proxy-subscriptions/:id — a JSON array body is an 'object' in JS, so it's a no-op update (200), not 400", async () => {
|
|
const fixture = await createValidSubscription("patch-arraybody");
|
|
const req = jsonRequest(
|
|
`http://localhost/api/v1/management/proxy-subscriptions/${fixture.id}`,
|
|
[1, 2, 3],
|
|
"PATCH"
|
|
);
|
|
const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: fixture.id }) });
|
|
|
|
assert.equal(
|
|
res.status,
|
|
200,
|
|
"matches the original inline parser: no typed field matches, no error"
|
|
);
|
|
const body = (await res.json()) as { name?: string };
|
|
assert.equal(body.name, "patch-arraybody", "name is unchanged — the array had no usable fields");
|
|
});
|
|
|
|
test("PATCH proxy-subscriptions/:id — unknown id still 404s past body validation", async () => {
|
|
const req = jsonRequest(
|
|
"http://localhost/api/v1/management/proxy-subscriptions/does-not-exist",
|
|
{ name: "whatever" },
|
|
"PATCH"
|
|
);
|
|
const res = await itemRoute.PATCH(req, { params: Promise.resolve({ id: "does-not-exist" }) });
|
|
|
|
assert.equal(res.status, 404);
|
|
const body = (await res.json()) as { error?: string };
|
|
assert.equal(body.error, "Subscription not found");
|
|
});
|