feat(tests): add comprehensive tests for TokenRouter and related components

- Implement tests for TokenRouter downstream policy, covering allowedRouteIds and site weight multipliers.
- Add tests for TokenRouter patterns and model mapping, including regex pattern matching and model mapping precedence.
- Create tests for BrandIcon component to verify brand detection logic.
- Develop tests for Accounts page, focusing on rebind panel behavior and refresh action on error.
- Introduce fuzzy search utility tests to validate substring and subsequence matching.
- Implement navigation focus helpers tests for building and parsing focus paths.
- Add progressive render tests to ensure correct initial and next visible counts.
- Create route model candidates index tests to validate route candidate indexing and matching logic.
This commit is contained in:
cita
2026-03-02 13:33:35 +08:00
parent 99c9e145c3
commit f6389a999a
98 changed files with 11286 additions and 801 deletions
@@ -0,0 +1,449 @@
import { and, eq, inArray } from 'drizzle-orm';
import { minimatch } from 'minimatch';
import { db, schema } from '../db/index.js';
import { config } from '../config.js';
import { EMPTY_DOWNSTREAM_ROUTING_POLICY, type DownstreamRoutingPolicy } from './downstreamPolicyTypes.js';
export type DownstreamApiKeyRow = typeof schema.downstreamApiKeys.$inferSelect;
export type DownstreamApiKeyPolicyView = {
id: number;
name: string;
key: string;
keyMasked: string;
description: string | null;
enabled: boolean;
expiresAt: string | null;
maxCost: number | null;
usedCost: number;
maxRequests: number | null;
usedRequests: number;
supportedModels: string[];
allowedRouteIds: number[];
siteWeightMultipliers: Record<number, number>;
lastUsedAt: string | null;
createdAt: string | null;
updatedAt: string | null;
};
export type DownstreamTokenAuthSuccess = {
ok: true;
source: 'managed' | 'global';
token: string;
key: DownstreamApiKeyPolicyView | null;
policy: DownstreamRoutingPolicy;
};
export type DownstreamTokenAuthFailure = {
ok: false;
statusCode: number;
error: string;
reason: 'missing' | 'invalid' | 'disabled' | 'expired' | 'over_cost' | 'over_requests';
};
export type DownstreamTokenAuthResult = DownstreamTokenAuthSuccess | DownstreamTokenAuthFailure;
function isRegexModelPattern(pattern: string): boolean {
return pattern.trim().toLowerCase().startsWith('re:');
}
function parseRegexModelPattern(pattern: string): RegExp | null {
if (!isRegexModelPattern(pattern)) return null;
const body = pattern.trim().slice(3).trim();
if (!body) return null;
try {
return new RegExp(body);
} catch {
return null;
}
}
function normalizeToken(raw: string): string {
return (raw || '').trim();
}
function maskSecret(value: string): string {
if (!value) return '';
if (value.length <= 8) return '****';
return `${value.slice(0, 4)}****${value.slice(-4)}`;
}
function normalizePositiveNumberOrNull(value: unknown): number | null {
if (value === null || value === undefined || value === '') return null;
const n = Number(value);
if (!Number.isFinite(n) || n < 0) return null;
return n;
}
function normalizePositiveIntegerOrNull(value: unknown): number | null {
if (value === null || value === undefined || value === '') return null;
const n = Number(value);
if (!Number.isFinite(n)) return null;
const normalized = Math.trunc(n);
if (normalized < 0) return null;
return normalized;
}
function parseJson(value: string | null | undefined): unknown {
if (!value) return null;
try {
return JSON.parse(value);
} catch {
return null;
}
}
export function normalizeSupportedModelsInput(input: unknown): string[] {
if (Array.isArray(input)) {
return input
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter((item, index, arr) => item.length > 0 && arr.indexOf(item) === index)
.slice(0, 200);
}
if (typeof input === 'string') {
return input
.split(/\r?\n|,/g)
.map((item) => item.trim())
.filter((item, index, arr) => item.length > 0 && arr.indexOf(item) === index)
.slice(0, 200);
}
return [];
}
export function normalizeAllowedRouteIdsInput(input: unknown): number[] {
const rawValues = Array.isArray(input)
? input
: (typeof input === 'string' ? input.split(/\r?\n|,/g) : []);
const routeIds: number[] = [];
for (const item of rawValues) {
const n = Number(item);
if (!Number.isFinite(n)) continue;
const normalized = Math.trunc(n);
if (normalized <= 0 || routeIds.includes(normalized)) continue;
routeIds.push(normalized);
if (routeIds.length >= 500) break;
}
return routeIds;
}
export function normalizeSiteWeightMultipliersInput(input: unknown): Record<number, number> {
const raw = (typeof input === 'string')
? parseJson(input)
: input;
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return {};
}
const result: Record<number, number> = {};
for (const [rawSiteId, rawMultiplier] of Object.entries(raw as Record<string, unknown>)) {
const siteId = Number(rawSiteId);
const multiplier = Number(rawMultiplier);
if (!Number.isFinite(siteId) || !Number.isFinite(multiplier)) continue;
const normalizedSiteId = Math.trunc(siteId);
if (normalizedSiteId <= 0 || multiplier <= 0) continue;
result[normalizedSiteId] = multiplier;
}
return result;
}
export function matchesDownstreamModelPattern(model: string, pattern: string): boolean {
const normalizedPattern = (pattern || '').trim();
if (!normalizedPattern) return false;
if (normalizedPattern === model) return true;
if (isRegexModelPattern(normalizedPattern)) {
const re = parseRegexModelPattern(normalizedPattern);
return !!re && re.test(model);
}
return minimatch(model, normalizedPattern);
}
export function isModelAllowedByPolicy(model: string, policy: DownstreamRoutingPolicy): boolean {
const patterns = Array.isArray(policy.supportedModels)
? policy.supportedModels
: [];
if (patterns.length === 0) return true;
return patterns.some((pattern) => matchesDownstreamModelPattern(model, pattern));
}
function isModelMatchedByAllowedRoutes(model: string, allowedRouteIds: number[]): boolean {
if (allowedRouteIds.length === 0) return false;
const routes = db.select({
id: schema.tokenRoutes.id,
modelPattern: schema.tokenRoutes.modelPattern,
displayName: schema.tokenRoutes.displayName,
})
.from(schema.tokenRoutes)
.where(and(
inArray(schema.tokenRoutes.id, allowedRouteIds),
eq(schema.tokenRoutes.enabled, true),
))
.all();
return routes.some((route) => {
if (matchesDownstreamModelPattern(model, route.modelPattern)) return true;
const alias = (route.displayName || '').trim();
return !!alias && alias === model;
});
}
export function isModelAllowedByPolicyOrAllowedRoutes(model: string, policy: DownstreamRoutingPolicy): boolean {
const patterns = normalizeSupportedModelsInput(policy.supportedModels);
const allowedRouteIds = normalizeAllowedRouteIdsInput(policy.allowedRouteIds);
const hasPatternRules = patterns.length > 0;
const hasRouteRules = allowedRouteIds.length > 0;
if (!hasPatternRules && !hasRouteRules) return true;
if (hasPatternRules && patterns.some((pattern) => matchesDownstreamModelPattern(model, pattern))) {
return true;
}
if (!hasRouteRules) return false;
return isModelMatchedByAllowedRoutes(model, allowedRouteIds);
}
export function toDownstreamApiKeyPolicyView(row: DownstreamApiKeyRow): DownstreamApiKeyPolicyView {
const supportedModels = normalizeSupportedModelsInput(parseJson(row.supportedModels));
const allowedRouteIds = normalizeAllowedRouteIdsInput(parseJson(row.allowedRouteIds));
const siteWeightMultipliers = normalizeSiteWeightMultipliersInput(parseJson(row.siteWeightMultipliers));
return {
id: row.id,
name: row.name,
key: row.key,
keyMasked: maskSecret(row.key),
description: row.description || null,
enabled: !!row.enabled,
expiresAt: row.expiresAt || null,
maxCost: row.maxCost ?? null,
usedCost: Number(row.usedCost || 0),
maxRequests: row.maxRequests ?? null,
usedRequests: Number(row.usedRequests || 0),
supportedModels,
allowedRouteIds,
siteWeightMultipliers,
lastUsedAt: row.lastUsedAt || null,
createdAt: row.createdAt || null,
updatedAt: row.updatedAt || null,
};
}
export function toPolicyFromView(view: Pick<DownstreamApiKeyPolicyView, 'supportedModels' | 'allowedRouteIds' | 'siteWeightMultipliers'>): DownstreamRoutingPolicy {
return {
supportedModels: normalizeSupportedModelsInput(view.supportedModels),
allowedRouteIds: normalizeAllowedRouteIdsInput(view.allowedRouteIds),
siteWeightMultipliers: normalizeSiteWeightMultipliersInput(view.siteWeightMultipliers),
};
}
export function listDownstreamApiKeys(): DownstreamApiKeyPolicyView[] {
return db.select().from(schema.downstreamApiKeys)
.all()
.map((row) => toDownstreamApiKeyPolicyView(row))
.sort((a, b) => b.id - a.id);
}
export function getDownstreamApiKeyById(id: number): DownstreamApiKeyPolicyView | null {
const row = db.select().from(schema.downstreamApiKeys)
.where(eq(schema.downstreamApiKeys.id, id))
.get();
if (!row) return null;
return toDownstreamApiKeyPolicyView(row);
}
export function getManagedDownstreamApiKeyByToken(token: string): DownstreamApiKeyPolicyView | null {
const normalizedToken = normalizeToken(token);
if (!normalizedToken) return null;
const row = db.select().from(schema.downstreamApiKeys)
.where(eq(schema.downstreamApiKeys.key, normalizedToken))
.get();
if (!row) return null;
return toDownstreamApiKeyPolicyView(row);
}
export function getDefaultGlobalPolicy(): DownstreamRoutingPolicy {
return EMPTY_DOWNSTREAM_ROUTING_POLICY;
}
export function authorizeDownstreamToken(token: string): DownstreamTokenAuthResult {
const normalizedToken = normalizeToken(token);
if (!normalizedToken) {
return {
ok: false,
statusCode: 401,
error: 'Missing Authorization or x-api-key header',
reason: 'missing',
};
}
const managed = getManagedDownstreamApiKeyByToken(normalizedToken);
if (managed) {
if (!managed.enabled) {
return {
ok: false,
statusCode: 403,
error: 'API key is disabled',
reason: 'disabled',
};
}
if (managed.expiresAt) {
const expiresAtTs = Date.parse(managed.expiresAt);
if (Number.isFinite(expiresAtTs) && expiresAtTs <= Date.now()) {
return {
ok: false,
statusCode: 403,
error: 'API key is expired',
reason: 'expired',
};
}
}
if (managed.maxCost !== null && managed.usedCost >= managed.maxCost) {
return {
ok: false,
statusCode: 403,
error: 'API key has exceeded max cost',
reason: 'over_cost',
};
}
if (managed.maxRequests !== null && managed.usedRequests >= managed.maxRequests) {
return {
ok: false,
statusCode: 403,
error: 'API key has exceeded max requests',
reason: 'over_requests',
};
}
return {
ok: true,
source: 'managed',
token: normalizedToken,
key: managed,
policy: toPolicyFromView(managed),
};
}
if (normalizedToken === config.proxyToken) {
return {
ok: true,
source: 'global',
token: normalizedToken,
key: null,
policy: getDefaultGlobalPolicy(),
};
}
return {
ok: false,
statusCode: 403,
error: 'Invalid API key',
reason: 'invalid',
};
}
export function consumeManagedKeyRequest(keyId: number): void {
const row = db.select().from(schema.downstreamApiKeys)
.where(eq(schema.downstreamApiKeys.id, keyId))
.get();
if (!row) return;
db.update(schema.downstreamApiKeys).set({
usedRequests: Math.max(0, (row.usedRequests || 0) + 1),
lastUsedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}).where(eq(schema.downstreamApiKeys.id, keyId)).run();
}
export function recordManagedKeyCostUsage(keyId: number, estimatedCost: number): void {
const cost = Number(estimatedCost);
if (!Number.isFinite(cost) || cost <= 0) return;
const row = db.select().from(schema.downstreamApiKeys)
.where(eq(schema.downstreamApiKeys.id, keyId))
.get();
if (!row) return;
db.update(schema.downstreamApiKeys).set({
usedCost: Math.max(0, (row.usedCost || 0) + cost),
lastUsedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}).where(eq(schema.downstreamApiKeys.id, keyId)).run();
}
export function normalizeDownstreamApiKeyPayload(input: {
name?: unknown;
key?: unknown;
description?: unknown;
enabled?: unknown;
expiresAt?: unknown;
maxCost?: unknown;
maxRequests?: unknown;
supportedModels?: unknown;
allowedRouteIds?: unknown;
siteWeightMultipliers?: unknown;
}) {
const name = typeof input.name === 'string' ? input.name.trim() : '';
const key = typeof input.key === 'string' ? input.key.trim() : '';
const description = typeof input.description === 'string'
? input.description.trim()
: '';
const enabled = input.enabled === undefined ? true : !!input.enabled;
const expiresAtRaw = typeof input.expiresAt === 'string'
? input.expiresAt.trim()
: (input.expiresAt === null ? '' : '');
let expiresAt: string | null = null;
if (expiresAtRaw) {
const ts = Date.parse(expiresAtRaw);
if (!Number.isFinite(ts)) {
throw new Error('expiresAt 必须是有效时间');
}
expiresAt = new Date(ts).toISOString();
}
const maxCost = normalizePositiveNumberOrNull(input.maxCost);
const maxRequests = normalizePositiveIntegerOrNull(input.maxRequests);
const supportedModels = normalizeSupportedModelsInput(input.supportedModels);
const allowedRouteIds = normalizeAllowedRouteIdsInput(input.allowedRouteIds);
const siteWeightMultipliers = normalizeSiteWeightMultipliersInput(input.siteWeightMultipliers);
return {
name,
key,
description: description || null,
enabled,
expiresAt,
maxCost,
maxRequests,
supportedModels,
allowedRouteIds,
siteWeightMultipliers,
};
}
export function toPersistenceJson(value: unknown): string | null {
if (value === null || value === undefined) return null;
if (Array.isArray(value) && value.length === 0) return null;
if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value as Record<string, unknown>).length === 0) {
return null;
}
return JSON.stringify(value);
}