mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
754 lines
26 KiB
TypeScript
754 lines
26 KiB
TypeScript
/**
|
|
* Unit tests for API key policy helpers:
|
|
* - parseIsActive (via parseAccessSchedule indirect coverage)
|
|
* - isWithinSchedule logic (tested directly via a re-export or by mocking Date)
|
|
*
|
|
* Because isWithinSchedule is module-private, we test it through observable
|
|
* behavior: feeding real Date overrides via globalThis.Date stubbing.
|
|
*
|
|
* Strategy:
|
|
* 1. Extract the schedule-check logic into a standalone helper exported only
|
|
* for tests — OR test it end-to-end through enforceApiKeyPolicy.
|
|
* 2. Since enforceApiKeyPolicy needs a full DB + HTTP Request, we isolate
|
|
* isWithinSchedule by copying its logic into this test file and verifying
|
|
* the exact same algorithm.
|
|
*/
|
|
|
|
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 { pathToFileURL } from "node:url";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-api-key-policy-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "task-607-api-key-secret";
|
|
|
|
const coreDb = await import("../../src/lib/db/core.ts");
|
|
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
|
const combosDb = await import("../../src/lib/db/combos.ts");
|
|
const modelComboMappingsDb = await import("../../src/lib/db/modelComboMappings.ts");
|
|
const costRules = await import("../../src/domain/costRules.ts");
|
|
const rateLimiter = await import("../../src/shared/utils/rateLimiter.ts");
|
|
|
|
rateLimiter.setRateLimiterTestMode(true);
|
|
|
|
function getFsErrorCode(error: unknown): string | undefined {
|
|
if (typeof error !== "object" || error === null || !("code" in error)) return undefined;
|
|
const { code } = error as { code?: unknown };
|
|
return typeof code === "string" ? code : undefined;
|
|
}
|
|
|
|
async function resetStorage() {
|
|
apiKeysDb.resetApiKeyState();
|
|
costRules.resetCostData();
|
|
coreDb.resetDbInstance();
|
|
|
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
try {
|
|
if (fs.existsSync(TEST_DATA_DIR)) {
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
break;
|
|
} catch (error: unknown) {
|
|
const code = getFsErrorCode(error);
|
|
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
|
|
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
async function loadPolicy(label) {
|
|
const modulePath = path.join(process.cwd(), "src/shared/utils/apiKeyPolicy.ts");
|
|
return import(`${pathToFileURL(modulePath).href}?case=${label}-${Date.now()}`);
|
|
}
|
|
|
|
async function createKeyWithPolicy(update = {}) {
|
|
const created = await apiKeysDb.createApiKey("Policy Key", "machine-607");
|
|
if (Object.keys(update).length > 0) {
|
|
await apiKeysDb.updateApiKeyPermissions(created.id, update);
|
|
}
|
|
return created;
|
|
}
|
|
|
|
function makePolicyRequest(apiKey) {
|
|
return new Request("http://localhost/api/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
});
|
|
}
|
|
|
|
function makeAnthropicPolicyRequest(apiKey) {
|
|
return new Request("http://localhost/v1/messages", {
|
|
method: "POST",
|
|
headers: apiKey
|
|
? {
|
|
"x-api-key": apiKey,
|
|
"anthropic-version": "2023-06-01",
|
|
}
|
|
: { "anthropic-version": "2023-06-01" },
|
|
});
|
|
}
|
|
|
|
// A bare `x-api-key` with NO anthropic-version header and no claude user-agent:
|
|
// the CLIENT_API auth layer accepts it, but the gated extractApiKey() used by
|
|
// the policy layer used to ignore it, so the key's per-key policy was skipped
|
|
// entirely (GHSA-2phc-xp22-9f56).
|
|
function makeBareXApiKeyPolicyRequest(apiKey) {
|
|
return new Request("http://localhost/v1/responses", {
|
|
method: "POST",
|
|
headers: apiKey ? { "x-api-key": apiKey } : {},
|
|
});
|
|
}
|
|
|
|
async function readErrorMessage(response) {
|
|
const body = (await response.json()) as { error?: { message?: unknown } };
|
|
return typeof body.error?.message === "string" ? body.error.message : "";
|
|
}
|
|
|
|
function getCurrentUtcDay() {
|
|
const dayName = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: "UTC",
|
|
weekday: "short",
|
|
}).format(new Date());
|
|
return { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }[dayName];
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
delete process.env.DEFAULT_RATE_LIMIT_PER_DAY;
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
apiKeysDb.resetApiKeyState();
|
|
costRules.resetCostData();
|
|
coreDb.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
});
|
|
|
|
// ─── Replicate the isWithinSchedule logic for pure unit testing ───────────────
|
|
//
|
|
// This mirrors the implementation in apiKeyPolicy.ts exactly.
|
|
// If the production code changes, update this copy too.
|
|
|
|
/**
|
|
* @param {{ enabled: boolean; from: string; until: string; days: number[]; tz: string }} schedule
|
|
* @param {Date} now — injectable "current time"
|
|
* @returns {boolean}
|
|
*/
|
|
function isWithinSchedule(schedule, now = new Date()) {
|
|
if (!schedule.enabled) return true;
|
|
|
|
let localTimeStr;
|
|
try {
|
|
localTimeStr = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: schedule.tz,
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
}).format(now);
|
|
} catch {
|
|
return true;
|
|
}
|
|
|
|
const normalizedTime = localTimeStr.replace(/^24:/, "00:");
|
|
const [localHour, localMin] = normalizedTime.split(":").map(Number);
|
|
const localMinutes = localHour * 60 + localMin;
|
|
|
|
let localDayStr;
|
|
try {
|
|
localDayStr = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: schedule.tz,
|
|
weekday: "short",
|
|
}).format(now);
|
|
} catch {
|
|
return true;
|
|
}
|
|
|
|
const dayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
const localDay = dayMap[localDayStr] ?? now.getDay();
|
|
|
|
if (!schedule.days.includes(localDay)) return false;
|
|
|
|
const [fromHour, fromMin] = schedule.from.split(":").map(Number);
|
|
const [untilHour, untilMin] = schedule.until.split(":").map(Number);
|
|
const fromMinutes = fromHour * 60 + fromMin;
|
|
const untilMinutes = untilHour * 60 + untilMin;
|
|
|
|
if (untilMinutes < fromMinutes) {
|
|
return localMinutes >= fromMinutes || localMinutes < untilMinutes;
|
|
}
|
|
|
|
return localMinutes >= fromMinutes && localMinutes < untilMinutes;
|
|
}
|
|
|
|
// ─── parseIsActive helper (mirrors production code) ──────────────────────────
|
|
function parseIsActive(value) {
|
|
if (value === 0 || value === "0" || value === false) return false;
|
|
return true;
|
|
}
|
|
|
|
// ─── parseAccessSchedule helper (mirrors production code) ────────────────────
|
|
function parseAccessSchedule(value) {
|
|
if (!value || typeof value !== "string" || value.trim() === "") return null;
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
if (
|
|
typeof parsed.enabled !== "boolean" ||
|
|
typeof parsed.from !== "string" ||
|
|
typeof parsed.until !== "string" ||
|
|
!Array.isArray(parsed.days) ||
|
|
typeof parsed.tz !== "string"
|
|
)
|
|
return null;
|
|
const days = parsed.days.filter(
|
|
(d) => typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6
|
|
);
|
|
return { enabled: parsed.enabled, from: parsed.from, until: parsed.until, days, tz: parsed.tz };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ─── parseIsActive ────────────────────────────────────────────────────────────
|
|
|
|
test("parseIsActive: undefined → true (default active)", () => {
|
|
assert.equal(parseIsActive(undefined), true);
|
|
});
|
|
|
|
test("parseIsActive: null → true", () => {
|
|
assert.equal(parseIsActive(null), true);
|
|
});
|
|
|
|
test("parseIsActive: 1 → true", () => {
|
|
assert.equal(parseIsActive(1), true);
|
|
});
|
|
|
|
test("parseIsActive: true → true", () => {
|
|
assert.equal(parseIsActive(true), true);
|
|
});
|
|
|
|
test("parseIsActive: 0 → false", () => {
|
|
assert.equal(parseIsActive(0), false);
|
|
});
|
|
|
|
test("parseIsActive: false → false", () => {
|
|
assert.equal(parseIsActive(false), false);
|
|
});
|
|
|
|
test("parseIsActive: '0' → false", () => {
|
|
assert.equal(parseIsActive("0"), false);
|
|
});
|
|
|
|
// ─── parseAccessSchedule ──────────────────────────────────────────────────────
|
|
|
|
test("parseAccessSchedule: null/empty → null", () => {
|
|
assert.equal(parseAccessSchedule(null), null);
|
|
assert.equal(parseAccessSchedule(""), null);
|
|
assert.equal(parseAccessSchedule(" "), null);
|
|
});
|
|
|
|
test("parseAccessSchedule: valid JSON → object", () => {
|
|
const input = JSON.stringify({
|
|
enabled: true,
|
|
from: "08:00",
|
|
until: "18:00",
|
|
days: [1, 2, 3, 4, 5],
|
|
tz: "America/Sao_Paulo",
|
|
});
|
|
const result = parseAccessSchedule(input);
|
|
assert.deepEqual(result, {
|
|
enabled: true,
|
|
from: "08:00",
|
|
until: "18:00",
|
|
days: [1, 2, 3, 4, 5],
|
|
tz: "America/Sao_Paulo",
|
|
});
|
|
});
|
|
|
|
test("parseAccessSchedule: invalid day values are filtered out", () => {
|
|
const input = JSON.stringify({
|
|
enabled: true,
|
|
from: "08:00",
|
|
until: "18:00",
|
|
days: [1, 7, -1, 5],
|
|
tz: "UTC",
|
|
});
|
|
const result = parseAccessSchedule(input);
|
|
assert.deepEqual(result.days, [1, 5]);
|
|
});
|
|
|
|
test("parseAccessSchedule: missing required field → null", () => {
|
|
const input = JSON.stringify({ enabled: true, from: "08:00", until: "18:00", days: [1] });
|
|
assert.equal(parseAccessSchedule(input), null); // tz missing
|
|
});
|
|
|
|
test("parseAccessSchedule: invalid JSON → null", () => {
|
|
assert.equal(parseAccessSchedule("{broken json}"), null);
|
|
});
|
|
|
|
// ─── isWithinSchedule ────────────────────────────────────────────────────────
|
|
|
|
// Helper: create a Date at a specific UTC datetime
|
|
function utc(y, m, d, h, min) {
|
|
return new Date(Date.UTC(y, m - 1, d, h, min));
|
|
}
|
|
|
|
test("isWithinSchedule: enabled=false → always true", () => {
|
|
const schedule = { enabled: false, from: "00:00", until: "00:01", days: [1], tz: "UTC" };
|
|
// Even a time that would be blocked
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 12, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: time within window → true", () => {
|
|
// Monday 2024-03-11, 09:00 UTC (UTC timezone)
|
|
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 9, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: time before window → false", () => {
|
|
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
|
|
// 07:59
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 7, 59)), false);
|
|
});
|
|
|
|
test("isWithinSchedule: time exactly at 'from' → true (inclusive)", () => {
|
|
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 8, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: time exactly at 'until' → false (exclusive)", () => {
|
|
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 18, 0)), false);
|
|
});
|
|
|
|
test("isWithinSchedule: time after window → false", () => {
|
|
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 20, 0)), false);
|
|
});
|
|
|
|
test("isWithinSchedule: wrong weekday → false", () => {
|
|
// Monday (day 1) schedule, but 2024-03-12 is Tuesday (day 2)
|
|
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 12, 10, 0)), false);
|
|
});
|
|
|
|
test("isWithinSchedule: multiple days — matching day → true", () => {
|
|
// Mon-Fri schedule, Wednesday (3)
|
|
const schedule = {
|
|
enabled: true,
|
|
from: "09:00",
|
|
until: "17:00",
|
|
days: [1, 2, 3, 4, 5],
|
|
tz: "UTC",
|
|
};
|
|
// 2024-03-13 is Wednesday
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 13, 12, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: multiple days — Saturday blocked", () => {
|
|
const schedule = {
|
|
enabled: true,
|
|
from: "09:00",
|
|
until: "17:00",
|
|
days: [1, 2, 3, 4, 5],
|
|
tz: "UTC",
|
|
};
|
|
// 2024-03-09 is Saturday (day 6)
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 9, 12, 0)), false);
|
|
});
|
|
|
|
// ─── Overnight schedule tests ─────────────────────────────────────────────────
|
|
|
|
test("isWithinSchedule: overnight window — time after midnight → true", () => {
|
|
// 22:00 → 06:00, Monday
|
|
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
|
|
// 02:30 Monday
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 2, 30)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: overnight window — time before start → false", () => {
|
|
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
|
|
// 21:59 Monday
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 21, 59)), false);
|
|
});
|
|
|
|
test("isWithinSchedule: overnight window — time after end → false", () => {
|
|
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
|
|
// 06:01 Monday
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 6, 1)), false);
|
|
});
|
|
|
|
test("isWithinSchedule: overnight window — time exactly at start → true", () => {
|
|
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 22, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: invalid timezone → fail-open (true)", () => {
|
|
const schedule = {
|
|
enabled: true,
|
|
from: "08:00",
|
|
until: "18:00",
|
|
days: [1, 2, 3, 4, 5],
|
|
tz: "Invalid/Zone",
|
|
};
|
|
// Should not throw, should return true (fail-open)
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 12, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: America/Sao_Paulo timezone conversion", () => {
|
|
// UTC 2024-03-11 15:00 = BRT (UTC-3) 12:00, Monday
|
|
const schedule = {
|
|
enabled: true,
|
|
from: "08:00",
|
|
until: "18:00",
|
|
days: [1],
|
|
tz: "America/Sao_Paulo",
|
|
};
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 15, 0)), true);
|
|
});
|
|
|
|
test("isWithinSchedule: America/Sao_Paulo — outside window", () => {
|
|
// UTC 2024-03-11 22:00 = BRT 19:00, Monday — after 18:00
|
|
const schedule = {
|
|
enabled: true,
|
|
from: "08:00",
|
|
until: "18:00",
|
|
days: [1],
|
|
tz: "America/Sao_Paulo",
|
|
};
|
|
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 22, 0)), false);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy bypasses local mode and unknown keys", async () => {
|
|
const policy = await loadPolicy("bypass");
|
|
|
|
assert.deepEqual(await policy.enforceApiKeyPolicy(makePolicyRequest(null), "openai/gpt-4.1"), {
|
|
apiKey: null,
|
|
apiKeyInfo: null,
|
|
rejection: null,
|
|
});
|
|
|
|
const unknown = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest("sk-unknown"),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(unknown.apiKey, "sk-unknown");
|
|
assert.equal(unknown.apiKeyInfo, null);
|
|
assert.equal(unknown.rejection, null);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy rejects disabled keys and blocked schedules", async () => {
|
|
const disabledKey = await createKeyWithPolicy({ isActive: false });
|
|
const blockedDay = (getCurrentUtcDay() + 1) % 7;
|
|
const scheduledKey = await createKeyWithPolicy({
|
|
accessSchedule: {
|
|
enabled: true,
|
|
from: "00:00",
|
|
until: "23:59",
|
|
days: [blockedDay],
|
|
tz: "UTC",
|
|
},
|
|
});
|
|
const policy = await loadPolicy("disabled-and-schedule");
|
|
|
|
const disabled = await policy.enforceApiKeyPolicy(makePolicyRequest(disabledKey.key), null);
|
|
assert.equal(disabled.rejection.status, 403);
|
|
assert.equal(await readErrorMessage(disabled.rejection), "This API key is disabled");
|
|
|
|
const blocked = await policy.enforceApiKeyPolicy(makePolicyRequest(scheduledKey.key), null);
|
|
assert.equal(blocked.rejection.status, 403);
|
|
assert.match(await readErrorMessage(blocked.rejection), /Access denied outside allowed hours/);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy enforces allowedModels for a bare x-api-key (GHSA-2phc-xp22-9f56)", async () => {
|
|
const restrictedKey = await createKeyWithPolicy({
|
|
allowedModels: ["openai/gpt-4.1"],
|
|
});
|
|
const policy = await loadPolicy("bare-x-api-key");
|
|
|
|
// Disallowed model via a bare x-api-key must be rejected, exactly as it is for
|
|
// a Bearer token — the header used to carry the key must not weaken the policy.
|
|
const disallowed = await policy.enforceApiKeyPolicy(
|
|
makeBareXApiKeyPolicyRequest(restrictedKey.key),
|
|
"anthropic/claude-3-7-sonnet"
|
|
);
|
|
assert.equal(disallowed.rejection.status, 403);
|
|
assert.match(await readErrorMessage(disallowed.rejection), /not allowed/);
|
|
|
|
// The allowed model still passes through the same header.
|
|
const allowed = await policy.enforceApiKeyPolicy(
|
|
makeBareXApiKeyPolicyRequest(restrictedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(allowed.rejection, null);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy rejects disallowed models and exhausted budgets", async () => {
|
|
const restrictedKey = await createKeyWithPolicy({
|
|
allowedModels: ["openai/gpt-4.1"],
|
|
});
|
|
const budgetedKey = await createKeyWithPolicy();
|
|
const policy = await loadPolicy("model-and-budget");
|
|
|
|
const disallowed = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(restrictedKey.key),
|
|
"anthropic/claude-3-7-sonnet"
|
|
);
|
|
assert.equal(disallowed.rejection.status, 403);
|
|
assert.match(await readErrorMessage(disallowed.rejection), /not allowed/);
|
|
|
|
const budgetMeta = await apiKeysDb.getApiKeyMetadata(budgetedKey.key);
|
|
costRules.setBudget(budgetMeta.id, { dailyLimitUsd: 1, warningThreshold: 0.5 });
|
|
costRules.recordCost(budgetMeta.id, 2);
|
|
|
|
const overBudget = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(budgetedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(overBudget.rejection.status, 429);
|
|
assert.match(await readErrorMessage(overBudget.rejection), /Daily budget exceeded/);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy returns Anthropic error envelope for /v1/messages model denials", async () => {
|
|
const restrictedKey = await createKeyWithPolicy({
|
|
allowedModels: ["cc/*"],
|
|
blockedModels: ["claude-fable*", "fable"],
|
|
});
|
|
const policy = await loadPolicy("anthropic-model-denial");
|
|
|
|
const denied = await policy.enforceApiKeyPolicy(
|
|
makeAnthropicPolicyRequest(restrictedKey.key),
|
|
"claude-fable-5"
|
|
);
|
|
|
|
assert.equal(denied.rejection.status, 400);
|
|
const body = await denied.rejection.json();
|
|
assert.equal(body.type, "error");
|
|
assert.equal(body.error.type, "invalid_request_error");
|
|
assert.match(body.error.message, /claude-fable-5/);
|
|
assert.equal(
|
|
body.error.message,
|
|
'Model "claude-fable-5" is not enabled or quota is insufficient. Choose another allowed model.'
|
|
);
|
|
assert.doesNotMatch(body.error.message, /login|authenticate|api key|credential|omniroute/i);
|
|
assert.equal(body.error.code, undefined);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy does not rate-limit unrestricted keys by default", async () => {
|
|
const unrestrictedKey = await createKeyWithPolicy({ allowedModels: ["openai/*"] });
|
|
const policy = await loadPolicy("default-no-request-limit");
|
|
|
|
for (let i = 0; i < 1005; i += 1) {
|
|
const result = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(unrestrictedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(result.rejection, null);
|
|
}
|
|
});
|
|
|
|
test("enforceApiKeyPolicy enforces explicit env fallback request limits", async () => {
|
|
process.env.DEFAULT_RATE_LIMIT_PER_DAY = "1";
|
|
const unrestrictedKey = await createKeyWithPolicy({ allowedModels: ["openai/*"] });
|
|
const policy = await loadPolicy("env-request-limit");
|
|
|
|
const first = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(unrestrictedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(first.rejection, null);
|
|
|
|
const second = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(unrestrictedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(second.rejection.status, 429);
|
|
assert.match(await readErrorMessage(second.rejection), /Request limit exceeded/);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy enforces custom multi-window rate limits", async () => {
|
|
const limitedKey = await createKeyWithPolicy({
|
|
allowedModels: ["openai/*"],
|
|
rateLimits: [{ limit: 1, window: 60 }],
|
|
});
|
|
const policy = await loadPolicy("custom-request-limits");
|
|
|
|
const first = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(limitedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(first.rejection, null);
|
|
|
|
const second = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(limitedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(second.rejection.status, 429);
|
|
assert.match(await readErrorMessage(second.rejection), /Request limit exceeded/);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy enforces combo allowlists separately from model allowlists", async () => {
|
|
const allowedKey = await createKeyWithPolicy({
|
|
allowedModels: ["openai/*"],
|
|
allowedCombos: ["fast-chat", "mapped-chat"],
|
|
});
|
|
const blockedKey = await createKeyWithPolicy({
|
|
allowedCombos: ["slow-chat"],
|
|
});
|
|
await combosDb.createCombo({
|
|
name: "fast-chat",
|
|
strategy: "priority",
|
|
models: ["anthropic/claude-3-5-sonnet"],
|
|
});
|
|
await combosDb.createCombo({
|
|
name: "mapped-chat",
|
|
strategy: "priority",
|
|
models: ["openai/gpt-4.1"],
|
|
});
|
|
const mappedCombo = await combosDb.getComboByName("mapped-chat");
|
|
assert.ok(mappedCombo?.id);
|
|
await modelComboMappingsDb.createModelComboMapping({
|
|
pattern: "mapped-model-*",
|
|
comboId: mappedCombo.id as string,
|
|
});
|
|
const policy = await loadPolicy("combo-access");
|
|
|
|
const allowed = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(allowedKey.key),
|
|
"combo/fast-chat"
|
|
);
|
|
assert.equal(allowed.rejection, null);
|
|
|
|
const blocked = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(blockedKey.key),
|
|
"combo/fast-chat"
|
|
);
|
|
assert.equal(blocked.rejection.status, 403);
|
|
assert.match(await readErrorMessage(blocked.rejection), /Combo "fast-chat" is not allowed/);
|
|
|
|
const mapped = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(allowedKey.key),
|
|
"mapped-model-1"
|
|
);
|
|
assert.equal(mapped.rejection, null);
|
|
});
|
|
|
|
test("new API keys allow all Combos explicitly", async () => {
|
|
const key = await apiKeysDb.createApiKey("Explicit Combo Default", "machine-607");
|
|
const stored = await apiKeysDb.getApiKeyMetadata(key.key);
|
|
|
|
assert.deepEqual(stored?.allowedCombos, ["combo/*"]);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy treats combo wildcard, empty list, and names as distinct access rules", async () => {
|
|
const allowAllKey = await createKeyWithPolicy({ allowedCombos: ["combo/*"] });
|
|
const denyAllKey = await createKeyWithPolicy({ allowedCombos: [] });
|
|
const allowNamedKey = await createKeyWithPolicy({ allowedCombos: ["fast-chat"] });
|
|
await combosDb.createCombo({
|
|
name: "fast-chat",
|
|
strategy: "priority",
|
|
models: ["openai/gpt-4.1"],
|
|
});
|
|
await combosDb.createCombo({
|
|
name: "slow-chat",
|
|
strategy: "priority",
|
|
models: ["anthropic/claude-3-5-sonnet"],
|
|
});
|
|
const policy = await loadPolicy("combo-access-modes");
|
|
|
|
const allowAll = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(allowAllKey.key),
|
|
"combo/slow-chat"
|
|
);
|
|
assert.equal(allowAll.rejection, null);
|
|
|
|
const denyAll = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(denyAllKey.key),
|
|
"combo/fast-chat"
|
|
);
|
|
assert.equal(denyAll.rejection.status, 403);
|
|
|
|
const allowNamed = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(allowNamedKey.key),
|
|
"combo/fast-chat"
|
|
);
|
|
assert.equal(allowNamed.rejection, null);
|
|
|
|
const denyOther = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(allowNamedKey.key),
|
|
"combo/slow-chat"
|
|
);
|
|
assert.equal(denyOther.rejection.status, 403);
|
|
|
|
const directModel = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(denyAllKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(directModel.rejection, null);
|
|
|
|
const routingRequest = makePolicyRequest(denyAllKey.key);
|
|
const routingCases = [
|
|
{ key: allowAllKey, model: "combo/slow-chat", status: null },
|
|
{ key: denyAllKey, model: "combo/fast-chat", status: 403 },
|
|
{ key: allowNamedKey, model: "combo/fast-chat", status: null },
|
|
{ key: allowNamedKey, model: "combo/slow-chat", status: 403 },
|
|
{ key: denyAllKey, model: "openai/gpt-4.1", status: null },
|
|
];
|
|
for (const routingCase of routingCases) {
|
|
const metadata = await apiKeysDb.getApiKeyMetadata(routingCase.key.key);
|
|
assert.ok(metadata);
|
|
const rejection = await policy.validateApiKeyRoutingTarget(
|
|
routingRequest,
|
|
routingCase.key.key,
|
|
metadata,
|
|
routingCase.model
|
|
);
|
|
assert.equal(rejection?.status ?? null, routingCase.status);
|
|
}
|
|
});
|
|
|
|
test("enforceApiKeyPolicy applies configured throttle delay", async () => {
|
|
const delayedKey = await createKeyWithPolicy({ throttleDelayMs: 25 });
|
|
const policy = await loadPolicy("throttle-delay");
|
|
|
|
const startedAt = Date.now();
|
|
const result = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(delayedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
|
|
assert.equal(result.rejection, null);
|
|
assert.equal(result.apiKeyInfo.throttleDelayMs, 25);
|
|
assert.ok(Date.now() - startedAt >= 20);
|
|
});
|
|
|
|
test("enforceApiKeyPolicy enforces request-per-minute limits and returns success when allowed", async () => {
|
|
const limitedKey = await createKeyWithPolicy({
|
|
allowedModels: ["openai/*"],
|
|
maxRequestsPerMinute: 1,
|
|
});
|
|
const policy = await loadPolicy("request-limits");
|
|
|
|
const first = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(limitedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(first.rejection, null);
|
|
assert.equal(first.apiKeyInfo.maxRequestsPerMinute, 1);
|
|
|
|
const second = await policy.enforceApiKeyPolicy(
|
|
makePolicyRequest(limitedKey.key),
|
|
"openai/gpt-4.1"
|
|
);
|
|
assert.equal(second.rejection.status, 429);
|
|
assert.match(await readErrorMessage(second.rejection), /Request limit exceeded/);
|
|
});
|