Import tests: app/backend/tests/integration.test.mjs
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user