import test from 'node:test'; import assert from 'node:assert/strict'; const BASE = process.env.TEST_BASE_URL || 'http://127.0.0.1:3010'; const TEST_ARTICLE_SLUG = process.env.TEST_ARTICLE_SLUG || 'agentic-ai-production-2026'; const REQUIRE_SERVER = process.env.TEST_REQUIRE_SERVER === '1'; async function serverAvailable() { try { const res = await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(1500) }); return res.status < 500; } catch { return false; } } const HAS_SERVER = await serverAvailable(); if (!HAS_SERVER && REQUIRE_SERVER) { throw new Error(`TEST_REQUIRE_SERVER=1 but no backend is reachable at ${BASE}`); } const SERVER_SKIP = HAS_SERVER ? false : `no backend reachable at ${BASE}`; async function request(path, options = {}) { const headers = { 'content-type': 'application/json', ...(options.headers || {}), }; const res = await fetch(`${BASE}${path}`, { ...options, headers }); const body = await res.json().catch(() => null); return { res, body }; } test('public content endpoints expose stable list payloads', { skip: SERVER_SKIP }, async () => { const endpoints = [ ['/api/v1/articles?limit=3', 'items'], ['/api/v1/categories', 'items'], ['/api/v1/tags', 'items'], ['/api/v1/authors', 'items'], ['/api/v1/series', 'items'], ['/api/v1/newsletter/archive', 'items'], ['/api/v1/jobs', 'items'], ['/api/v1/courses', 'items'], ]; for (const [path, key] of endpoints) { const { res, body } = await request(path); assert.equal(res.status, 200, `${path} should be reachable`); assert.ok(Array.isArray(body?.[key]), `${path} should return ${key} array`); } }); test('article detail, comments, related, and qa endpoints keep their contracts', { skip: SERVER_SKIP }, async () => { const article = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}`); assert.equal(article.res.status, 200); assert.ok(article.body?.article?.id); assert.equal(article.body.article.slug, TEST_ARTICLE_SLUG); const comments = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/comments`); assert.equal(comments.res.status, 200); assert.ok(Array.isArray(comments.body?.items)); const related = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/related`); assert.equal(related.res.status, 200); assert.ok(Array.isArray(related.body?.items)); const qa = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/qa`); assert.equal(qa.res.status, 200); assert.ok(Array.isArray(qa.body?.items)); }); test('missing public resources return 404 instead of leaking internals', { skip: SERVER_SKIP }, async () => { const article = await request('/api/v1/articles/definitely-missing-article'); assert.equal(article.res.status, 404); assert.equal(article.body?.error, 'not_found'); const profile = await request('/api/v1/profile/@definitely-missing-user'); assert.equal(profile.res.status, 404); assert.equal(profile.body?.error, 'not_found'); }); test('authenticated surfaces reject anonymous requests consistently', { skip: SERVER_SKIP }, async () => { const protectedGets = [ '/api/v1/auth/me', '/api/v1/auth/lists', '/api/v1/auth/history', '/api/v1/auth/notifications', '/api/v1/auth/notifications/preferences', '/api/v1/auth/badges', '/api/v1/membership/status', '/api/v1/admin/dashboard', '/api/v1/admin/errors', '/api/v1/admin/articles', ]; for (const path of protectedGets) { const { res, body } = await request(path); assert.equal(res.status, 401, `${path} should reject anonymous access`); assert.equal(body?.error, 'unauthenticated'); } }); test('public mutating endpoints validate bad input without creating records', { skip: SERVER_SKIP }, async () => { const newsletter = await request('/api/v1/newsletter', { method: 'POST', body: JSON.stringify({ email: 'not-an-email' }), }); assert.equal(newsletter.res.status, 400); assert.equal(newsletter.body?.error, 'invalid_input'); const errorReport = await request('/api/v1/errors', { method: 'POST', body: JSON.stringify({ message: '' }), }); assert.equal(errorReport.res.status, 400); assert.equal(errorReport.body?.error, 'invalid_input'); const job = await request('/api/v1/jobs', { method: 'POST', body: JSON.stringify({ company: 'x', title: 'y', description: 'too short' }), }); assert.equal(job.res.status, 400); assert.equal(job.body?.error, 'invalid_input'); });