Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
131
tests/unit/api-response-time.test.ts
Normal file
131
tests/unit/api-response-time.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
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+\//);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user