Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
232 lines
7.4 KiB
JavaScript
232 lines
7.4 KiB
JavaScript
/**
|
|
* Health Check Endpoint Tests - ADR-064 Phase 3, Task 12
|
|
* Testing GET /api/health and GET /api/health/deep
|
|
*/
|
|
|
|
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
|
|
// Mock database connection
|
|
const mockDbGet = vi.fn();
|
|
vi.mock('../../../database/connection.js', () => ({
|
|
dbGet: (...args) => mockDbGet(...args),
|
|
dbRun: vi.fn().mockResolvedValue({ changes: 1 }),
|
|
dbAll: vi.fn().mockResolvedValue([])
|
|
}));
|
|
|
|
// Inline authenticate middleware for testing
|
|
function mockAuthenticate(req, _res, next) {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return _res.status(401).json({ success: false, error: { code: 'AUTH_REQUIRED' } });
|
|
}
|
|
const token = authHeader.split(' ')[1];
|
|
if (token === 'admin-token') {
|
|
req.user = { id: 1, email: 'admin@test.com', role: 'admin' };
|
|
} else if (token === 'owner-token') {
|
|
req.user = { id: 1, email: 'owner@test.com', role: 'owner' };
|
|
} else if (token === 'user-token') {
|
|
req.user = { id: 2, email: 'user@test.com', role: 'user' };
|
|
} else {
|
|
return _res.status(401).json({ success: false, error: { code: 'INVALID_TOKEN' } });
|
|
}
|
|
next();
|
|
}
|
|
|
|
// Build a mini app that replicates the health endpoints from server.js
|
|
function createHealthApp() {
|
|
const app = express();
|
|
|
|
// Basic health check
|
|
app.get('/api/health', async (req, res) => {
|
|
let dbStatus = 'unknown';
|
|
let dbLatencyMs = null;
|
|
try {
|
|
const dbStart = Date.now();
|
|
await mockDbGet('SELECT 1 AS ping');
|
|
dbLatencyMs = Date.now() - dbStart;
|
|
dbStatus = 'connected';
|
|
} catch {
|
|
dbStatus = 'disconnected';
|
|
}
|
|
const mem = process.memoryUsage();
|
|
res.json({
|
|
status: dbStatus === 'connected' ? 'ok' : 'degraded',
|
|
version: '0.003.001',
|
|
uptime_seconds: Math.floor(process.uptime()),
|
|
timestamp: new Date().toISOString(),
|
|
database: { status: dbStatus, latency_ms: dbLatencyMs },
|
|
memory: {
|
|
rss_mb: Math.round(mem.rss / 1024 / 1024),
|
|
heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024)
|
|
}
|
|
});
|
|
});
|
|
|
|
// Deep health check
|
|
app.get('/api/health/deep', mockAuthenticate, async (req, res) => {
|
|
if (req.user.role !== 'admin' && req.user.role !== 'owner') {
|
|
return res.status(403).json({ success: false, error: { code: 'FORBIDDEN', message: 'Admin access required' } });
|
|
}
|
|
let dbStatus = 'unknown';
|
|
let dbLatencyMs = null;
|
|
try {
|
|
const dbStart = Date.now();
|
|
await mockDbGet('SELECT 1 AS ping');
|
|
dbLatencyMs = Date.now() - dbStart;
|
|
dbStatus = 'connected';
|
|
} catch {
|
|
dbStatus = 'disconnected';
|
|
}
|
|
const mem = process.memoryUsage();
|
|
res.json({
|
|
status: dbStatus === 'connected' ? 'ok' : 'degraded',
|
|
version: '0.003.001',
|
|
uptime_seconds: Math.floor(process.uptime()),
|
|
timestamp: new Date().toISOString(),
|
|
database: { status: dbStatus, latency_ms: dbLatencyMs },
|
|
memory: {
|
|
rss_mb: Math.round(mem.rss / 1024 / 1024),
|
|
heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
|
heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024),
|
|
external_mb: Math.round(mem.external / 1024 / 1024)
|
|
},
|
|
node_version: process.version,
|
|
env: process.env.NODE_ENV || 'development'
|
|
});
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
describe('GET /api/health - ADR-064 Enhanced Health Check', () => {
|
|
let app;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockDbGet.mockResolvedValue({ ping: 1 });
|
|
app = createHealthApp();
|
|
});
|
|
|
|
test('should return 200 with status ok when DB is connected', async () => {
|
|
const res = await request(app).get('/api/health').expect(200);
|
|
|
|
expect(res.body.status).toBe('ok');
|
|
expect(res.body.version).toBe('0.003.001');
|
|
expect(res.body.uptime_seconds).toBeGreaterThanOrEqual(0);
|
|
expect(res.body.timestamp).toBeDefined();
|
|
});
|
|
|
|
test('should include database connectivity status', async () => {
|
|
const res = await request(app).get('/api/health').expect(200);
|
|
|
|
expect(res.body.database).toBeDefined();
|
|
expect(res.body.database.status).toBe('connected');
|
|
expect(typeof res.body.database.latency_ms).toBe('number');
|
|
expect(res.body.database.latency_ms).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test('should include memory usage', async () => {
|
|
const res = await request(app).get('/api/health').expect(200);
|
|
|
|
expect(res.body.memory).toBeDefined();
|
|
expect(typeof res.body.memory.rss_mb).toBe('number');
|
|
expect(typeof res.body.memory.heap_used_mb).toBe('number');
|
|
});
|
|
|
|
test('should not require authentication', async () => {
|
|
const res = await request(app).get('/api/health').expect(200);
|
|
expect(res.body.status).toBe('ok');
|
|
});
|
|
|
|
test('should return degraded status when DB is unreachable', async () => {
|
|
mockDbGet.mockRejectedValue(new Error('Connection refused'));
|
|
|
|
const res = await request(app).get('/api/health').expect(200);
|
|
|
|
expect(res.body.status).toBe('degraded');
|
|
expect(res.body.database.status).toBe('disconnected');
|
|
expect(res.body.database.latency_ms).toBeNull();
|
|
});
|
|
|
|
test('should include version from package.json', async () => {
|
|
const res = await request(app).get('/api/health').expect(200);
|
|
expect(res.body.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/health/deep - ADR-064 Admin Deep Health Check', () => {
|
|
let app;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockDbGet.mockResolvedValue({ ping: 1 });
|
|
app = createHealthApp();
|
|
});
|
|
|
|
test('should require authentication', async () => {
|
|
const res = await request(app).get('/api/health/deep').expect(401);
|
|
expect(res.body.error.code).toBe('AUTH_REQUIRED');
|
|
});
|
|
|
|
test('should allow admin access', async () => {
|
|
const res = await request(app)
|
|
.get('/api/health/deep')
|
|
.set('Authorization', 'Bearer admin-token')
|
|
.expect(200);
|
|
|
|
expect(res.body.status).toBe('ok');
|
|
expect(res.body.node_version).toBeDefined();
|
|
expect(res.body.env).toBeDefined();
|
|
});
|
|
|
|
test('should allow owner access', async () => {
|
|
const res = await request(app)
|
|
.get('/api/health/deep')
|
|
.set('Authorization', 'Bearer owner-token')
|
|
.expect(200);
|
|
|
|
expect(res.body.status).toBe('ok');
|
|
});
|
|
|
|
test('should reject non-admin users with 403', async () => {
|
|
const res = await request(app)
|
|
.get('/api/health/deep')
|
|
.set('Authorization', 'Bearer user-token')
|
|
.expect(403);
|
|
|
|
expect(res.body.error.code).toBe('FORBIDDEN');
|
|
});
|
|
|
|
test('should include extended memory info', async () => {
|
|
const res = await request(app)
|
|
.get('/api/health/deep')
|
|
.set('Authorization', 'Bearer admin-token')
|
|
.expect(200);
|
|
|
|
expect(res.body.memory.heap_total_mb).toBeDefined();
|
|
expect(res.body.memory.external_mb).toBeDefined();
|
|
});
|
|
|
|
test('should return degraded when DB unreachable', async () => {
|
|
mockDbGet.mockRejectedValue(new Error('Connection refused'));
|
|
|
|
const res = await request(app)
|
|
.get('/api/health/deep')
|
|
.set('Authorization', 'Bearer admin-token')
|
|
.expect(200);
|
|
|
|
expect(res.body.status).toBe('degraded');
|
|
expect(res.body.database.status).toBe('disconnected');
|
|
});
|
|
|
|
test('should check database connectivity', async () => {
|
|
await request(app)
|
|
.get('/api/health/deep')
|
|
.set('Authorization', 'Bearer admin-token')
|
|
.expect(200);
|
|
|
|
expect(mockDbGet).toHaveBeenCalledWith('SELECT 1 AS ping');
|
|
});
|
|
});
|