mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Two gaps found in second-pass code review of Group B: 1. B26 violation: DELETE /api/quota/plans/[connectionId] did not emit logAuditEvent. Per master plan B26, every plan mutation must audit. Now emits quota.plan.updated with metadata.reverted=true to mark the revert-to-auto/catalog semantic. Test integration extended with assertion that audit event is present after DELETE. 2. pt-BR / pt locales had "costsSection": "Costs" (English label) instead of the Portuguese "Custos". Other section labels in the same block are left in English intentionally (analytics, monitoring) — they are project-wide untranslated terms; "Custos" is the established repo translation for the Costs section title. Validation: - npm run typecheck:core: clean - tests/integration/quota-plans-crud.test.ts: 10/10 pass (includes new assertion on DELETE → audit event) - eslint --no-warn-ignored on touched files: clean
267 lines
11 KiB
TypeScript
267 lines
11 KiB
TypeScript
/**
|
|
* 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));
|
|
|
|
// B26: DELETE must emit logAuditEvent with quota.plan.updated + metadata.reverted=true
|
|
const logs = compliance.getAuditLog({ action: "quota.plan.updated", limit: 20 });
|
|
const events = Array.isArray(logs) ? logs : [];
|
|
const deleteEvt = events.find(
|
|
(e) =>
|
|
typeof e === "object" &&
|
|
e !== null &&
|
|
(e as Record<string, unknown>).action === "quota.plan.updated" &&
|
|
(e as Record<string, unknown>).target === connectionId &&
|
|
(e as { metadata?: { reverted?: boolean } }).metadata?.reverted === true
|
|
);
|
|
assert.ok(deleteEvt, "quota.plan.updated audit event (reverted=true) must be present after DELETE");
|
|
});
|
|
|
|
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);
|
|
});
|