diff --git a/app/backend/tests/phase10-smoke.test.mjs b/app/backend/tests/phase10-smoke.test.mjs new file mode 100644 index 0000000..ecbf380 --- /dev/null +++ b/app/backend/tests/phase10-smoke.test.mjs @@ -0,0 +1,73 @@ +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 SESSION = process.env.TEST_SESSION_COOKIE || ''; +const CSRF = process.env.TEST_CSRF_TOKEN || ''; +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', + ...(SESSION ? { cookie: SESSION } : {}), + ...(CSRF ? { 'x-csrf-token': CSRF } : {}), + ...(options.headers || {}), + }; + return fetch(`${BASE}${path}`, { ...options, headers }); +} + +test('auth rejects invalid credentials without leaking internals', { skip: SERVER_SKIP }, async () => { + const res = await request('/api/v1/auth/login', { + method: 'POST', + body: JSON.stringify({ identifier: 'nobody@example.invalid', password: 'wrong', captchaId: 'invalid', userSolution: '0000' }), + }); + assert.ok([400, 401, 429].includes(res.status)); + const body = await res.json().catch(() => ({})); + assert.ok(body.error || body.message); +}); + +test('public article and comments endpoints are reachable', { skip: SERVER_SKIP }, async () => { + const articleRes = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}`); + assert.equal(articleRes.status, 200); + const articleBody = await articleRes.json(); + assert.ok(articleBody.article?.id); + + const commentsRes = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/comments`); + assert.equal(commentsRes.status, 200); + const commentsBody = await commentsRes.json(); + assert.ok(Array.isArray(commentsBody.items)); +}); + +test('authenticated comment flow works when TEST_SESSION_COOKIE and TEST_CSRF_TOKEN are set', { skip: SERVER_SKIP || !SESSION || !CSRF }, async () => { + const res = await request(`/api/v1/articles/${TEST_ARTICLE_SLUG}/comments`, { + method: 'POST', + body: JSON.stringify({ body: `Phase 10 smoke ${Date.now()}` }), + }); + assert.equal(res.status, 200); + const body = await res.json(); + assert.ok(body.comment?.id); +}); + +test('authenticated article publish workflow is protected and callable when configured', { skip: SERVER_SKIP || !SESSION || !CSRF || !process.env.TEST_PUBLISH_ARTICLE_ID }, async () => { + const res = await request(`/api/v1/admin/articles/${encodeURIComponent(process.env.TEST_PUBLISH_ARTICLE_ID)}/publish`, { + method: 'POST', + body: JSON.stringify({}), + }); + assert.ok([200, 403, 404].includes(res.status)); +});