Spaces:
Sleeping
Sleeping
File size: 6,854 Bytes
ed57015 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | import { describe, it, expect, beforeAll, vi } from 'vitest';
import type { Express } from 'express';
import { createApp } from '../../app.js';
import { initDb, getDb, getUnifiedApiKey } from '../../db/index.js';
import { mintDashboardToken, isGatedApiPath } from '../helpers/auth.js';
let dashToken = '';
async function req(app: Express, method: string, path: string, body?: any, headers: Record<string, string> = {}) {
const server = app.listen(0);
const addr = server.address() as any;
const url = `http://127.0.0.1:${addr.port}${path}`;
const res = await fetch(url, {
method,
headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...(isGatedApiPath(path) && !('Authorization' in headers) ? { Authorization: `Bearer ${dashToken}` } : {}), ...headers },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.text();
server.close();
let json: any = null;
try { json = JSON.parse(data); } catch {}
return { status: res.status, body: json, headers: res.headers, raw: data };
}
function authHeaders() {
return { Authorization: `Bearer ${getUnifiedApiKey()}` };
}
describe('Full Integration Flow', () => {
let app: Express;
beforeAll(() => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
app = createApp();
dashToken = mintDashboardToken();
// Clean
const db = getDb();
db.prepare('DELETE FROM api_keys').run();
db.prepare('DELETE FROM requests').run();
});
it('Step 1: Verify models are seeded', async () => {
const { status, body } = await req(app, 'GET', '/api/models');
expect(status).toBe(200);
// Tightened from >= 14 — current catalog post-V9 is 60+ rows; if a future
// migration accidentally drops a chunk we want to know.
expect(body.length).toBeGreaterThanOrEqual(50);
expect(body[0]).toHaveProperty('modelId');
expect(body[0]).toHaveProperty('hasProvider');
// All should have providers (catches drift between catalog and providers/index.ts)
for (const m of body) {
expect(m.hasProvider).toBe(true);
}
});
it('Step 2: Verify fallback chain is populated', async () => {
const { status, body } = await req(app, 'GET', '/api/fallback');
expect(status).toBe(200);
expect(body.length).toBeGreaterThanOrEqual(50);
expect(body[0]).toHaveProperty('priority');
expect(body[0]).toHaveProperty('enabled');
});
it('Step 3: Authenticated proxy returns 429 with no keys', async () => {
const { status, body } = await req(app, 'POST', '/v1/chat/completions', {
messages: [{ role: 'user', content: 'hello' }],
}, authHeaders());
// 429 (all exhausted) or 502 (provider error) or 503 (no route)
expect([429, 502, 503]).toContain(status);
expect(body.error).toBeDefined();
});
it('Step 4: Add a Groq key', async () => {
const { status, body } = await req(app, 'POST', '/api/keys', {
platform: 'groq',
key: 'gsk_integration_test_key',
label: 'Integration Test',
});
expect(status).toBe(201);
expect(body.platform).toBe('groq');
expect(body.maskedKey).toContain('...');
});
it('Step 5: Proxy routes to Groq and handles provider error gracefully', async () => {
// Mock fetch to simulate a Groq API error
const origFetch = global.fetch;
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
const urlStr = typeof url === 'string' ? url : url.toString();
// If it's calling the Groq API, return an error
if (urlStr.includes('api.groq.com')) {
return {
ok: false,
status: 401,
statusText: 'Unauthorized',
json: () => Promise.resolve({ error: { message: 'Invalid API Key' } }),
} as any;
}
// Otherwise pass through (for our test server)
return origFetch(url, init);
});
const { status, body } = await req(app, 'POST', '/v1/chat/completions', {
messages: [{ role: 'user', content: 'hello' }],
}, authHeaders());
// 502 (provider error) or 429 (all exhausted after retries)
expect([502, 429]).toContain(status);
expect(body.error).toBeDefined();
vi.restoreAllMocks();
});
it('Step 6: Error was logged in analytics', async () => {
const { status, body } = await req(app, 'GET', '/api/analytics/summary?range=24h');
expect(status).toBe(200);
// May or may not have logged depending on retry behavior
expect(body.totalRequests).toBeGreaterThanOrEqual(0);
});
it('Step 7: Sort fallback by speed', async () => {
const { status } = await req(app, 'POST', '/api/fallback/sort/speed');
expect(status).toBe(200);
const { body } = await req(app, 'GET', '/api/fallback');
expect(body[0].speedRank).toBe(1);
});
it('Step 8: Health endpoint works', async () => {
const { status, body } = await req(app, 'GET', '/api/health');
expect(status).toBe(200);
expect(body).toHaveProperty('platforms');
expect(body).toHaveProperty('keys');
});
it('Step 9: Delete a key if any exist', async () => {
// Add a fresh key to ensure we have one to delete
await req(app, 'POST', '/api/keys', {
platform: 'groq', key: 'gsk_delete_test', label: 'delete-test',
});
const { body: keys } = await req(app, 'GET', '/api/keys');
const target = keys.find((k: any) => k.label === 'delete-test');
expect(target).toBeDefined();
const { status } = await req(app, 'DELETE', `/api/keys/${target.id}`);
expect(status).toBe(200);
});
it('Step 10: Validate request schema', async () => {
const { status } = await req(app, 'POST', '/v1/chat/completions', {
messages: [], // empty
}, authHeaders());
expect(status).toBe(400);
const { status: s2 } = await req(app, 'POST', '/v1/chat/completions', {
// missing messages entirely
}, authHeaders());
expect(s2).toBe(400);
});
it('Step 11: Explicit unknown model returns 400 (not silent fallthrough)', async () => {
const { status, body } = await req(app, 'POST', '/v1/chat/completions', {
model: 'definitely-not-a-real-model',
messages: [{ role: 'user', content: 'hi' }],
}, authHeaders());
expect(status).toBe(400);
expect(body.error.code).toBe('model_not_found');
expect(body.error.message).toContain('not in the catalog');
});
it('Step 12: Explicit disabled model returns 400 with disabled reason', async () => {
// gemini-2.5-pro is disabled (V1 migration). Reuse it as a known-disabled fixture.
const { status, body } = await req(app, 'POST', '/v1/chat/completions', {
model: 'gemini-2.5-pro',
messages: [{ role: 'user', content: 'hi' }],
}, authHeaders());
expect(status).toBe(400);
expect(body.error.code).toBe('model_not_found');
expect(body.error.message).toContain('is disabled');
});
});
|