import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; const BASE = process.env.TEST_BASE_URL || 'http://127.0.0.1:3010'; const SERVER_SKIP = !(await fetch(`${BASE}/api/health`).then(r => r.ok).catch(() => false)); async function request(path, opts = {}) { const res = await fetch(`${BASE}${path}`, { headers: { 'Content-Type': 'application/json', ...opts.headers }, method: opts.method || 'GET', body: opts.body ? JSON.stringify(opts.body) : undefined, signal: AbortSignal.timeout(5000), }); const text = await res.text(); let body; try { body = JSON.parse(text); } catch { body = text; } return { res, body, status: res.status }; } describe('API Integration Tests', { skip: SERVER_SKIP }, () => { describe('Health endpoints', () => { it('GET /api/health returns 200', async () => { const { status, body } = await request('/api/v1/health'); assert.equal(status, 200); assert.ok(body.ok !== undefined); }); it('GET /api/ready returns 200 or 503', async () => { const { status } = await request('/api/v1/ready'); assert.ok(status === 200 || status === 503); }); }); describe('Public content endpoints', () => { it('lists articles', async () => { const { status, body } = await request('/api/v1/articles?limit=3'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('lists categories', async () => { const { status, body } = await request('/api/v1/categories'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('lists tags', async () => { const { status, body } = await request('/api/v1/tags'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('lists authors', async () => { const { status, body } = await request('/api/v1/authors'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('lists series', async () => { const { status, body } = await request('/api/v1/series'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('search works', async () => { const { status, body } = await request('/api/v1/search?q=AI'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('lists newsletter archive', async () => { const { status, body } = await request('/api/v1/newsletter/archive'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); it('lists jobs', async () => { const { status, body } = await request('/api/v1/jobs'); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); }); describe('Auth gates', () => { it('rejects unauthenticated access to protected endpoints', async () => { const endpoints = [ '/api/v1/auth/me', '/api/v1/auth/lists', '/api/v1/auth/history', '/api/v1/auth/notifications', '/api/v1/auth/badges', '/api/v1/membership/status', '/api/v1/admin/dashboard', '/api/v1/admin/articles', ]; for (const path of endpoints) { const { status, body } = await request(path); assert.equal(status, 401, `${path} should return 401`); assert.equal(body.error, 'unauthenticated', `${path} should return unauthenticated`); } }); }); describe('Request tracing', () => { it('returns x-request-id header', async () => { const res = await fetch(`${BASE}/api/health`); const reqId = res.headers.get('x-request-id'); assert.ok(reqId && reqId.length > 0, 'x-request-id header should be present'); }); it('echoes back sent x-request-id', async () => { const customId = 'test-req-123'; const res = await fetch(`${BASE}/api/health`, { headers: { 'x-request-id': customId }, }); const echoed = res.headers.get('x-request-id'); assert.equal(echoed, customId); }); }); describe('Security headers', () => { it('includes X-Content-Type-Options', async () => { const res = await fetch(`${BASE}/api/health`); assert.equal(res.headers.get('x-content-type-options'), 'nosniff'); }); it('includes X-Frame-Options', async () => { const res = await fetch(`${BASE}/api/health`); assert.equal(res.headers.get('x-frame-options'), 'SAMEORIGIN'); }); it('includes Content-Security-Policy', async () => { const res = await fetch(`${BASE}/api/health`); assert.ok(res.headers.get('content-security-policy')?.includes("default-src 'self'")); }); it('includes Permissions-Policy', async () => { const res = await fetch(`${BASE}/api/health`); assert.ok(res.headers.get('permissions-policy')); }); }); describe('404 handling', () => { it('returns 404 for missing article', async () => { const { status, body } = await request('/api/v1/articles/definitely-missing-article-xyz'); assert.equal(status, 404); assert.equal(body.error, 'not_found'); }); }); describe('Input validation', () => { it('rejects bad newsletter subscription', async () => { const { status, body } = await request('/api/v1/newsletter', { method: 'POST', body: { email: 'not-an-email' }, }); assert.equal(status, 400); assert.equal(body.error, 'invalid_input'); }); it('rejects bad error report', async () => { const { status, body } = await request('/api/v1/errors', { method: 'POST', body: { message: '' }, }); assert.equal(status, 400); assert.equal(body.error, 'invalid_input'); }); }); });