156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
|
|
import Fastify from 'fastify';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { SignJWT } from 'jose';
|
|
import { config } from './lib/config.js';
|
|
import { productConfig } from './lib/product-config.js';
|
|
import { assertDiagnosticsAccess, diagnosticsRoutes } from './lib/diagnostics-routes.js';
|
|
|
|
let app: FastifyInstance;
|
|
|
|
beforeAll(async () => {
|
|
app = Fastify({ logger: false });
|
|
await diagnosticsRoutes(app);
|
|
app.get('/health', async (req) => ({
|
|
status: 'ok',
|
|
service: config.SERVICE_NAME,
|
|
version: '0.1.0',
|
|
timestamp: new Date().toISOString(),
|
|
requestId: req.id,
|
|
}));
|
|
app.get('/api/bootstrap', async () => ({
|
|
productId: productConfig.productId,
|
|
displayName: productConfig.displayName,
|
|
backendPort: config.PORT,
|
|
}));
|
|
await app.ready();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function makeToken(role: string) {
|
|
return new SignJWT({
|
|
sub: 'diagnostics-user',
|
|
role,
|
|
type: 'access',
|
|
})
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime('5m')
|
|
.sign(new TextEncoder().encode(config.JWT_SECRET));
|
|
}
|
|
|
|
describe('diagnostics routes', () => {
|
|
it('GET /api/diagnostics/flags returns feature flags', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/api/diagnostics/flags' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(typeof body).toBe('object');
|
|
expect(body).not.toBeNull();
|
|
});
|
|
|
|
it('GET /api/diagnostics/telemetry returns buffered events', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/api/diagnostics/telemetry' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body).toHaveProperty('events');
|
|
expect(Array.isArray(body.events)).toBe(true);
|
|
});
|
|
|
|
it('POST /api/diagnostics/telemetry/flush returns flushed count', async () => {
|
|
const res = await app.inject({ method: 'POST', url: '/api/diagnostics/telemetry/flush' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body).toHaveProperty('flushed');
|
|
expect(typeof body.flushed).toBe('number');
|
|
});
|
|
|
|
it('GET /api/diagnostics/config returns sanitized config', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/api/diagnostics/config' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body).toHaveProperty('productId');
|
|
expect(body).toHaveProperty('serviceName');
|
|
expect(body).toHaveProperty('port');
|
|
expect(body).toHaveProperty('nodeEnv');
|
|
expect(body).toHaveProperty('dbProvider');
|
|
expect(typeof body.telemetryEnabled).toBe('boolean');
|
|
expect(typeof body.featureFlagsEnabled).toBe('boolean');
|
|
});
|
|
|
|
it('GET /api/diagnostics/readiness returns dependency health summary', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => new Response(JSON.stringify({ status: 'ok' }), { status: 200 })),
|
|
);
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/api/diagnostics/readiness' });
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body.overall).toBe('ready');
|
|
expect(body.summary).toMatchObject({ total: 5, unhealthy: 0, unreachable: 0 });
|
|
expect(body.dependencies.map((dep: { name: string }) => dep.name)).toEqual([
|
|
'datastore',
|
|
'encryption',
|
|
'platform-service',
|
|
'extraction-service',
|
|
'mcp',
|
|
]);
|
|
});
|
|
|
|
it('GET /health returns standard health response', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body.status).toBe('ok');
|
|
expect(body).toHaveProperty('service');
|
|
expect(body).toHaveProperty('version');
|
|
expect(body).toHaveProperty('timestamp');
|
|
expect(body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
|
|
it('GET /api/bootstrap returns product identity', async () => {
|
|
const res = await app.inject({ method: 'GET', url: '/api/bootstrap' });
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json();
|
|
expect(body).toHaveProperty('productId');
|
|
expect(body).toHaveProperty('displayName');
|
|
expect(body).toHaveProperty('backendPort');
|
|
expect(typeof body.productId).toBe('string');
|
|
expect(typeof body.displayName).toBe('string');
|
|
expect(typeof body.backendPort).toBe('number');
|
|
});
|
|
|
|
it('keeps diagnostics open outside production for local smoke ergonomics', async () => {
|
|
await expect(assertDiagnosticsAccess({ headers: {} }, 'development')).resolves.toBeUndefined();
|
|
await expect(assertDiagnosticsAccess({ headers: {} }, 'test')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('requires auth for diagnostics in production', async () => {
|
|
await expect(assertDiagnosticsAccess({ headers: {} }, 'production')).rejects.toThrow('Unauthorized');
|
|
});
|
|
|
|
it('allows production diagnostics for admin or owner roles only', async () => {
|
|
const adminToken = await makeToken('admin');
|
|
const ownerToken = await makeToken('owner');
|
|
const viewerToken = await makeToken('viewer');
|
|
|
|
await expect(
|
|
assertDiagnosticsAccess({ headers: { authorization: `Bearer ${adminToken}` } }, 'production')
|
|
).resolves.toBeUndefined();
|
|
await expect(
|
|
assertDiagnosticsAccess({ headers: { authorization: `Bearer ${ownerToken}` } }, 'production')
|
|
).resolves.toBeUndefined();
|
|
await expect(
|
|
assertDiagnosticsAccess({ headers: { authorization: `Bearer ${viewerToken}` } }, 'production')
|
|
).rejects.toThrow('Insufficient permissions');
|
|
});
|
|
});
|