kyush-llm-router/server/tests/integration/admin.test.ts

451 lines
16 KiB
TypeScript

import { describe, it, expect, beforeAll } from 'vitest';
import { createTestApp } from '../utils/testApp';
import { createAdminClient } from '../utils/adminClient';
import { createMockBackend } from '../utils/mockBackend';
let app: ReturnType<typeof createTestApp>;
let admin: Awaited<ReturnType<typeof createAdminClient>>;
beforeAll(() => {
app = createTestApp();
});
beforeAll(async () => {
admin = await createAdminClient(app);
});
describe('Admin API - User Management', () => {
describe('GET /admin/users', () => {
it('should return empty array initially', async () => {
const response = await admin.get('/admin/users');
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('POST /admin/users', () => {
it('should create a new user', async () => {
const userData = { name: 'Test User', email: 'test@example.com' };
const response = await admin.post('/admin/users').send(userData);
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(userData.name);
expect(response.body.email).toBe(userData.email);
expect(response.body).toHaveProperty('api_key');
expect(response.body.api_key).toMatch(/^sk-/);
expect(response.body.copy_reasoning_to_reasoning_content).toBe(false);
});
it('should create a user with reasoning compatibility enabled', async () => {
const response = await admin.post('/admin/users').send({
name: 'Reasoning Compat User',
copy_reasoning_to_reasoning_content: true,
});
expect(response.status).toBe(201);
expect(response.body.copy_reasoning_to_reasoning_content).toBe(true);
});
it('should create a user with a manually supplied api key', async () => {
const userData = { name: 'Migrated User', api_key: 'legacy-user-key-001' };
const response = await admin.post('/admin/users').send(userData);
expect(response.status).toBe(201);
expect(response.body.api_key).toBe(userData.api_key);
});
it('should return 409 if a manually supplied api key already exists', async () => {
await admin.post('/admin/users').send({ name: 'Duplicate Key Source', api_key: 'legacy-duplicate-key' });
const response = await admin.post('/admin/users').send({ name: 'Duplicate Key Target', api_key: 'legacy-duplicate-key' });
expect(response.status).toBe(409);
expect(response.body.error).toBe('API key already exists');
});
it('should return 400 if name is missing', async () => {
const response = await admin.post('/admin/users').send({ email: 'test@example.com' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
describe('GET /admin/users/:id', () => {
let userId: number;
beforeAll(async () => {
const response = await admin.post('/admin/users').send({ name: 'User for Get' });
userId = response.body.id;
});
it('should return a user by id', async () => {
const response = await admin.get(`/admin/users/${userId}`);
expect(response.status).toBe(200);
expect(response.body.id).toBe(userId);
expect(response.body).toHaveProperty('api_key');
});
it('should return 404 for non-existent user', async () => {
const response = await admin.get('/admin/users/99999');
expect(response.status).toBe(404);
expect(response.body).toHaveProperty('error');
});
});
describe('PUT /admin/users/:id', () => {
let userId: number;
beforeAll(async () => {
const response = await admin.post('/admin/users').send({ name: 'User for Update' });
userId = response.body.id;
});
it('should update user', async () => {
const response = await admin
.put(`/admin/users/${userId}`)
.send({ name: 'Updated Name', email: 'updated@example.com' });
expect(response.status).toBe(200);
expect(response.body.name).toBe('Updated Name');
expect(response.body.email).toBe('updated@example.com');
});
it('should update user api key manually', async () => {
const response = await admin
.put(`/admin/users/${userId}`)
.send({ api_key: 'legacy-updated-key-001' });
expect(response.status).toBe(200);
expect(response.body.api_key).toBe('legacy-updated-key-001');
});
it('should update user reasoning compatibility setting', async () => {
const response = await admin
.put(`/admin/users/${userId}`)
.send({ copy_reasoning_to_reasoning_content: true });
expect(response.status).toBe(200);
expect(response.body.copy_reasoning_to_reasoning_content).toBe(true);
});
it('should return 404 for non-existent user', async () => {
const response = await admin.put('/admin/users/99999').send({ name: 'Test' });
expect(response.status).toBe(404);
});
});
describe('POST /admin/users/:id/regenerate-api-key', () => {
let userId: number;
let oldApiKey: string;
beforeAll(async () => {
const response = await admin.post('/admin/users').send({ name: 'User for Key Regen' });
userId = response.body.id;
oldApiKey = response.body.api_key;
});
it('should regenerate API key', async () => {
const response = await admin.post(`/admin/users/${userId}/regenerate-api-key`);
expect(response.status).toBe(200);
expect(response.body.api_key).toMatch(/^sk-/);
expect(response.body.api_key).not.toBe(oldApiKey);
});
});
describe('DELETE /admin/users/:id', () => {
let userId: number;
beforeAll(async () => {
const response = await admin.post('/admin/users').send({ name: 'User for Delete' });
userId = response.body.id;
});
it('should delete a user', async () => {
const response = await admin.delete(`/admin/users/${userId}`);
expect(response.status).toBe(204);
});
it('should return 404 for already deleted user', async () => {
const response = await admin.delete(`/admin/users/${userId}`);
expect(response.status).toBe(404);
});
});
});
describe('Admin API - Backend Management', () => {
describe('GET /admin/backends', () => {
it('should return empty array initially', async () => {
const response = await admin.get('/admin/backends');
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('POST /admin/backends', () => {
it('should create a new backend', async () => {
const backendData = {
name: 'Test Backend',
base_url: 'http://localhost:8000/v1',
api_key: 'backend-key-123'
};
const response = await admin.post('/admin/backends').send(backendData);
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(backendData.name);
expect(response.body.base_url).toBe(backendData.base_url);
expect(response.body.api_key).toBe(backendData.api_key);
});
it('should return 400 if name or base_url is missing', async () => {
const response = await admin.post('/admin/backends').send({ name: 'Test' });
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('error');
});
});
describe('GET /admin/backends/:id', () => {
let backendId: number;
beforeAll(async () => {
const response = await admin.post('/admin/backends').send({
name: 'Backend for Get',
base_url: 'http://localhost:8001/v1'
});
backendId = response.body.id;
});
it('should return a backend by id', async () => {
const response = await admin.get(`/admin/backends/${backendId}`);
expect(response.status).toBe(200);
expect(response.body.id).toBe(backendId);
});
it('should return 404 for non-existent backend', async () => {
const response = await admin.get('/admin/backends/99999');
expect(response.status).toBe(404);
});
});
describe('PUT /admin/backends/:id', () => {
let backendId: number;
beforeAll(async () => {
const response = await admin.post('/admin/backends').send({
name: 'Backend for Update',
base_url: 'http://localhost:8002/v1'
});
backendId = response.body.id;
});
it('should update backend', async () => {
const response = await admin
.put(`/admin/backends/${backendId}`)
.send({ name: 'Updated Backend', is_active: false });
expect(response.status).toBe(200);
expect(response.body.name).toBe('Updated Backend');
expect(response.body.is_active).toBe(false);
expect(response.body.model_cache_state).toBe('inactive');
});
it('should return 404 for non-existent backend', async () => {
const response = await admin.put('/admin/backends/99999').send({ name: 'Test' });
expect(response.status).toBe(404);
});
});
describe('DELETE /admin/backends/:id', () => {
let backendId: number;
beforeAll(async () => {
const response = await admin.post('/admin/backends').send({
name: 'Backend for Delete',
base_url: 'http://localhost:8003/v1'
});
backendId = response.body.id;
});
it('should delete a backend', async () => {
const response = await admin.delete(`/admin/backends/${backendId}`);
expect(response.status).toBe(204);
});
it('should return 404 for already deleted backend', async () => {
const response = await admin.delete(`/admin/backends/${backendId}`);
expect(response.status).toBe(404);
});
});
describe('Backend model cache endpoints', () => {
it('should expose backend cache details and allow manual refresh for active backends', async () => {
const { server, port } = createMockBackend({
modelsResponse: [{ id: 'admin-refresh-model', object: 'model' }],
});
const backendResponse = await admin.post('/admin/backends').send({
name: 'Backend Cache Admin Test',
base_url: `http://localhost:${port}`,
});
const backendId = backendResponse.body.id;
const beforeRefresh = await admin.get(`/admin/backends/${backendId}/models`);
expect(beforeRefresh.status).toBe(200);
expect(beforeRefresh.body.cache.state).toBe('uninitialized');
const refreshResponse = await admin.post(`/admin/backends/${backendId}/models/refresh`);
expect(refreshResponse.status).toBe(200);
expect(refreshResponse.body.models).toContain('admin-refresh-model');
const cacheOverview = await admin.get('/admin/models/cache');
expect(cacheOverview.status).toBe(200);
expect(Array.isArray(cacheOverview.body.models)).toBe(true);
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it('should reject manual refresh for inactive backends', async () => {
const backendResponse = await admin.post('/admin/backends').send({
name: 'Inactive Refresh Reject',
base_url: 'http://localhost:8041',
});
await admin.put(`/admin/backends/${backendResponse.body.id}`).send({ is_active: false });
const refreshResponse = await admin.post(`/admin/backends/${backendResponse.body.id}/models/refresh`);
expect(refreshResponse.status).toBe(409);
});
});
});
describe('Admin API - Model Rewrite Management', () => {
it('should create, update, list, and delete model rewrite rules', async () => {
const createResponse = await admin.post('/admin/model-rewrites').send({
source_model: 'gpt-3.5-turbo-admin-test',
target_model: 'gpt-3.5-admin-test',
force: true,
note: 'fallback alias',
});
expect(createResponse.status).toBe(201);
expect(createResponse.body.source_model).toBe('gpt-3.5-turbo-admin-test');
expect(createResponse.body.force).toBe(true);
const listResponse = await admin.get('/admin/model-rewrites');
expect(listResponse.status).toBe(200);
expect(listResponse.body.some((rule: any) => rule.source_model === 'gpt-3.5-turbo-admin-test')).toBe(true);
const updateResponse = await admin.put(`/admin/model-rewrites/${createResponse.body.id}`).send({
target_model: 'gpt-3.5-mini-admin-test',
is_active: false,
force: false,
});
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.target_model).toBe('gpt-3.5-mini-admin-test');
expect(updateResponse.body.is_active).toBe(false);
expect(updateResponse.body.force).toBe(false);
const deleteResponse = await admin.delete(`/admin/model-rewrites/${createResponse.body.id}`);
expect(deleteResponse.status).toBe(204);
});
});
describe('Admin API - Permission Management', () => {
let userId: number;
let backendId: number;
beforeAll(async () => {
const userResponse = await admin.post('/admin/users').send({ name: 'User for Permission' });
userId = userResponse.body.id;
const backendResponse = await admin.post('/admin/backends').send({
name: 'Backend for Permission',
base_url: 'http://localhost:8004/v1'
});
backendId = backendResponse.body.id;
});
describe('GET /admin/permissions', () => {
it('should return empty array initially', async () => {
const response = await admin.get('/admin/permissions');
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('POST /admin/permissions', () => {
it('should create a new permission', async () => {
const response = await admin
.post('/admin/permissions')
.send({ user_id: userId, backend_id: backendId });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.user_id).toBe(userId);
expect(response.body.backend_id).toBe(backendId);
});
it('should return 409 if permission already exists', async () => {
const response = await admin
.post('/admin/permissions')
.send({ user_id: userId, backend_id: backendId });
expect(response.status).toBe(409);
expect(response.body).toHaveProperty('error');
});
it('should return 400 if user_id or backend_id is missing', async () => {
const response = await admin.post('/admin/permissions').send({ user_id: userId });
expect(response.status).toBe(400);
});
});
describe('GET /admin/permissions/user/:userId', () => {
it('should return permissions for user', async () => {
const response = await admin.get(`/admin/permissions/user/${userId}`);
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
});
});
describe('GET /admin/permissions/backend/:backendId', () => {
it('should return permissions for backend', async () => {
const response = await admin.get(`/admin/permissions/backend/${backendId}`);
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
});
});
describe('DELETE /admin/permissions', () => {
it('should delete a permission', async () => {
const response = await admin
.delete(`/admin/permissions?user_id=${userId}&backend_id=${backendId}`);
expect(response.status).toBe(204);
});
it('should return 404 for already deleted permission', async () => {
const response = await admin
.delete(`/admin/permissions?user_id=${userId}&backend_id=${backendId}`);
expect(response.status).toBe(404);
});
});
});