Files
metapi/src/server/services/downstreamApiKeyService.test.ts
T

210 lines
7.7 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
type DbModule = typeof import('../db/index.js');
type ServiceModule = typeof import('./downstreamApiKeyService.js');
type ConfigModule = typeof import('../config.js');
describe('downstreamApiKeyService', () => {
let db: DbModule['db'];
let schema: DbModule['schema'];
let service: ServiceModule;
let config: ConfigModule['config'];
let dataDir = '';
beforeAll(async () => {
dataDir = mkdtempSync(join(tmpdir(), 'metapi-downstream-key-'));
process.env.DATA_DIR = dataDir;
await import('../db/migrate.js');
const dbModule = await import('../db/index.js');
const configModule = await import('../config.js');
const serviceModule = await import('./downstreamApiKeyService.js');
db = dbModule.db;
schema = dbModule.schema;
config = configModule.config;
service = serviceModule;
});
beforeEach(async () => {
await db.delete(schema.downstreamApiKeys).run();
await db.delete(schema.tokenRoutes).run();
config.proxyToken = 'sk-global-proxy-token';
});
afterAll(() => {
delete process.env.DATA_DIR;
});
it('authorizes global proxy token when no managed key matches', async () => {
const result = await service.authorizeDownstreamToken('sk-global-proxy-token');
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.key).toBeNull();
expect(result.policy.allowedRouteIds).toEqual([]);
expect(result.policy.supportedModels).toEqual([]);
}
});
it('rejects managed keys by lifecycle guards (disabled, expired, over budget, over requests)', async () => {
const now = Date.now();
const disabled = await db.insert(schema.downstreamApiKeys).values({
name: 'disabled',
key: 'sk-disabled',
enabled: false,
}).returning().get();
const expired = await db.insert(schema.downstreamApiKeys).values({
name: 'expired',
key: 'sk-expired',
enabled: true,
expiresAt: new Date(now - 60_000).toISOString(),
}).returning().get();
const overBudget = await db.insert(schema.downstreamApiKeys).values({
name: 'over-budget',
key: 'sk-over-budget',
enabled: true,
maxCost: 1,
usedCost: 1.2,
}).returning().get();
const overRequests = await db.insert(schema.downstreamApiKeys).values({
name: 'over-requests',
key: 'sk-over-requests',
enabled: true,
maxRequests: 10,
usedRequests: 10,
}).returning().get();
const r1 = await service.authorizeDownstreamToken(disabled.key);
const r2 = await service.authorizeDownstreamToken(expired.key);
const r3 = await service.authorizeDownstreamToken(overBudget.key);
const r4 = await service.authorizeDownstreamToken(overRequests.key);
expect(r1.ok).toBe(false);
expect(r2.ok).toBe(false);
expect(r3.ok).toBe(false);
expect(r4.ok).toBe(false);
});
it('parses policy fields and supports model matching patterns', async () => {
const row = await db.insert(schema.downstreamApiKeys).values({
name: 'project-a',
key: 'sk-project-a',
enabled: true,
supportedModels: JSON.stringify(['re:^claude-(opus|sonnet)-4-6$', 'gpt-4o-mini']),
allowedRouteIds: JSON.stringify([101, 102]),
siteWeightMultipliers: JSON.stringify({ '1': 2.5, '7': 0.4 }),
}).returning().get();
const result = await service.authorizeDownstreamToken(row.key);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.key?.id).toBe(row.id);
expect(result.policy.allowedRouteIds).toEqual([101, 102]);
expect(result.policy.siteWeightMultipliers[1]).toBeCloseTo(2.5);
expect(result.policy.siteWeightMultipliers[7]).toBeCloseTo(0.4);
expect(service.isModelAllowedByPolicy('claude-opus-4-6', result.policy)).toBe(true);
expect(service.isModelAllowedByPolicy('gpt-4o-mini', result.policy)).toBe(true);
expect(service.isModelAllowedByPolicy('gemini-2.0-flash', result.policy)).toBe(false);
});
it('treats selected groups as additional allowed exposed route scope (union semantics)', async () => {
const claudeGroup = await db.insert(schema.tokenRoutes).values({
modelPattern: 're:^claude-(opus|sonnet)-4-6$',
displayName: 'claude-4-6-group',
enabled: true,
}).returning().get();
const policy = {
supportedModels: ['gpt-4o-mini'],
allowedRouteIds: [claudeGroup.id],
siteWeightMultipliers: {},
};
expect(service.isModelAllowedByPolicy('claude-4-6-group', policy)).toBe(false);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-4-6-group', policy)).toBe(true);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-opus-4-6', policy)).toBe(false);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('gpt-4o-mini', policy)).toBe(true);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('gemini-2.0-flash', policy)).toBe(false);
});
it('denies all models when both supportedModels and allowedRouteIds are empty', async () => {
const policy = {
supportedModels: [],
allowedRouteIds: [],
siteWeightMultipliers: {},
denyAllWhenEmpty: true,
};
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('gpt-4o-mini', policy)).toBe(false);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-opus-4-6', policy)).toBe(false);
});
it('authorizes by selected group model pattern only, not arbitrary internal models', async () => {
const virtualModelGroup = await db.insert(schema.tokenRoutes).values({
modelPattern: 'claude-opus-4-6',
enabled: true,
}).returning().get();
const policy = {
supportedModels: [],
allowedRouteIds: [virtualModelGroup.id],
siteWeightMultipliers: {},
};
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-opus-4-6', policy)).toBe(true);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-sonnet-4-6', policy)).toBe(false);
});
it('only authorizes selected route display name alias, not models covered by group pattern', async () => {
const aliasRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 're:^claude-(opus|sonnet)-4-5$',
displayName: 'claude-opus-4-6',
enabled: true,
}).returning().get();
const policy = {
supportedModels: [],
allowedRouteIds: [aliasRoute.id],
siteWeightMultipliers: {},
};
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-opus-4-6', policy)).toBe(true);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-sonnet-4-5', policy)).toBe(false);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('claude-opus-4-5', policy)).toBe(false);
expect(await service.isModelAllowedByPolicyOrAllowedRoutes('gpt-4o-mini', policy)).toBe(false);
});
it('accumulates managed key request/cost usage and applies limits', async () => {
const row = await db.insert(schema.downstreamApiKeys).values({
name: 'metered-key',
key: 'sk-metered-key',
enabled: true,
maxRequests: 2,
maxCost: 1,
usedRequests: 0,
usedCost: 0,
}).returning().get();
await service.consumeManagedKeyRequest(row.id);
await service.consumeManagedKeyRequest(row.id);
await service.recordManagedKeyCostUsage(row.id, 0.4);
await service.recordManagedKeyCostUsage(row.id, 0.6);
const latest = await service.getDownstreamApiKeyById(row.id);
expect(latest?.usedRequests).toBe(2);
expect(latest?.usedCost).toBeCloseTo(1);
const authResult = await service.authorizeDownstreamToken(row.key);
expect(authResult.ok).toBe(false);
});
});