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:
arsam
2026-09-18 23:20:47 +03:30
parent ea897c34c4
commit 46887066bc
+170
View File
@@ -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');
});
});
});