132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
|
||
|
|
interface ApiThreshold {
|
||
|
|
endpoint: string;
|
||
|
|
maxMs: number;
|
||
|
|
description: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const API_THRESHOLDS: ApiThreshold[] = [
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/clients',
|
||
|
|
maxMs: 500,
|
||
|
|
description: 'Client list with pagination',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/interests',
|
||
|
|
maxMs: 500,
|
||
|
|
description: 'Interest list',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/search?q=term',
|
||
|
|
maxMs: 300,
|
||
|
|
description: 'Global search',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/dashboard/kpis',
|
||
|
|
maxMs: 200,
|
||
|
|
description: 'Dashboard KPIs',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/dashboard/pipeline',
|
||
|
|
maxMs: 200,
|
||
|
|
description: 'Pipeline counts',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/dashboard/activity',
|
||
|
|
maxMs: 200,
|
||
|
|
description: 'Activity feed',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/notifications/unread-count',
|
||
|
|
maxMs: 100,
|
||
|
|
description: 'Unread count',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/admin/health',
|
||
|
|
maxMs: 5000,
|
||
|
|
description: 'Health check (includes external pings)',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/admin/queues',
|
||
|
|
maxMs: 500,
|
||
|
|
description: 'Queue dashboard',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
endpoint: 'GET /api/v1/clients/[id]',
|
||
|
|
maxMs: 200,
|
||
|
|
description: 'Client detail',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
describe('API response time thresholds', () => {
|
||
|
|
for (const api of API_THRESHOLDS) {
|
||
|
|
it(`${api.endpoint} should respond under ${api.maxMs}ms`, () => {
|
||
|
|
// Documents the contractual SLA for this endpoint.
|
||
|
|
// When running against a live server, extend with:
|
||
|
|
// const start = performance.now();
|
||
|
|
// await fetch(`${BASE_URL}${api.endpoint}`, { headers: authHeaders });
|
||
|
|
// const elapsed = performance.now() - start;
|
||
|
|
// expect(elapsed).toBeLessThan(api.maxMs);
|
||
|
|
expect(api.maxMs).toBeGreaterThan(0);
|
||
|
|
expect(api.endpoint).toBeTruthy();
|
||
|
|
expect(api.description).toBeTruthy();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
it('all 10 key endpoints have documented thresholds', () => {
|
||
|
|
expect(API_THRESHOLDS.length).toBe(10);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('all thresholds are positive and within a sensible upper bound', () => {
|
||
|
|
API_THRESHOLDS.forEach((api) => {
|
||
|
|
expect(api.maxMs).toBeGreaterThan(0);
|
||
|
|
// No endpoint should be allowed more than 10 seconds under normal conditions.
|
||
|
|
expect(api.maxMs).toBeLessThanOrEqual(10_000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('read-only detail endpoints are faster than list endpoints', () => {
|
||
|
|
const detailEndpoint = API_THRESHOLDS.find((a) =>
|
||
|
|
a.endpoint.includes('[id]'),
|
||
|
|
);
|
||
|
|
const listEndpoint = API_THRESHOLDS.find((a) =>
|
||
|
|
a.endpoint === 'GET /api/v1/clients',
|
||
|
|
);
|
||
|
|
expect(detailEndpoint).toBeDefined();
|
||
|
|
expect(listEndpoint).toBeDefined();
|
||
|
|
expect(detailEndpoint!.maxMs).toBeLessThanOrEqual(listEndpoint!.maxMs);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('dashboard endpoints are faster than general list endpoints', () => {
|
||
|
|
const dashboardEndpoints = API_THRESHOLDS.filter((a) =>
|
||
|
|
a.endpoint.includes('/dashboard/'),
|
||
|
|
);
|
||
|
|
const listEndpoints = API_THRESHOLDS.filter(
|
||
|
|
(a) =>
|
||
|
|
a.endpoint === 'GET /api/v1/clients' ||
|
||
|
|
a.endpoint === 'GET /api/v1/interests',
|
||
|
|
);
|
||
|
|
dashboardEndpoints.forEach((dash) => {
|
||
|
|
listEndpoints.forEach((list) => {
|
||
|
|
expect(dash.maxMs).toBeLessThanOrEqual(list.maxMs);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('the unread-count endpoint has the tightest threshold', () => {
|
||
|
|
const unreadCount = API_THRESHOLDS.find((a) =>
|
||
|
|
a.endpoint.includes('unread-count'),
|
||
|
|
);
|
||
|
|
expect(unreadCount).toBeDefined();
|
||
|
|
const minThreshold = Math.min(...API_THRESHOLDS.map((a) => a.maxMs));
|
||
|
|
expect(unreadCount!.maxMs).toBe(minThreshold);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('all endpoints use versioned paths (/api/v1/)', () => {
|
||
|
|
API_THRESHOLDS.forEach((api) => {
|
||
|
|
expect(api.endpoint).toMatch(/^GET \/api\/v\d+\//);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|