Files
diegosouzapw--omniroute/tests/unit/quota-key-models-route.test.ts
2026-07-13 13:39:12 +08:00

201 lines
8.7 KiB
TypeScript

/**
* tests/unit/quota-key-models-route.test.ts
*
* Task 2 (quota-share-v2) — source-level assertions for:
* GET /api/quota/keys/[id]/models
*
* Uses the same source-scan technique as:
* tests/unit/quota-groups-route.test.ts
* tests/unit/quota-pool-log-route.test.ts
*
* This allows the test to run with the Node.js native test runner without a
* Next.js / DOM setup, while providing strong structural coverage of:
* - Auth guard pattern (requireManagementAuth, 401 gate)
* - Error sanitization (buildErrorBody, no raw err.stack / err.message leaks)
* - Quota model resolution imports (resolveQuotaKeyScope, filterModelsToQuotaPools)
* - Response shape ({ models })
* - 404 on missing key (buildErrorBody(404, ...))
* - force-dynamic export
* - await params (Next 16 async params pattern)
* - DB imports sourced from @/lib/localDb
*
* The behavioral path (allowedQuotas → scope → filterModelsToQuotaPools → models)
* is covered by:
* - tests/unit/quota-catalog-filter.test.ts — filterModelsToQuotaPools unit tests
* - tests/unit/quota-key-resolve.test.ts — resolveQuotaKeyScope unit tests
* - tests/unit/quota-group-scope.test.ts — group-scoped pool resolution
* Re-testing the same logic inline here would duplicate DB-setup boilerplate
* without additional signal. Source-scan matches the project's established
* convention for route test files (quota-groups-route.test.ts, etc.).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const ROUTE_PATH = join(ROOT, "src/app/api/quota/keys/[id]/models/route.ts");
const src = readFileSync(ROUTE_PATH, "utf8");
// ── Auth guard ────────────────────────────────────────────────────────────────
test("quota/keys/[id]/models: imports requireManagementAuth", () => {
assert.ok(
src.includes("requireManagementAuth"),
"route must import and call requireManagementAuth",
);
});
test("quota/keys/[id]/models: GET calls requireManagementAuth before data access", () => {
const getIdx = src.indexOf("export async function GET");
assert.ok(getIdx >= 0, "GET handler must exist");
const getBody = src.slice(getIdx);
const authIdx = getBody.indexOf("requireManagementAuth(request)");
const dataIdx = getBody.indexOf("getApiKeyById(");
assert.ok(authIdx >= 0, "requireManagementAuth call must be present in GET handler");
assert.ok(dataIdx >= 0, "getApiKeyById call must be present in GET handler");
assert.ok(authIdx < dataIdx, "auth check must come before getApiKeyById call");
});
test("quota/keys/[id]/models: GET returns early when authError is truthy", () => {
const getIdx = src.indexOf("export async function GET");
const getBody = src.slice(getIdx);
assert.ok(
getBody.includes("if (authError) return authError"),
"GET must return authError immediately — 401 without auth",
);
});
// ── Error sanitization ────────────────────────────────────────────────────────
test("quota/keys/[id]/models: imports buildErrorBody from @omniroute/open-sse/utils/error", () => {
assert.ok(
src.includes("buildErrorBody"),
"route must use buildErrorBody — Hard Rule #12",
);
assert.ok(
src.includes("@omniroute/open-sse/utils/error"),
"route must import buildErrorBody from @omniroute/open-sse/utils/error",
);
});
test("quota/keys/[id]/models: does NOT leak raw err.stack in response", () => {
assert.ok(!src.includes("err.stack"), "route must not leak err.stack in response body");
});
test("quota/keys/[id]/models: does NOT leak raw err.message outside buildErrorBody", () => {
// The only use of err.message must be inside the buildErrorBody(...) call, never
// passed raw to NextResponse.json() or similar.
const rawMessageLeak = /NextResponse\.json\s*\(\s*err\.message/.test(src);
assert.ok(!rawMessageLeak, "route must not pass raw err.message to NextResponse.json");
});
// ── Quota resolution imports ──────────────────────────────────────────────────
test("quota/keys/[id]/models: imports resolveQuotaKeyScope from @/lib/quota/quotaKey", () => {
assert.ok(
src.includes("resolveQuotaKeyScope"),
"route must call resolveQuotaKeyScope to compute pool scope",
);
assert.ok(
src.includes("@/lib/quota/quotaKey"),
"route must import resolveQuotaKeyScope from @/lib/quota/quotaKey",
);
});
test("quota/keys/[id]/models: imports filterModelsToQuotaPools from @/lib/quota/quotaCombos", () => {
assert.ok(
src.includes("filterModelsToQuotaPools"),
"route must call filterModelsToQuotaPools to filter combo candidates",
);
assert.ok(
src.includes("@/lib/quota/quotaCombos"),
"route must import filterModelsToQuotaPools from @/lib/quota/quotaCombos",
);
});
test("quota/keys/[id]/models: passes scope.poolSlugs to filterModelsToQuotaPools", () => {
assert.ok(
src.includes("scope.poolSlugs"),
"route must pass scope.poolSlugs to filterModelsToQuotaPools (same as catalog.ts approach)",
);
});
// ── DB imports from localDb ───────────────────────────────────────────────────
test("quota/keys/[id]/models: imports getApiKeyById from @/lib/localDb", () => {
assert.ok(src.includes("getApiKeyById"), "route must call getApiKeyById");
assert.ok(
src.includes("@/lib/localDb"),
"route must import from @/lib/localDb (not from @/lib/db/apiKeys directly)",
);
});
test("quota/keys/[id]/models: imports getCombos from @/lib/localDb", () => {
assert.ok(src.includes("getCombos"), "route must call getCombos to build combo candidates");
});
// ── Response shape ────────────────────────────────────────────────────────────
test("quota/keys/[id]/models: GET returns { models } shape", () => {
assert.ok(
src.includes("{ models }") || src.includes("{models}") || src.includes("models:"),
"GET must return { models } in the response body",
);
});
test("quota/keys/[id]/models: maps filtered combos to model id strings (.map(m => m.id))", () => {
assert.ok(
src.includes(".map(") && src.includes("m.id"),
"route must map filtered combo entries to their id strings",
);
});
// ── 404 on missing key ────────────────────────────────────────────────────────
test("quota/keys/[id]/models: returns buildErrorBody(404, ...) when key not found", () => {
assert.ok(
src.includes("buildErrorBody(404"),
"route must use buildErrorBody(404, ...) when the API key is not found",
);
assert.ok(src.includes("404"), "route must return HTTP 404 on missing key");
});
// ── force-dynamic ─────────────────────────────────────────────────────────────
test("quota/keys/[id]/models: has dynamic = 'force-dynamic' export", () => {
assert.ok(
src.includes('dynamic = "force-dynamic"') || src.includes("dynamic = 'force-dynamic'"),
"route must export dynamic = 'force-dynamic'",
);
});
// ── Next 16 async params ──────────────────────────────────────────────────────
test("quota/keys/[id]/models: reads id via await params (Next 16 pattern)", () => {
assert.ok(
src.includes("await params"),
"route must use await params — Next 16 async params pattern",
);
});
test("quota/keys/[id]/models: params typed as Promise<{ id: string }>", () => {
assert.ok(
src.includes("Promise<") && src.includes("id: string"),
"params type must be Promise<{ id: string }> — matching groups/[id] pattern",
);
});
// ── exports GET ───────────────────────────────────────────────────────────────
test("quota/keys/[id]/models: exports GET handler", () => {
assert.ok(
src.includes("export async function GET"),
"route must export the GET handler",
);
});