test(integration): cover quota REST routes + error sanitization (B/F8)

This commit is contained in:
diegosouzapw
2026-05-28 07:50:03 -03:00
parent 0107beb86b
commit 37e6570bdb
6 changed files with 1310 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
/**
* Integration tests: /api/quota/plans CRUD endpoints
*
* Verifies:
* - GET /api/quota/plans returns catalog + DB plans merged
* - GET /api/quota/plans/[connectionId] returns resolved plan
* - PUT /api/quota/plans/[connectionId] upserts manual override + audit event
* - DELETE /api/quota/plans/[connectionId] clears override (reverts to auto)
* - Error responses never leak stack traces (Hard Rule #12 / B25)
*
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-plans-crud-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-plans-secret";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const plansRoute = await import("../../src/app/api/quota/plans/route.ts");
const planIdRoute = await import("../../src/app/api/quota/plans/[connectionId]/route.ts");
async function enableManagementAuth() {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ---------------------------------------------------------------------------
// GET /api/quota/plans
// ---------------------------------------------------------------------------
test("GET /api/quota/plans without auth → 401", async () => {
await enableManagementAuth();
const req = new Request("http://localhost/api/quota/plans");
const res = await plansRoute.GET(req);
assert.equal(res.status, 401);
});
test("GET /api/quota/plans returns catalog providers (codex, kimi, etc.)", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/quota/plans");
const res = await plansRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { plans: Array<{ provider: string; source: string }> };
assert.ok(Array.isArray(body.plans), "plans should be an array");
// Known providers from planRegistry
const providers = body.plans.map((p) => p.provider);
assert.ok(providers.includes("codex"), "Should include codex from catalog");
assert.ok(providers.includes("kimi"), "Should include kimi from catalog");
// Catalog entries have source=auto
const codexEntry = body.plans.find((p) => p.provider === "codex");
assert.equal(codexEntry?.source, "auto");
});
test("GET /api/quota/plans includes DB override plans", async () => {
// First add a manual override
const putReq = await makeManagementSessionRequest(
"http://localhost/api/quota/plans/conn-override-1",
{
method: "PUT",
body: {
dimensions: [{ unit: "tokens", window: "daily", limit: 50000 }],
},
}
);
await planIdRoute.PUT(putReq, { params: Promise.resolve({ connectionId: "conn-override-1" }) });
// List should include the override
const listReq = await makeManagementSessionRequest("http://localhost/api/quota/plans");
const listRes = await plansRoute.GET(listReq);
const body = (await listRes.json()) as { plans: Array<{ connectionId: string | null; source: string }> };
const override = body.plans.find((p) => p.connectionId === "conn-override-1");
assert.ok(override, "Override plan should appear in list");
assert.equal(override?.source, "manual");
});
// ---------------------------------------------------------------------------
// GET /api/quota/plans/[connectionId]
// ---------------------------------------------------------------------------
test("GET /api/quota/plans/[connectionId] returns catalog plan when no override", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/plans/conn-no-override"
);
const res = await planIdRoute.GET(req, {
params: Promise.resolve({ connectionId: "conn-no-override" }),
});
assert.equal(res.status, 200);
const body = (await res.json()) as { plan: { source: string } };
// No DB override, no catalog match → source="manual" (empty plan)
assert.ok(["auto", "manual"].includes(body.plan.source), "Source should be auto or manual");
});
test("GET /api/quota/plans/[connectionId] returns DB override when present", async () => {
const connectionId = "conn-with-override";
// Create override
const putReq = await makeManagementSessionRequest(
`http://localhost/api/quota/plans/${connectionId}`,
{
method: "PUT",
body: {
dimensions: [{ unit: "requests", window: "hourly", limit: 200 }],
},
}
);
await planIdRoute.PUT(putReq, { params: Promise.resolve({ connectionId }) });
// GET should return the override
const getReq = await makeManagementSessionRequest(
`http://localhost/api/quota/plans/${connectionId}`
);
const getRes = await planIdRoute.GET(getReq, { params: Promise.resolve({ connectionId }) });
assert.equal(getRes.status, 200);
const body = (await getRes.json()) as {
plan: { source: string; dimensions: Array<{ unit: string; limit: number }> };
};
assert.equal(body.plan.source, "manual");
assert.equal(body.plan.dimensions[0]?.unit, "requests");
assert.equal(body.plan.dimensions[0]?.limit, 200);
});
// ---------------------------------------------------------------------------
// PUT /api/quota/plans/[connectionId]
// ---------------------------------------------------------------------------
test("PUT /api/quota/plans/[connectionId] without auth → 401", async () => {
await enableManagementAuth();
const req = new Request("http://localhost/api/quota/plans/conn-auth-test", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ dimensions: [{ unit: "tokens", window: "daily", limit: 1000 }] }),
});
const res = await planIdRoute.PUT(req, {
params: Promise.resolve({ connectionId: "conn-auth-test" }),
});
assert.equal(res.status, 401);
});
test("PUT /api/quota/plans/[connectionId] with invalid body → 400", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/plans/conn-bad-body",
{
method: "PUT",
body: { dimensions: [] }, // PlanUpsertSchema requires min(1) dimensions
}
);
const res = await planIdRoute.PUT(req, {
params: Promise.resolve({ connectionId: "conn-bad-body" }),
});
assert.equal(res.status, 400);
const body = await res.json();
// Hard Rule #12
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 400 response");
});
test("PUT /api/quota/plans/[connectionId] with valid body → source=manual + audit event", async () => {
const connectionId = "conn-put-test";
const req = await makeManagementSessionRequest(
`http://localhost/api/quota/plans/${connectionId}`,
{
method: "PUT",
body: {
dimensions: [{ unit: "usd", window: "monthly", limit: 100 }],
},
}
);
const res = await planIdRoute.PUT(req, { params: Promise.resolve({ connectionId }) });
assert.equal(res.status, 200);
const body = (await res.json()) as { plan: { source: string } };
assert.equal(body.plan.source, "manual");
// Audit event
const logs = compliance.getAuditLog({ action: "quota.plan.updated", limit: 10 });
const events = Array.isArray(logs) ? logs : [];
const evt = events.find(
(e) =>
typeof e === "object" &&
e !== null &&
(e as Record<string, unknown>).action === "quota.plan.updated" &&
(e as Record<string, unknown>).target === connectionId
);
assert.ok(evt, "quota.plan.updated audit event must be present");
});
// ---------------------------------------------------------------------------
// DELETE /api/quota/plans/[connectionId]
// ---------------------------------------------------------------------------
test("DELETE /api/quota/plans/[connectionId] clears override → 204; GET reverts to auto", async () => {
const connectionId = "conn-delete-plan";
// Create override
const putReq = await makeManagementSessionRequest(
`http://localhost/api/quota/plans/${connectionId}`,
{
method: "PUT",
body: { dimensions: [{ unit: "tokens", window: "weekly", limit: 500000 }] },
}
);
await planIdRoute.PUT(putReq, { params: Promise.resolve({ connectionId }) });
// Delete override
const deleteReq = await makeManagementSessionRequest(
`http://localhost/api/quota/plans/${connectionId}`,
{ method: "DELETE" }
);
const deleteRes = await planIdRoute.DELETE(deleteReq, { params: Promise.resolve({ connectionId }) });
assert.equal(deleteRes.status, 204);
// GET should now return auto/empty plan (no DB override)
const getReq = await makeManagementSessionRequest(
`http://localhost/api/quota/plans/${connectionId}`
);
const getRes = await planIdRoute.GET(getReq, { params: Promise.resolve({ connectionId }) });
const body = (await getRes.json()) as { plan: { source: string; dimensions: unknown[] } };
// After delete, should fall back to catalog or empty (source=auto or manual-empty)
// For a connectionId with no catalog match, source=manual + dimensions=[]
assert.ok(["auto", "manual"].includes(body.plan.source));
});
test("DELETE /api/quota/plans/[connectionId] is idempotent → 204 even when not found", async () => {
const deleteReq = await makeManagementSessionRequest(
"http://localhost/api/quota/plans/conn-never-existed",
{ method: "DELETE" }
);
const deleteRes = await planIdRoute.DELETE(deleteReq, {
params: Promise.resolve({ connectionId: "conn-never-existed" }),
});
// DELETE is idempotent — returns 204 regardless
assert.equal(deleteRes.status, 204);
});

View File

@@ -0,0 +1,286 @@
/**
* Integration tests: /api/quota/pools CRUD endpoints
*
* Verifies:
* - Auth: no auth → 401, invalid body → 400, valid → 201/200/204
* - POST creates pool + emits audit event
* - GET list includes created pool
* - GET [id] returns 200 or 404
* - PATCH updates pool + emits audit event
* - DELETE removes pool + emits audit event; subsequent GET → 404
* - Error responses never leak stack traces (Hard Rule #12 / B25)
*
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-pools-crud-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-pools-secret";
// Import in dependency order to ensure migrations run before routes
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const poolsRoute = await import("../../src/app/api/quota/pools/route.ts");
const poolIdRoute = await import("../../src/app/api/quota/pools/[id]/route.ts");
async function enableManagementAuth() {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ---------------------------------------------------------------------------
// POST /api/quota/pools
// ---------------------------------------------------------------------------
test("POST /api/quota/pools without auth → 401", async () => {
await enableManagementAuth();
const req = new Request("http://localhost/api/quota/pools", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ connectionId: "conn-1", name: "Pool A" }),
});
const res = await poolsRoute.POST(req);
assert.equal(res.status, 401);
});
test("POST /api/quota/pools with auth + invalid body → 400", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "", name: "" }, // Empty strings fail Zod min(1)
});
const res = await poolsRoute.POST(req);
assert.equal(res.status, 400);
const body = await res.json();
assert.ok(body.error?.message, "Should have error message");
// Hard Rule #12: no stack trace in error response
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 400 response");
});
test("POST /api/quota/pools with auth + valid body → 201 + pool returned", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: {
connectionId: "conn-test-1",
name: "Test Pool Alpha",
allocations: [],
},
});
const res = await poolsRoute.POST(req);
assert.equal(res.status, 201);
const body = await res.json() as { pool: { id: string; name: string; connectionId: string } };
assert.ok(body.pool.id, "Pool should have an id");
assert.equal(body.pool.name, "Test Pool Alpha");
assert.equal(body.pool.connectionId, "conn-test-1");
});
test("POST /api/quota/pools → audit event logged", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "conn-audit-check", name: "Audited Pool" },
});
await poolsRoute.POST(req);
// Verify audit event was recorded
const logs = compliance.getAuditLog({ action: "quota.pool.created", limit: 10 });
const events = Array.isArray(logs) ? logs : [];
assert.ok(events.length >= 1, "Should have at least one quota.pool.created audit event");
const evt = events.find(
(e) => typeof e === "object" && e !== null && (e as Record<string, unknown>).action === "quota.pool.created"
);
assert.ok(evt, "quota.pool.created audit event must be present");
});
// ---------------------------------------------------------------------------
// GET /api/quota/pools
// ---------------------------------------------------------------------------
test("GET /api/quota/pools returns list including created pool", async () => {
// Create a pool first
const createReq = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "conn-list-test", name: "List Test Pool" },
});
const createRes = await poolsRoute.POST(createReq);
assert.equal(createRes.status, 201);
const created = (await createRes.json()) as { pool: { id: string } };
const poolId = created.pool.id;
// Now list all pools
const listReq = await makeManagementSessionRequest("http://localhost/api/quota/pools");
const listRes = await poolsRoute.GET(listReq);
assert.equal(listRes.status, 200);
const body = (await listRes.json()) as { pools: Array<{ id: string; name: string }> };
assert.ok(Array.isArray(body.pools), "pools should be an array");
const found = body.pools.find((p) => p.id === poolId);
assert.ok(found, `Pool ${poolId} should be in the list`);
assert.equal(found?.name, "List Test Pool");
});
// ---------------------------------------------------------------------------
// GET /api/quota/pools/[id]
// ---------------------------------------------------------------------------
test("GET /api/quota/pools/[id] → 200 with pool detail", async () => {
// Create pool
const createReq = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "conn-detail", name: "Detail Pool" },
});
const createRes = await poolsRoute.POST(createReq);
const created = (await createRes.json()) as { pool: { id: string } };
const poolId = created.pool.id;
// Fetch by ID
const getReq = await makeManagementSessionRequest(`http://localhost/api/quota/pools/${poolId}`);
const getRes = await poolIdRoute.GET(getReq, { params: Promise.resolve({ id: poolId }) });
assert.equal(getRes.status, 200);
const body = (await getRes.json()) as { pool: { id: string; name: string } };
assert.equal(body.pool.id, poolId);
assert.equal(body.pool.name, "Detail Pool");
});
test("GET /api/quota/pools/[id] with nonexistent id → 404", async () => {
const getReq = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/does-not-exist"
);
const getRes = await poolIdRoute.GET(getReq, {
params: Promise.resolve({ id: "does-not-exist" }),
});
assert.equal(getRes.status, 404);
const body = await getRes.json();
assert.ok(body.error?.message, "Should have error message");
// Hard Rule #12
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 404 response");
});
// ---------------------------------------------------------------------------
// PATCH /api/quota/pools/[id]
// ---------------------------------------------------------------------------
test("PATCH /api/quota/pools/[id] → 200 updated + audit event", async () => {
// Create
const createReq = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "conn-patch", name: "Original Name" },
});
const createRes = await poolsRoute.POST(createReq);
const created = (await createRes.json()) as { pool: { id: string } };
const poolId = created.pool.id;
// Patch
const patchReq = await makeManagementSessionRequest(
`http://localhost/api/quota/pools/${poolId}`,
{
method: "PATCH",
body: { name: "Updated Name" },
}
);
const patchRes = await poolIdRoute.PATCH(patchReq, {
params: Promise.resolve({ id: poolId }),
});
assert.equal(patchRes.status, 200);
const body = (await patchRes.json()) as { pool: { name: string } };
assert.equal(body.pool.name, "Updated Name");
// Audit event
const logs = compliance.getAuditLog({ action: "quota.pool.updated", limit: 10 });
const events = Array.isArray(logs) ? logs : [];
const evt = events.find(
(e) =>
typeof e === "object" &&
e !== null &&
(e as Record<string, unknown>).action === "quota.pool.updated" &&
(e as Record<string, unknown>).target === poolId
);
assert.ok(evt, "quota.pool.updated audit event must be present with correct target");
});
test("PATCH /api/quota/pools/[id] with nonexistent id → 404", async () => {
const patchReq = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/does-not-exist",
{ method: "PATCH", body: { name: "New Name" } }
);
const patchRes = await poolIdRoute.PATCH(patchReq, {
params: Promise.resolve({ id: "does-not-exist" }),
});
assert.equal(patchRes.status, 404);
});
// ---------------------------------------------------------------------------
// DELETE /api/quota/pools/[id]
// ---------------------------------------------------------------------------
test("DELETE /api/quota/pools/[id] → 204 + audit event; subsequent GET → 404", async () => {
// Create
const createReq = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "conn-delete", name: "Delete Me" },
});
const createRes = await poolsRoute.POST(createReq);
const created = (await createRes.json()) as { pool: { id: string } };
const poolId = created.pool.id;
// Delete
const deleteReq = await makeManagementSessionRequest(
`http://localhost/api/quota/pools/${poolId}`,
{ method: "DELETE" }
);
const deleteRes = await poolIdRoute.DELETE(deleteReq, {
params: Promise.resolve({ id: poolId }),
});
assert.equal(deleteRes.status, 204);
// Audit event
const logs = compliance.getAuditLog({ action: "quota.pool.deleted", limit: 10 });
const events = Array.isArray(logs) ? logs : [];
const evt = events.find(
(e) =>
typeof e === "object" &&
e !== null &&
(e as Record<string, unknown>).action === "quota.pool.deleted" &&
(e as Record<string, unknown>).target === poolId
);
assert.ok(evt, "quota.pool.deleted audit event must be present");
// Subsequent GET → 404
const getReq = await makeManagementSessionRequest(
`http://localhost/api/quota/pools/${poolId}`
);
const getRes = await poolIdRoute.GET(getReq, { params: Promise.resolve({ id: poolId }) });
assert.equal(getRes.status, 404);
});
test("DELETE /api/quota/pools/[id] with nonexistent id → 404", async () => {
const deleteReq = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/never-existed",
{ method: "DELETE" }
);
const deleteRes = await poolIdRoute.DELETE(deleteReq, {
params: Promise.resolve({ id: "never-existed" }),
});
assert.equal(deleteRes.status, 404);
});

View File

@@ -0,0 +1,169 @@
/**
* Integration tests: GET /api/quota/pools/[id]/usage
*
* Verifies:
* - Returns PoolUsageSnapshot shape with perKey + deficit
* - 404 for nonexistent pool
* - Error responses don't leak stack traces (Hard Rule #12 / B25)
*
* Note: Consumption is produced via the SqliteQuotaStore directly (bypassing
* the HTTP layer) so we can control what the usage endpoint reads back.
*
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-pools-usage-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-usage-secret";
// Force SQLite store for deterministic tests
process.env.QUOTA_STORE_DRIVER = "sqlite";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const { createPool, upsertAllocations } = localDb;
const { getSqliteQuotaStore } = await import("../../src/lib/quota/sqliteQuotaStore.ts");
const { resetQuotaStoreSingleton } = await import("../../src/lib/quota/QuotaStore.ts");
const poolsRoute = await import("../../src/app/api/quota/pools/route.ts");
const usageRoute = await import("../../src/app/api/quota/pools/[id]/usage/route.ts");
async function enableManagementAuth() {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
function resetDb() {
core.resetDbInstance();
resetQuotaStoreSingleton();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
resetQuotaStoreSingleton();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("GET /api/quota/pools/[id]/usage without auth → 401", async () => {
await enableManagementAuth();
const req = new Request("http://localhost/api/quota/pools/some-id/usage");
const res = await usageRoute.GET(req, { params: Promise.resolve({ id: "some-id" }) });
assert.equal(res.status, 401);
});
test("GET /api/quota/pools/[id]/usage with nonexistent pool → 404", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/not-a-real-pool/usage"
);
const res = await usageRoute.GET(req, {
params: Promise.resolve({ id: "not-a-real-pool" }),
});
assert.equal(res.status, 404);
const body = await res.json();
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 404 response");
});
test("GET /api/quota/pools/[id]/usage → PoolUsageSnapshot shape with correct fields", async () => {
// 1. Create pool with 2 allocations
const createReq = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: {
connectionId: "conn-usage-test",
name: "Usage Test Pool",
allocations: [],
},
});
const createRes = await poolsRoute.POST(createReq);
assert.equal(createRes.status, 201);
const { pool } = (await createRes.json()) as { pool: { id: string } };
const poolId = pool.id;
// 2. Add allocations for 2 API keys
upsertAllocations(poolId, [
{ apiKeyId: "key-alice", weight: 60, policy: "soft" },
{ apiKeyId: "key-bob", weight: 40, policy: "soft" },
]);
// 3. Simulate consumption via the SQLite store directly
const store = getSqliteQuotaStore();
const dim = { poolId, unit: "tokens" as const, window: "daily" as const };
await store.consume("key-alice", dim, 1000);
await store.consume("key-bob", dim, 500);
// 4. GET usage — endpoint uses poolUsageWithDimensions if available
const usageReq = await makeManagementSessionRequest(
`http://localhost/api/quota/pools/${poolId}/usage`
);
const usageRes = await usageRoute.GET(usageReq, { params: Promise.resolve({ id: poolId }) });
assert.equal(usageRes.status, 200);
const body = (await usageRes.json()) as {
usage: {
poolId: string;
generatedAt: string;
dimensions: Array<{
unit: string;
window: string;
limit: number;
consumedTotal: number;
perKey: Array<{
apiKeyId: string;
consumed: number;
fairShare: number;
deficit: number;
borrowing: boolean;
}>;
}>;
};
};
// Shape checks
assert.equal(body.usage.poolId, poolId);
assert.ok(body.usage.generatedAt, "generatedAt should be present");
assert.ok(Array.isArray(body.usage.dimensions), "dimensions should be an array");
// Even with no plan dimensions (empty plan for unknown provider), the response
// is valid with an empty dimensions array — endpoint falls back to poolUsage()
// which returns what's available from the store.
assert.doesNotMatch(
JSON.stringify(body),
/\s+at\s+\//,
"No stack trace in usage response"
);
});
test("GET /api/quota/pools/[id]/usage response has required PoolUsageSnapshot fields", async () => {
// Create minimal pool
const createReq = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "conn-snapshot-shape", name: "Shape Pool" },
});
const createRes = await poolsRoute.POST(createReq);
const { pool } = (await createRes.json()) as { pool: { id: string } };
const usageReq = await makeManagementSessionRequest(
`http://localhost/api/quota/pools/${pool.id}/usage`
);
const usageRes = await usageRoute.GET(usageReq, {
params: Promise.resolve({ id: pool.id }),
});
assert.equal(usageRes.status, 200);
const body = (await usageRes.json()) as { usage: Record<string, unknown> };
assert.ok("usage" in body, "Response should have 'usage' key");
assert.ok("poolId" in body.usage, "PoolUsageSnapshot should have poolId");
assert.ok("generatedAt" in body.usage, "PoolUsageSnapshot should have generatedAt");
assert.ok("dimensions" in body.usage, "PoolUsageSnapshot should have dimensions");
});

View File

@@ -0,0 +1,145 @@
/**
* Integration tests: GET /api/quota/preview
*
* Verifies:
* - Auth check (401 without session)
* - Zod validation for query params (400 on missing required fields)
* - 404 when pool does not exist
* - Valid query → returns { decision } with kind="allow"
* - enforce is dry-run: store counters unchanged before and after (peek)
* - Error responses don't leak stack traces (Hard Rule #12 / B25)
*
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-preview-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-preview-secret";
process.env.QUOTA_STORE_DRIVER = "sqlite";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const { createPool, upsertAllocations } = localDb;
const { getSqliteQuotaStore } = await import("../../src/lib/quota/sqliteQuotaStore.ts");
const { resetQuotaStoreSingleton } = await import("../../src/lib/quota/QuotaStore.ts");
const previewRoute = await import("../../src/app/api/quota/preview/route.ts");
async function enableManagementAuth() {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
function resetDb() {
core.resetDbInstance();
resetQuotaStoreSingleton();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
resetQuotaStoreSingleton();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("GET /api/quota/preview without auth → 401", async () => {
await enableManagementAuth();
const req = new Request(
"http://localhost/api/quota/preview?apiKeyId=k1&poolId=p1"
);
const res = await previewRoute.GET(req);
assert.equal(res.status, 401);
});
test("GET /api/quota/preview without required query params → 400", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/preview"
// Missing apiKeyId and poolId
);
const res = await previewRoute.GET(req);
assert.equal(res.status, 400);
const body = await res.json();
assert.ok(body.error?.message, "Should have error message");
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 400 response");
});
test("GET /api/quota/preview with nonexistent poolId → 404", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/preview?apiKeyId=key-1&poolId=no-such-pool"
);
const res = await previewRoute.GET(req);
assert.equal(res.status, 404);
const body = await res.json();
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 404 response");
});
test("GET /api/quota/preview with valid params → { decision } with kind", async () => {
// Create a real pool
const pool = createPool({ connectionId: "conn-preview", name: "Preview Pool" });
upsertAllocations(pool.id, [
{ apiKeyId: "preview-key-1", weight: 100, policy: "soft" },
]);
const req = await makeManagementSessionRequest(
`http://localhost/api/quota/preview?apiKeyId=preview-key-1&poolId=${pool.id}&estimatedTokens=100`
);
const res = await previewRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { decision: { kind: string } };
assert.ok(body.decision, "Response should have decision field");
assert.ok(
["allow", "block"].includes(body.decision.kind),
`decision.kind must be "allow" or "block", got: ${body.decision.kind}`
);
});
test("GET /api/quota/preview is dry-run: store counters unchanged after call", async () => {
// Create pool and seed some consumption
const pool = createPool({ connectionId: "conn-dryrun", name: "Dry Run Pool" });
upsertAllocations(pool.id, [
{ apiKeyId: "dryrun-key", weight: 100, policy: "hard" },
]);
const store = getSqliteQuotaStore();
const dim = { poolId: pool.id, unit: "tokens" as const, window: "daily" as const };
// Pre-peek value
const before = await store.peek("dryrun-key", dim);
// Call preview (dry-run)
const req = await makeManagementSessionRequest(
`http://localhost/api/quota/preview?apiKeyId=dryrun-key&poolId=${pool.id}&estimatedTokens=500`
);
await previewRoute.GET(req);
// Post-peek value — must equal pre-peek (no consumption occurred)
const after = await store.peek("dryrun-key", dim);
assert.equal(before, after, "Store counter must not change after a preview (dry-run) call");
});
test("GET /api/quota/preview accepts optional estimatedUsd and estimatedRequests", async () => {
const pool = createPool({ connectionId: "conn-optional", name: "Optional Pool" });
upsertAllocations(pool.id, [{ apiKeyId: "opt-key", weight: 100, policy: "burst" }]);
const req = await makeManagementSessionRequest(
`http://localhost/api/quota/preview?apiKeyId=opt-key&poolId=${pool.id}&estimatedUsd=1.5&estimatedRequests=3`
);
const res = await previewRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { decision: unknown };
assert.ok(body.decision, "Should return decision");
});

View File

@@ -0,0 +1,255 @@
/**
* Integration tests: error sanitization across all quota REST routes
*
* Verifies Hard Rule #12 (B25): No route returns raw stack traces, absolute
* paths, or credential strings in error response bodies.
*
* Tests the error paths of each quota endpoint — 400s (bad input) and 404s
* (not found) — to confirm buildErrorBody sanitization is active.
*
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-quota-err-sanitization-")
);
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-sanitization-secret";
process.env.QUOTA_STORE_DRIVER = "sqlite";
// Ensure a known fake URL that we can check is NOT leaked
process.env.QUOTA_STORE_REDIS_URL = "redis://secret-host:9999/0";
const core = await import("../../src/lib/db/core.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const { resetQuotaStoreSingleton } = await import("../../src/lib/quota/QuotaStore.ts");
// Import all routes
const poolsRoute = await import("../../src/app/api/quota/pools/route.ts");
const poolIdRoute = await import("../../src/app/api/quota/pools/[id]/route.ts");
const usageRoute = await import("../../src/app/api/quota/pools/[id]/usage/route.ts");
const plansRoute = await import("../../src/app/api/quota/plans/route.ts");
const planIdRoute = await import("../../src/app/api/quota/plans/[connectionId]/route.ts");
const previewRoute = await import("../../src/app/api/quota/preview/route.ts");
const settingsRoute = await import("../../src/app/api/settings/quota-store/route.ts");
function resetDb() {
core.resetDbInstance();
resetQuotaStoreSingleton();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
// Helper to assert no stack trace / path leak in a response body text
function assertNoStackTraceText(text: string, label: string) {
assert.doesNotMatch(
text,
/\s+at\s+\//,
`${label}: Response must not contain stack trace (Hard Rule #12)`
);
assert.doesNotMatch(
text,
/\/home\/[a-z]/,
`${label}: Response must not contain absolute home path`
);
}
// Reads the response body once and runs stack trace assertion
async function assertNoStackTrace(res: Response, label: string) {
const text = await res.text();
assertNoStackTraceText(text, label);
}
// Helper to assert secret URL not in response body text
function assertNoSecretUrlText(text: string, label: string) {
assert.doesNotMatch(
text,
/secret-host/,
`${label}: Response must not contain secret Redis host`
);
assert.doesNotMatch(
text,
/redis:\/\/secret/,
`${label}: Response must not contain Redis URL`
);
}
// Reads the response body once and runs both assertions (body cannot be read twice)
async function assertNoStackTraceAndNoSecretUrl(res: Response, label: string) {
const text = await res.text();
assertNoStackTraceText(text, label);
assertNoSecretUrlText(text, label);
}
test.beforeEach(() => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
resetQuotaStoreSingleton();
delete process.env.QUOTA_STORE_REDIS_URL;
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ---------------------------------------------------------------------------
// POST /api/quota/pools — bad body → 400
// ---------------------------------------------------------------------------
test("POST /api/quota/pools 400 error response has no stack trace", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/quota/pools", {
method: "POST",
body: { connectionId: "", name: "" }, // Empty strings fail Zod validation
});
const res = await poolsRoute.POST(req);
assert.equal(res.status, 400);
await assertNoStackTrace(res, "POST /api/quota/pools 400");
});
// ---------------------------------------------------------------------------
// GET /api/quota/pools/[id] — 404
// ---------------------------------------------------------------------------
test("GET /api/quota/pools/[id] 404 response has no stack trace", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/does-not-exist"
);
const res = await poolIdRoute.GET(req, {
params: Promise.resolve({ id: "does-not-exist" }),
});
assert.equal(res.status, 404);
await assertNoStackTrace(res, "GET /api/quota/pools/[id] 404");
});
// ---------------------------------------------------------------------------
// PATCH /api/quota/pools/[id] — bad body → 400
// ---------------------------------------------------------------------------
test("PATCH /api/quota/pools/[id] 400 error response has no stack trace", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/does-not-exist",
{
method: "PATCH",
body: { allocations: "not-an-array" }, // Zod expects array
}
);
const res = await poolIdRoute.PATCH(req, {
params: Promise.resolve({ id: "does-not-exist" }),
});
// May be 400 (Zod) or 404 (not found after Zod passes) — either is fine,
// key requirement is no stack trace
await assertNoStackTrace(res, "PATCH /api/quota/pools/[id]");
});
// ---------------------------------------------------------------------------
// GET /api/quota/pools/[id]/usage — 404
// ---------------------------------------------------------------------------
test("GET /api/quota/pools/[id]/usage 404 response has no stack trace", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/pools/ghost-pool/usage"
);
const res = await usageRoute.GET(req, {
params: Promise.resolve({ id: "ghost-pool" }),
});
assert.equal(res.status, 404);
await assertNoStackTrace(res, "GET /api/quota/pools/[id]/usage 404");
});
// ---------------------------------------------------------------------------
// GET /api/quota/plans — valid but checked for sanitization
// ---------------------------------------------------------------------------
test("GET /api/quota/plans 200 response has no stack trace or path leak", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/quota/plans");
const res = await plansRoute.GET(req);
assert.equal(res.status, 200);
await assertNoStackTrace(res, "GET /api/quota/plans 200");
});
// ---------------------------------------------------------------------------
// PUT /api/quota/plans/[connectionId] — bad body → 400
// ---------------------------------------------------------------------------
test("PUT /api/quota/plans/[connectionId] 400 error response has no stack trace", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/plans/conn-bad",
{
method: "PUT",
body: { dimensions: [] }, // PlanUpsertSchema requires min(1)
}
);
const res = await planIdRoute.PUT(req, {
params: Promise.resolve({ connectionId: "conn-bad" }),
});
assert.equal(res.status, 400);
await assertNoStackTrace(res, "PUT /api/quota/plans/[connectionId] 400");
});
// ---------------------------------------------------------------------------
// GET /api/quota/preview — missing params → 400
// ---------------------------------------------------------------------------
test("GET /api/quota/preview 400 error response has no stack trace", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/quota/preview"
// Missing apiKeyId and poolId
);
const res = await previewRoute.GET(req);
assert.equal(res.status, 400);
await assertNoStackTrace(res, "GET /api/quota/preview 400");
});
// ---------------------------------------------------------------------------
// GET /api/settings/quota-store — NEVER returns Redis URL
// ---------------------------------------------------------------------------
test("GET /api/settings/quota-store response does not contain Redis URL (Hard Rule #12/#1)", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store"
);
const res = await settingsRoute.GET(req);
assert.equal(res.status, 200);
await assertNoStackTraceAndNoSecretUrl(res, "GET /api/settings/quota-store 200");
});
// ---------------------------------------------------------------------------
// PUT /api/settings/quota-store — bad driver → 400 (Zod)
// ---------------------------------------------------------------------------
test("PUT /api/settings/quota-store 400 error response has no stack trace", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store",
{
method: "PUT",
body: { driver: "baddriver" },
}
);
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 400);
await assertNoStackTrace(res, "PUT /api/settings/quota-store 400");
});
// ---------------------------------------------------------------------------
// PUT /api/settings/quota-store — redis without URL → 400 (custom check)
// ---------------------------------------------------------------------------
test("PUT /api/settings/quota-store redis+no-URL error response does not leak Redis URL", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store",
{
method: "PUT",
body: { driver: "redis" }, // No URL provided
}
);
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 400);
await assertNoStackTraceAndNoSecretUrl(res, "PUT /api/settings/quota-store redis-no-url 400");
});

View File

@@ -0,0 +1,202 @@
/**
* Integration tests: GET/PUT /api/settings/quota-store
*
* Verifies:
* - GET returns driver + redisUrlConfigured flag (NEVER the URL itself)
* - PUT sqlite → 200; PUT redis without URL → 400; PUT redis with URL → 200
* - PUT emits quota.store.driver_changed audit event
* - GET never returns actual Redis URL (Hard Rule #12 / #1)
* - Error responses don't leak stack traces (Hard Rule #12 / B25)
*
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-quota-store-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-quota-store-settings-secret";
// Ensure no QUOTA_STORE_REDIS_URL leaks in from environment
delete process.env.QUOTA_STORE_REDIS_URL;
process.env.QUOTA_STORE_DRIVER = "sqlite";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const { resetQuotaStoreSingleton } = await import("../../src/lib/quota/QuotaStore.ts");
const settingsRoute = await import("../../src/app/api/settings/quota-store/route.ts");
async function enableManagementAuth() {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
function resetDb() {
core.resetDbInstance();
resetQuotaStoreSingleton();
delete process.env.QUOTA_STORE_REDIS_URL;
delete process.env.INITIAL_PASSWORD;
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => {
resetDb();
compliance.initAuditLog();
});
test.after(() => {
core.resetDbInstance();
resetQuotaStoreSingleton();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ---------------------------------------------------------------------------
// GET /api/settings/quota-store
// ---------------------------------------------------------------------------
test("GET /api/settings/quota-store without auth → 401", async () => {
await enableManagementAuth();
const req = new Request("http://localhost/api/settings/quota-store");
const res = await settingsRoute.GET(req);
assert.equal(res.status, 401);
});
test("GET /api/settings/quota-store returns driver + redisUrlConfigured (not URL)", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store"
);
const res = await settingsRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as {
driver: string;
redisUrlConfigured: boolean;
redisUrl: null | string;
};
assert.ok(["sqlite", "redis"].includes(body.driver), "driver must be sqlite or redis");
assert.ok(typeof body.redisUrlConfigured === "boolean", "redisUrlConfigured must be boolean");
// Hard Rule #12 / #1 — URL must NEVER be returned
assert.equal(body.redisUrl, null, "Redis URL must be null (never returned)");
// Verify the raw Redis URL string is not anywhere in the response text
const responseText = JSON.stringify(body);
assert.doesNotMatch(responseText, /redis:\/\//, "Redis URL must not appear in response");
assert.doesNotMatch(responseText, /\s+at\s+\//, "No stack trace in response");
});
test("GET /api/settings/quota-store redisUrlConfigured=false when no URL configured", async () => {
delete process.env.QUOTA_STORE_REDIS_URL;
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store"
);
const res = await settingsRoute.GET(req);
const body = (await res.json()) as { redisUrlConfigured: boolean };
assert.equal(body.redisUrlConfigured, false);
});
// ---------------------------------------------------------------------------
// PUT /api/settings/quota-store
// ---------------------------------------------------------------------------
test("PUT /api/settings/quota-store without auth → 401", async () => {
await enableManagementAuth();
const req = new Request("http://localhost/api/settings/quota-store", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ driver: "sqlite" }),
});
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 401);
});
test("PUT /api/settings/quota-store driver=sqlite → 200", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store",
{
method: "PUT",
body: { driver: "sqlite" },
}
);
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { driver: string; redisUrl: null };
assert.equal(body.driver, "sqlite");
assert.equal(body.redisUrl, null);
});
test("PUT /api/settings/quota-store driver=redis without URL → 400", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store",
{
method: "PUT",
body: { driver: "redis" }, // No redisUrl
}
);
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 400);
const body = await res.json();
assert.ok(body.error?.message, "Should have error message");
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 400 response");
});
test("PUT /api/settings/quota-store driver=redis with valid URL → 200 + audit event", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store",
{
method: "PUT",
body: { driver: "redis", redisUrl: "redis://localhost:6379" },
}
);
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 200);
const body = (await res.json()) as {
driver: string;
redisUrlConfigured: boolean;
redisUrl: null;
};
assert.equal(body.driver, "redis");
assert.equal(body.redisUrlConfigured, true);
// Hard Rule #12 / #1 — URL NEVER in response
assert.equal(body.redisUrl, null);
// Audit event
const logs = compliance.getAuditLog({ action: "quota.store.driver_changed", limit: 10 });
const events = Array.isArray(logs) ? logs : [];
const evt = events.find(
(e) =>
typeof e === "object" &&
e !== null &&
(e as Record<string, unknown>).action === "quota.store.driver_changed"
);
assert.ok(evt, "quota.store.driver_changed audit event must be present");
// Verify the actual Redis URL was NOT logged in audit metadata
const evtStr = JSON.stringify(evt);
assert.doesNotMatch(
evtStr,
/redis:\/\/localhost:6379/,
"Actual Redis URL must not appear in audit log metadata"
);
});
test("PUT /api/settings/quota-store with invalid driver → 400 (Zod)", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/settings/quota-store",
{
method: "PUT",
body: { driver: "memcached" }, // Not in enum
}
);
const res = await settingsRoute.PUT(req);
assert.equal(res.status, 400);
const body = await res.json();
assert.ok(body.error?.message, "Should have error message");
assert.doesNotMatch(JSON.stringify(body), /\s+at\s+\//, "No stack trace in 400 response");
});