From 46887066bc9577c3af1206d9b7210d39f6e1e56d Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:20:47 +0330 Subject: [PATCH] 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. --- app/backend/tests/integration.test.mjs | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 app/backend/tests/integration.test.mjs diff --git a/app/backend/tests/integration.test.mjs b/app/backend/tests/integration.test.mjs new file mode 100644 index 0000000..6ffcb59 --- /dev/null +++ b/app/backend/tests/integration.test.mjs @@ -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'); + }); + }); +});