Files
2026-07-13 13:39:12 +08:00

543 lines
22 KiB
TypeScript

/**
* Tests for src/lib/db/apiKeys.ts — API key lifecycle, validation, caching, wildcard matching.
*
* Coverage targets:
* - createApiKey, getApiKeys, getApiKeyById
* - validateApiKey (env key, DB-backed, cache, banned/revoked/expired/inactive)
* - getApiKeyMetadata (env key, DB-backed, cache)
* - isModelAllowedForKey (no restrictions, exact, prefix, wildcard, group deny)
* - updateApiKeyPermissions (all field types, scopes with transaction)
* - deleteApiKey, revokeApiKey, setApiKeyExpiry, regenerateApiKey
* - clearApiKeyCaches, resetApiKeyState
* - matchesWildcardPattern (unit-level)
*/
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";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-apikeys-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-for-crc-operations-do-not-use-in-prod";
const core = await import("../../src/lib/db/core.ts");
const apiKeys = await import("../../src/lib/db/apiKeys.ts");
async function resetStorage() {
apiKeys.resetApiKeyState();
core.resetDbInstance();
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
break;
} catch {
await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
core.getDbInstance();
}
await resetStorage();
// ──────────────── createApiKey ────────────────
test("createApiKey creates a key and returns it with id, key, name, machineId", async () => {
await resetStorage();
const key = await apiKeys.createApiKey("Test Key", "machine-001");
assert.ok(key.id);
assert.ok(key.key);
assert.ok(key.key.startsWith("omni_") || key.key.length > 0);
assert.equal(key.name, "Test Key");
assert.equal(key.machineId, "machine-001");
});
test("createApiKey with scopes stores them", async () => {
await resetStorage();
const key = await apiKeys.createApiKey("Scoped Key", "machine-002", ["read", "write"]);
assert.ok(key.id);
assert.deepEqual(key.scopes, ["read", "write"]);
});
test("createApiKey rejects empty machineId", async () => {
await resetStorage();
await assert.rejects(
() => apiKeys.createApiKey("Bad Key", ""),
{ message: /machineId is required/i }
);
});
// ──────────────── getApiKeys ────────────────
test("getApiKeys returns empty array when no keys exist", async () => {
await resetStorage();
const keys = await apiKeys.getApiKeys();
assert.deepEqual(keys, []);
});
test("getApiKeys returns all created keys", async () => {
await resetStorage();
await apiKeys.createApiKey("Key A", "ma-001");
await apiKeys.createApiKey("Key B", "ma-002");
const all = await apiKeys.getApiKeys();
assert.equal(all.length, 2);
const names = all.map((k) => k.name).sort();
assert.deepEqual(names, ["Key A", "Key B"]);
});
// ──────────────── getApiKeyById ────────────────
test("getApiKeyById returns key by id", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Find Me", "ma-003");
const loaded = await apiKeys.getApiKeyById(created.id);
assert.ok(loaded !== null);
assert.equal(loaded!.name, "Find Me");
assert.equal(loaded!.id, created.id);
assert.equal(loaded!.machineId, "ma-003");
});
test("getApiKeyById returns null for missing id", async () => {
await resetStorage();
const loaded = await apiKeys.getApiKeyById("no-such-id");
assert.equal(loaded, null);
});
// ──────────────── validateApiKey ────────────────
test("validateApiKey returns false for null / undefined / empty", async () => {
await resetStorage();
assert.equal(await apiKeys.validateApiKey(null), false);
assert.equal(await apiKeys.validateApiKey(undefined), false);
assert.equal(await apiKeys.validateApiKey(""), false);
});
test("validateApiKey returns true for env key", async () => {
await resetStorage();
const prev = process.env.OMNIROUTE_API_KEY;
process.env.OMNIROUTE_API_KEY = "env-key-test-abc123";
try {
assert.equal(await apiKeys.validateApiKey("env-key-test-abc123"), true);
} finally {
process.env.OMNIROUTE_API_KEY = prev;
}
});
test("validateApiKey returns true for valid key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Valid Key", "ma-004");
assert.equal(await apiKeys.validateApiKey(created.key), true);
});
test("validateApiKey returns false for non-existent key", async () => {
await resetStorage();
assert.equal(await apiKeys.validateApiKey("omni_nonexistent_key_abc123"), false);
});
test("validateApiKey returns false for banned key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Banned Soon", "ma-005");
await apiKeys.updateApiKeyPermissions(created.id, { isBanned: true });
// Caches may still hold the old valid state — validateApiKey should check DB
// after cache miss; call resetApiKeyState() to clear caches for a fresh read.
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.validateApiKey(created.key), false);
});
test("validateApiKey returns false for revoked key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Revoked Soon", "ma-006");
await apiKeys.revokeApiKey(created.id);
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.validateApiKey(created.key), false);
});
test("validateApiKey returns false for expired key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Expiring Soon", "ma-007");
await apiKeys.setApiKeyExpiry(created.id, "2020-01-01T00:00:00Z");
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.validateApiKey(created.key), false);
});
test("validateApiKey returns false for inactive key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Inactive Soon", "ma-008");
await apiKeys.updateApiKeyPermissions(created.id, { isActive: false });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.validateApiKey(created.key), false);
});
// ──────────────── getApiKeyMetadata ────────────────
test("getApiKeyMetadata returns null for null / undefined / empty", async () => {
await resetStorage();
assert.equal(await apiKeys.getApiKeyMetadata(null), null);
assert.equal(await apiKeys.getApiKeyMetadata(undefined), null);
assert.equal(await apiKeys.getApiKeyMetadata(""), null);
});
test("getApiKeyMetadata returns env-key record for env key", async () => {
await resetStorage();
const prev = process.env.OMNIROUTE_API_KEY;
process.env.OMNIROUTE_API_KEY = "env-key-meta-001";
try {
const meta = await apiKeys.getApiKeyMetadata("env-key-meta-001");
assert.ok(meta !== null);
assert.equal(meta!.id, "env-key");
assert.equal(meta!.name, "Environment Key");
assert.ok(meta!.scopes.includes("manage"));
assert.equal(meta!.isActive, true);
} finally {
process.env.OMNIROUTE_API_KEY = prev;
}
});
test("getApiKeyMetadata returns metadata for valid key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Meta Key", "ma-009");
const meta = await apiKeys.getApiKeyMetadata(created.key);
assert.ok(meta !== null);
assert.equal(meta!.id, created.id);
assert.equal(meta!.name, "Meta Key");
assert.equal(meta!.machineId, "ma-009");
assert.deepEqual(meta!.allowedModels, []);
assert.equal(meta!.noLog, false);
assert.equal(meta!.isActive, true);
});
test("getApiKeyMetadata returns null for non-existent key", async () => {
await resetStorage();
assert.equal(await apiKeys.getApiKeyMetadata("omni_meta_nonexistent"), null);
});
// ──────────────── isModelAllowedForKey ────────────────
test("isModelAllowedForKey returns true when no key provided", async () => {
await resetStorage();
assert.equal(await apiKeys.isModelAllowedForKey(null, "gpt-4"), true);
assert.equal(await apiKeys.isModelAllowedForKey(undefined, "gpt-4"), true);
});
test("isModelAllowedForKey returns false when no modelId provided", async () => {
await resetStorage();
assert.equal(await apiKeys.isModelAllowedForKey("some-key", null), false);
assert.equal(await apiKeys.isModelAllowedForKey("some-key", undefined), false);
});
test("isModelAllowedForKey returns false for non-existent key", async () => {
await resetStorage();
assert.equal(await apiKeys.isModelAllowedForKey("omni_bogus_key", "gpt-4"), false);
});
test("isModelAllowedForKey returns true when allowedModels is unrestricted (empty)", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Unrestricted", "ma-010");
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-4"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "claude-opus-4"), true);
});
test("isModelAllowedForKey exact match", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Exact", "ma-011");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["gpt-4", "claude-3"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-4"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "claude-3"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-5"), false);
});
test("isModelAllowedForKey prefix match (/*)", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Prefix", "ma-012");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["openai/*"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-4"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-5"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "anthropic/claude-3"), false);
});
test("isModelAllowedForKey wildcard match", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Wildcard", "ma-013");
// Pattern "gpt-*-turbo" should match "gpt-4-turbo" but not "gpt-4"
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["gpt-*-turbo"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-4-turbo"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-4"), false);
});
// ──────────────── updateApiKeyPermissions ────────────────
test("updateApiKeyPermissions updates name", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Old Name", "ma-020");
const result = await apiKeys.updateApiKeyPermissions(created.id, { name: "New Name" });
assert.equal(result, true);
const loaded = await apiKeys.getApiKeyById(created.id);
assert.equal(loaded!.name, "New Name");
});
test("updateApiKeyPermissions toggles isActive", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Toggle Key", "ma-021");
assert.equal((await apiKeys.getApiKeyById(created.id))!.isActive, true);
await apiKeys.updateApiKeyPermissions(created.id, { isActive: false });
assert.equal((await apiKeys.getApiKeyById(created.id))!.isActive, false);
await apiKeys.updateApiKeyPermissions(created.id, { isActive: true });
assert.equal((await apiKeys.getApiKeyById(created.id))!.isActive, true);
});
test("updateApiKeyPermissions toggles noLog", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("NoLog Key", "ma-022");
await apiKeys.updateApiKeyPermissions(created.id, { noLog: true });
assert.equal((await apiKeys.getApiKeyById(created.id))!.noLog, true);
});
test("updateApiKeyPermissions sets isBanned", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Ban Key", "ma-023");
await apiKeys.updateApiKeyPermissions(created.id, { isBanned: true });
assert.equal((await apiKeys.getApiKeyById(created.id))!.isBanned, true);
await apiKeys.updateApiKeyPermissions(created.id, { isBanned: false });
assert.equal((await apiKeys.getApiKeyById(created.id))!.isBanned, false);
});
test("updateApiKeyPermissions sets accessSchedule", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Scheduled", "ma-024");
const schedule = {
enabled: true,
from: "09:00",
until: "17:00",
days: [1, 2, 3, 4, 5],
tz: "America/New_York",
};
await apiKeys.updateApiKeyPermissions(created.id, { accessSchedule: schedule });
const loaded = await apiKeys.getApiKeyById(created.id);
assert.deepEqual(loaded!.accessSchedule, schedule);
});
test("updateApiKeyPermissions clears accessSchedule with null", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Clear Schedule", "ma-025");
await apiKeys.updateApiKeyPermissions(created.id, {
accessSchedule: { enabled: true, from: "00:00", until: "23:59", days: [0], tz: "UTC" },
});
assert.ok((await apiKeys.getApiKeyById(created.id))!.accessSchedule !== null);
await apiKeys.updateApiKeyPermissions(created.id, { accessSchedule: null });
assert.equal((await apiKeys.getApiKeyById(created.id))!.accessSchedule, null);
});
test("updateApiKeyPermissions sets rateLimits", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Rate Limited", "ma-026");
const limits = [{ limit: 100, window: 60 }, { limit: 1000, window: 3600 }];
await apiKeys.updateApiKeyPermissions(created.id, { rateLimits: limits });
const loaded = await apiKeys.getApiKeyById(created.id);
assert.deepEqual(loaded!.rateLimits, limits);
});
test("updateApiKeyPermissions sets maxSessions", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Session Limit", "ma-027");
await apiKeys.updateApiKeyPermissions(created.id, { maxSessions: 5 });
// maxSessions is not exposed in the getApiKeyById return type directly,
// but is part of metadata — verify via getApiKeyMetadata
const meta = await apiKeys.getApiKeyMetadata(created.key);
assert.ok(meta);
assert.equal(meta!.maxSessions, 5);
});
test("updateApiKeyPermissions with legacy array format sets allowedModels", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Legacy Format", "ma-028");
const result = await apiKeys.updateApiKeyPermissions(created.id, ["model-a", "model-b"]);
assert.equal(result, true);
const loaded = await apiKeys.getApiKeyById(created.id);
assert.deepEqual(loaded!.allowedModels, ["model-a", "model-b"]);
});
test("updateApiKeyPermissions updates scopes", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Scopes Update", "ma-029");
const result = await apiKeys.updateApiKeyPermissions(created.id, { scopes: ["read", "manage"] });
assert.equal(result, true);
const loaded = await apiKeys.getApiKeyById(created.id);
assert.deepEqual(loaded!.scopes, ["read", "manage"]);
});
test("updateApiKeyPermissions empty update returns false", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("No Changes", "ma-030");
const result = await apiKeys.updateApiKeyPermissions(created.id, {});
assert.equal(result, false);
});
test("updateApiKeyPermissions non-existent id returns false", async () => {
await resetStorage();
const result = await apiKeys.updateApiKeyPermissions("no-such-key", {
name: "Wont Work",
});
assert.equal(result, false);
});
// ──────────────── deleteApiKey ────────────────
test("deleteApiKey deletes existing key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Delete Me", "ma-040");
const result = await apiKeys.deleteApiKey(created.id);
assert.equal(result, true);
const loaded = await apiKeys.getApiKeyById(created.id);
assert.equal(loaded, null);
});
test("deleteApiKey non-existent id returns false", async () => {
await resetStorage();
const result = await apiKeys.deleteApiKey("no-such-id");
assert.equal(result, false);
});
// ──────────────── revokeApiKey ────────────────
test("revokeApiKey revokes existing key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Revoke Me", "ma-050");
const result = await apiKeys.revokeApiKey(created.id);
assert.equal(result, true);
const loaded = await apiKeys.getApiKeyById(created.id);
assert.ok(loaded !== null);
assert.equal(loaded!.isActive, false);
assert.ok(loaded!.revokedAt);
});
test("revokeApiKey non-existent id returns false", async () => {
await resetStorage();
assert.equal(await apiKeys.revokeApiKey("no-such-id"), false);
});
test("revokeApiKey double revoke is idempotent", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Double Revoke", "ma-051");
assert.equal(await apiKeys.revokeApiKey(created.id), true);
assert.equal(await apiKeys.revokeApiKey(created.id), true);
});
// ──────────────── setApiKeyExpiry ────────────────
test("setApiKeyExpiry sets expiry on existing key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Expiry Test", "ma-060");
const result = await apiKeys.setApiKeyExpiry(created.id, "2030-12-31T23:59:59Z");
assert.equal(result, true);
const meta = await apiKeys.getApiKeyMetadata(created.key);
assert.ok(meta);
assert.equal(meta!.expiresAt, "2030-12-31T23:59:59Z");
});
test("setApiKeyExpiry clears expiry with null", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Clear Expiry", "ma-061");
await apiKeys.setApiKeyExpiry(created.id, "2030-01-01T00:00:00Z");
await apiKeys.setApiKeyExpiry(created.id, null);
const meta = await apiKeys.getApiKeyMetadata(created.key);
assert.ok(meta);
assert.equal(meta!.expiresAt, null);
});
test("setApiKeyExpiry non-existent id returns false", async () => {
await resetStorage();
assert.equal(await apiKeys.setApiKeyExpiry("no-such-id", "2030-01-01T00:00:00Z"), false);
});
// ──────────────── regenerateApiKey ────────────────
test("regenerateApiKey regenerates key", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Regen Key", "ma-070");
const oldKey = created.key;
const result = await apiKeys.regenerateApiKey(created.id);
assert.ok(result !== null);
assert.equal(result!.id, created.id);
assert.ok(result!.key);
assert.notEqual(result!.key, oldKey);
});
test("regenerateApiKey non-existent id returns null", async () => {
await resetStorage();
assert.equal(await apiKeys.regenerateApiKey("no-such-id"), null);
});
// ──────────────── clearApiKeyCaches / resetApiKeyState ────────────────
test("clearApiKeyCaches and resetApiKeyState do not throw", async () => {
await resetStorage();
// Fill caches with some activity
const created = await apiKeys.createApiKey("Cache Test", "ma-080");
await apiKeys.validateApiKey(created.key);
await apiKeys.getApiKeyMetadata(created.key);
// Should not throw
apiKeys.clearApiKeyCaches();
apiKeys.resetApiKeyState();
assert.ok(true);
});
// ──────────────── matchesWildcardPattern (unit-level) ────────────────
test("matchesWildcardPattern exact match", async () => {
// Directly test the underlying function logic via isModelAllowedForKey
await resetStorage();
const created = await apiKeys.createApiKey("Wild Exact", "ma-090");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["openai/gpt-4"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-4"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/claude"), false);
});
test("isModelAllowedForKey segment count mismatch via matchesWildcardPattern", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Wild Segments", "ma-091");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["openai/gpt-*"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-4"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-4/sub"), false);
});
test("matchesWildcardPattern wildcard within segment", async () => {
// Pattern "gpt-*-turbo" should match "gpt-4-turbo" but not "gpt-4"
await resetStorage();
const created = await apiKeys.createApiKey("Wild Within", "ma-092");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["gpt-*-turbo"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-4-turbo"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-5-turbo"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "gpt-4"), false);
});
test("matchesWildcardPattern pattern with only *", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Wild Star", "ma-093");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["openai/*"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/anything"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/"), true);
});
test("matchesWildcardPattern multiple segments with wildcards", async () => {
await resetStorage();
const created = await apiKeys.createApiKey("Wild Multi", "ma-094");
await apiKeys.updateApiKeyPermissions(created.id, { allowedModels: ["openai/*/turbo"] });
apiKeys.resetApiKeyState();
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-4/turbo"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-5/turbo"), true);
assert.equal(await apiKeys.isModelAllowedForKey(created.key, "openai/gpt-4"), false);
});