forked from boos/BoosAPI
257 lines
7.9 KiB
TypeScript
257 lines
7.9 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';
|
|
import { sql } from 'drizzle-orm';
|
|
|
|
type DbModule = typeof import('../db/index.js');
|
|
type TokenRouterModule = typeof import('./tokenRouter.js');
|
|
|
|
describe('TokenRouter site status guard', () => {
|
|
let db: DbModule['db'];
|
|
let schema: DbModule['schema'];
|
|
let TokenRouter: TokenRouterModule['TokenRouter'];
|
|
let invalidateTokenRouterCache: TokenRouterModule['invalidateTokenRouterCache'];
|
|
let dataDir = '';
|
|
|
|
beforeAll(async () => {
|
|
dataDir = mkdtempSync(join(tmpdir(), 'metapi-token-router-site-status-'));
|
|
process.env.DATA_DIR = dataDir;
|
|
|
|
await import('../db/migrate.js');
|
|
const dbModule = await import('../db/index.js');
|
|
const tokenRouterModule = await import('./tokenRouter.js');
|
|
db = dbModule.db;
|
|
schema = dbModule.schema;
|
|
TokenRouter = tokenRouterModule.TokenRouter;
|
|
invalidateTokenRouterCache = tokenRouterModule.invalidateTokenRouterCache;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await db.delete(schema.routeChannels).run();
|
|
await db.delete(schema.tokenRoutes).run();
|
|
await db.delete(schema.tokenModelAvailability).run();
|
|
await db.delete(schema.modelAvailability).run();
|
|
await db.delete(schema.accountTokens).run();
|
|
await db.delete(schema.accounts).run();
|
|
await db.delete(schema.sites).run();
|
|
invalidateTokenRouterCache();
|
|
});
|
|
|
|
afterAll(() => {
|
|
invalidateTokenRouterCache();
|
|
delete process.env.DATA_DIR;
|
|
});
|
|
|
|
it('does not select channels from disabled sites', async () => {
|
|
const site = await db.insert(schema.sites).values({
|
|
name: 'disabled-site',
|
|
url: 'https://disabled.example.com',
|
|
platform: 'new-api',
|
|
}).returning().get();
|
|
await db.run(sql`update sites set status = 'disabled' where id = ${site.id}`);
|
|
|
|
const account = await db.insert(schema.accounts).values({
|
|
siteId: site.id,
|
|
username: 'disabled-user',
|
|
accessToken: 'access-disabled',
|
|
apiToken: 'sk-disabled',
|
|
status: 'active',
|
|
}).returning().get();
|
|
|
|
const route = await db.insert(schema.tokenRoutes).values({
|
|
modelPattern: 'gpt-4o-mini',
|
|
enabled: true,
|
|
}).returning().get();
|
|
|
|
const channel = await db.insert(schema.routeChannels).values({
|
|
routeId: route.id,
|
|
accountId: account.id,
|
|
tokenId: null,
|
|
priority: 0,
|
|
weight: 10,
|
|
enabled: true,
|
|
manualOverride: false,
|
|
}).returning().get();
|
|
|
|
const router = new TokenRouter();
|
|
const selected = await router.selectChannel('gpt-4o-mini');
|
|
expect(selected).toBeNull();
|
|
|
|
const decision = await router.explainSelection('gpt-4o-mini');
|
|
expect(decision.matched).toBe(true);
|
|
const candidate = decision.candidates.find((item) => item.channelId === channel.id);
|
|
expect(candidate?.eligible).toBe(false);
|
|
});
|
|
|
|
it('does not fallback to deprecated site apiKey when channel and account tokens are missing', async () => {
|
|
const site = await db.insert(schema.sites).values({
|
|
name: 'site-api-key-only',
|
|
url: 'https://site-key.example.com',
|
|
platform: 'new-api',
|
|
apiKey: 'sk-site-only',
|
|
}).returning().get();
|
|
|
|
const account = await db.insert(schema.accounts).values({
|
|
siteId: site.id,
|
|
username: 'site-key-user',
|
|
accessToken: '',
|
|
apiToken: null,
|
|
status: 'active',
|
|
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
|
|
}).returning().get();
|
|
|
|
const route = await db.insert(schema.tokenRoutes).values({
|
|
modelPattern: 'gpt-4.1-mini',
|
|
enabled: true,
|
|
}).returning().get();
|
|
|
|
await db.insert(schema.routeChannels).values({
|
|
routeId: route.id,
|
|
accountId: account.id,
|
|
tokenId: null,
|
|
priority: 0,
|
|
weight: 10,
|
|
enabled: true,
|
|
manualOverride: false,
|
|
}).run();
|
|
|
|
const selected = await new TokenRouter().selectChannel('gpt-4.1-mini');
|
|
expect(selected).toBeNull();
|
|
});
|
|
|
|
it('uses codex oauth access token when no api token is present', async () => {
|
|
const site = await db.insert(schema.sites).values({
|
|
name: 'codex-site',
|
|
url: 'https://chatgpt.com/backend-api/codex',
|
|
platform: 'codex',
|
|
status: 'active',
|
|
}).returning().get();
|
|
|
|
const account = await db.insert(schema.accounts).values({
|
|
siteId: site.id,
|
|
username: 'codex-user@example.com',
|
|
accessToken: 'oauth-access-token',
|
|
apiToken: null,
|
|
status: 'active',
|
|
extraConfig: JSON.stringify({
|
|
credentialMode: 'session',
|
|
oauth: {
|
|
provider: 'codex',
|
|
accountId: 'chatgpt-account-123',
|
|
email: 'codex-user@example.com',
|
|
planType: 'plus',
|
|
},
|
|
}),
|
|
}).returning().get();
|
|
|
|
const route = await db.insert(schema.tokenRoutes).values({
|
|
modelPattern: 'gpt-5.2-codex',
|
|
enabled: true,
|
|
}).returning().get();
|
|
|
|
await db.insert(schema.routeChannels).values({
|
|
routeId: route.id,
|
|
accountId: account.id,
|
|
tokenId: null,
|
|
priority: 0,
|
|
weight: 10,
|
|
enabled: true,
|
|
manualOverride: false,
|
|
}).run();
|
|
|
|
const selected = await new TokenRouter().selectChannel('gpt-5.2-codex');
|
|
expect(selected).not.toBeNull();
|
|
expect(selected?.tokenValue).toBe('oauth-access-token');
|
|
});
|
|
|
|
it('prefers codex oauth access token over a stale legacy api token on oauth-managed rows', async () => {
|
|
const site = await db.insert(schema.sites).values({
|
|
name: 'codex-site',
|
|
url: 'https://chatgpt.com/backend-api/codex',
|
|
platform: 'codex',
|
|
status: 'active',
|
|
}).returning().get();
|
|
|
|
const account = await db.insert(schema.accounts).values({
|
|
siteId: site.id,
|
|
username: 'codex-user@example.com',
|
|
accessToken: 'oauth-access-token',
|
|
apiToken: 'stale-direct-token',
|
|
status: 'active',
|
|
extraConfig: JSON.stringify({
|
|
credentialMode: 'session',
|
|
oauth: {
|
|
provider: 'codex',
|
|
accountId: 'chatgpt-account-123',
|
|
email: 'codex-user@example.com',
|
|
planType: 'plus',
|
|
},
|
|
}),
|
|
}).returning().get();
|
|
|
|
const route = await db.insert(schema.tokenRoutes).values({
|
|
modelPattern: 'gpt-5.2-codex',
|
|
enabled: true,
|
|
}).returning().get();
|
|
|
|
await db.insert(schema.routeChannels).values({
|
|
routeId: route.id,
|
|
accountId: account.id,
|
|
tokenId: null,
|
|
priority: 0,
|
|
weight: 10,
|
|
enabled: true,
|
|
manualOverride: false,
|
|
}).run();
|
|
|
|
const selected = await new TokenRouter().selectChannel('gpt-5.2-codex');
|
|
expect(selected).not.toBeNull();
|
|
expect(selected?.tokenValue).toBe('oauth-access-token');
|
|
});
|
|
|
|
it('does not fall back to a stale legacy api token when an oauth-managed row has no access token', async () => {
|
|
const site = await db.insert(schema.sites).values({
|
|
name: 'codex-site',
|
|
url: 'https://chatgpt.com/backend-api/codex',
|
|
platform: 'codex',
|
|
status: 'active',
|
|
}).returning().get();
|
|
|
|
const account = await db.insert(schema.accounts).values({
|
|
siteId: site.id,
|
|
username: 'codex-user@example.com',
|
|
accessToken: '',
|
|
apiToken: 'stale-direct-token',
|
|
status: 'active',
|
|
extraConfig: JSON.stringify({
|
|
credentialMode: 'session',
|
|
oauth: {
|
|
provider: 'codex',
|
|
accountId: 'chatgpt-account-123',
|
|
email: 'codex-user@example.com',
|
|
planType: 'plus',
|
|
},
|
|
}),
|
|
}).returning().get();
|
|
|
|
const route = await db.insert(schema.tokenRoutes).values({
|
|
modelPattern: 'gpt-5.2-codex',
|
|
enabled: true,
|
|
}).returning().get();
|
|
|
|
await db.insert(schema.routeChannels).values({
|
|
routeId: route.id,
|
|
accountId: account.id,
|
|
tokenId: null,
|
|
priority: 0,
|
|
weight: 10,
|
|
enabled: true,
|
|
manualOverride: false,
|
|
}).run();
|
|
|
|
const selected = await new TokenRouter().selectChannel('gpt-5.2-codex');
|
|
expect(selected).toBeNull();
|
|
});
|
|
});
|