confluence_web/frontend/tests/contract.test.js
Artur Mukhamadiev 77768ba3ca credentials: offer the approved Confluence origin as a fixed choice
The origin check is an exact match including the context path, so users had
to type "https://collab.lge.com/main" precisely. GET /api/v1/config now
returns the approved origins in canonical form (non-secret: they are the only
destinations the backend will talk to), and the UI swaps the URL text field
for a select listing them, keeping the element id, focus handling and the
Test connection flow unchanged. The text field remains the fallback when the
fetch fails. Backend validation of the submitted URL is untouched. Mock server
serves the endpoint; contract, API and e2e tests cover it.
2026-09-15 15:24:17 +03:00

518 lines
20 KiB
JavaScript

/**
* Contract and Mock Server tests.
* Validates wire formats, security headers, session cookies, and scenarios against CONTRACTS.md.
*/
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { createMockServer, SCENARIOS } from '../dev/mock-server.js';
const TEST_PORT = 5199;
const BASE_URL = `http://127.0.0.1:${TEST_PORT}`;
describe('Mock Server and Wire Contract Tests', () => {
let server;
before(async () => {
server = createMockServer();
await new Promise((resolve) => {
server.listen(TEST_PORT, '127.0.0.1', resolve);
});
});
after(async () => {
await new Promise((resolve) => {
server.close(resolve);
});
});
test('GET /api/v1/config returns the approved origins with no-store', async () => {
const res = await fetch(`${BASE_URL}/api/v1/config`);
assert.equal(res.status, 200);
assert.equal(res.headers.get('cache-control'), 'no-store');
const data = await res.json();
assert.deepEqual(Object.keys(data), ['approved_origins']);
assert.deepEqual(data.approved_origins, ['https://approved.example.com']);
});
test('GET / sets cw_session cookie and serves security headers', async () => {
const res = await fetch(`${BASE_URL}/`);
assert.equal(res.status, 200);
// Security headers
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
assert.equal(res.headers.get('referrer-policy'), 'no-referrer');
const csp = res.headers.get('content-security-policy');
assert.ok(csp.includes("default-src 'none'"));
assert.ok(csp.includes("script-src 'self'"));
assert.ok(csp.includes("style-src 'self'"));
assert.ok(csp.includes("connect-src 'self'"));
assert.ok(csp.includes("img-src 'self'"));
// Session cookie
const cookie = res.headers.get('set-cookie');
assert.ok(cookie, 'Set-Cookie header must be present');
assert.ok(cookie.includes('cw_session='), 'Cookie name must be cw_session');
assert.ok(cookie.includes('HttpOnly'), 'Cookie must be HttpOnly');
assert.ok(cookie.includes('SameSite=Strict'), 'Cookie must be SameSite=Strict');
});
test('POST /api/v1/auth/verify succeeds with dummy credentials', async () => {
const res = await fetch(`${BASE_URL}/api/v1/auth/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
url: 'https://confluence.example.com',
pat: 'dummy-pat-token-123'
})
});
assert.equal(res.status, 200);
assert.equal(res.headers.get('cache-control'), 'no-store');
const body = await res.json();
assert.deepEqual(body, { valid: true });
});
test('POST /api/v1/auth/verify fails with 403 in 403_verify scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/auth/verify?scenario=403_verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
url: 'https://confluence.example.com',
pat: 'dummy-pat-token-123'
})
});
assert.equal(res.status, 403);
const body = await res.json();
assert.ok(body.error);
assert.equal(body.error.code, 'confluence_auth_failed');
});
test('POST /api/v1/query handles normal scenario matching section 7 shared contract', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=normal`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'deploy service X',
credentials: {
url: 'https://approved.example.com',
pat: 'dummy-pat-123'
}
})
});
assert.equal(res.status, 200);
assert.equal(res.headers.get('cache-control'), 'no-store');
const data = await res.json();
assert.ok(data.session_id);
assert.ok(data.markdown.includes('Deployment Guide'));
assert.equal(data.pages_accessed.length, 1);
assert.equal(data.pages_accessed[0].page_id, '847291');
assert.equal(data.pages_accessed[0].space, 'OPS');
assert.ok(data.pages_accessed[0].accessed_at);
assert.equal(data.tool_history.length, 2);
assert.equal(data.tool_history[0].tool, 'confluence_search');
assert.equal(data.tool_history[1].tool, 'confluence_view');
assert.equal(data.tool_history[1].cache_hit, false);
assert.equal(data.artifacts.length, 1);
assert.equal(data.artifacts[0].name, 'checklist.md');
assert.equal(data.artifacts[0].size_bytes, 32);
assert.ok(data.artifacts[0].expires_at);
});
test('GET /api/v1/artifacts/:id downloads exact 32 bytes for checklist.md', async () => {
const res = await fetch(`${BASE_URL}/api/v1/artifacts/art-checklist-01`);
assert.equal(res.status, 200);
assert.equal(res.headers.get('content-type'), 'application/octet-stream');
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
assert.ok(res.headers.get('content-disposition').includes('attachment; filename="checklist.md"'));
const text = await res.text();
assert.equal(text, '# Checklist\n\n- Deploy service X\n');
assert.equal(Buffer.byteLength(text, 'utf-8'), 32);
});
test('GET /api/v1/artifacts/:id returns 404 in unknown_expired_download scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/artifacts/art-checklist-01?scenario=unknown_expired_download`);
assert.equal(res.status, 404);
const body = await res.json();
assert.equal(body.error.code, 'artifact_not_found');
});
test('POST /api/v1/query handles 409 busy scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=409_busy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'test prompt',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 409);
const body = await res.json();
assert.equal(body.error.code, 'busy');
});
test('POST /api/v1/query handles 504 timeout scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=504_timeout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'test prompt',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 504);
const body = await res.json();
assert.equal(body.error.code, 'query_timeout');
});
test('POST /api/v1/query handles empty_search scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=empty_search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'non-existent query',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.pages_accessed.length, 0);
assert.equal(body.artifacts.length, 0);
assert.equal(body.tool_history.length, 1);
assert.equal(body.tool_history[0].result.pages.length, 0);
});
test('POST /api/v1/query handles repeated_cached_view scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=repeated_cached_view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'cache test',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.pages_accessed.length, 1);
assert.equal(body.tool_history.length, 2);
assert.equal(body.tool_history[0].cache_hit, false);
assert.equal(body.tool_history[1].cache_hit, true);
});
test('POST /api/v1/query handles failed_tool scenario with status="error"', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=failed_tool`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'failed tool test',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 200);
const body = await res.json();
const errorTool = body.tool_history.find((t) => t.status === 'error');
assert.ok(errorTool);
assert.equal(errorTool.result, null);
assert.equal(errorTool.error.code, 'page_not_found');
});
test('POST /api/v1/query handles warning_truncated_history scenario', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=warning_truncated_history`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': BASE_URL
},
body: JSON.stringify({
prompt: 'truncation test',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(body.warnings.length > 0);
assert.equal(body.tool_history[0].parameters_truncated, true);
assert.equal(body.tool_history[0].result_truncated, true);
});
test('Origin check rejects untrusted external origins', async () => {
const res = await fetch(`${BASE_URL}/api/v1/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Origin': 'https://evil-attacker.example.com'
},
body: JSON.stringify({
prompt: 'attack',
credentials: { url: 'https://example.com', pat: 'dummy' }
})
});
assert.equal(res.status, 403);
const body = await res.json();
assert.equal(body.error.code, 'origin_denied');
});
/**
* Extracts `cw_session=...` from a Set-Cookie header so subsequent requests in the same
* test can carry the session forward (raw fetch has no cookie jar).
*/
function extractSessionCookie(res, previous) {
const setCookie = res.headers.get('set-cookie');
if (!setCookie) return previous;
const match = setCookie.match(/cw_session=[^;]+/);
return match ? match[0] : previous;
}
describe('Admission queue scenarios (docs/QUEUE_SPECIFICATION.md §10)', () => {
test('queue_full: join answers 503 queue_full', async () => {
const res = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=queue_full`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
body: '{}'
});
assert.equal(res.status, 503);
assert.equal(res.headers.get('cache-control'), 'no-store');
const body = await res.json();
assert.equal(body.error.code, 'queue_full');
});
test('queued: position 3 with eta, decrements on each poll, ready on the third, query succeeds', async () => {
let cookie;
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=queued`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
body: '{}'
});
cookie = extractSessionCookie(joinRes, cookie);
assert.equal(joinRes.status, 200);
assert.equal(joinRes.headers.get('cache-control'), 'no-store');
const joinBody = await joinRes.json();
assert.equal(joinBody.status, 'queued');
assert.equal(joinBody.position, 3);
assert.equal(joinBody.ahead, 2);
assert.equal(joinBody.eta_seconds, 150);
assert.ok(joinBody.ticket_id);
const poll1 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued`, {
headers: { Cookie: cookie }
});
const poll1Body = await poll1.json();
assert.equal(poll1Body.status, 'queued');
assert.equal(poll1Body.position, 2);
assert.equal(poll1Body.ticket_id, joinBody.ticket_id);
const poll2 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued`, {
headers: { Cookie: cookie }
});
const poll2Body = await poll2.json();
assert.equal(poll2Body.status, 'queued');
assert.equal(poll2Body.position, 1);
const poll3 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued`, {
headers: { Cookie: cookie }
});
const poll3Body = await poll3.json();
assert.equal(poll3Body.status, 'ready');
assert.ok(poll3Body.reservation_expires_in_seconds > 0);
const queryRes = await fetch(`${BASE_URL}/api/v1/query?scenario=queued`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
body: JSON.stringify({ prompt: 'deploy service X', credentials: { url: 'https://example.com', pat: 'dummy' } })
});
assert.equal(queryRes.status, 200);
const queryBody = await queryRes.json();
assert.ok(queryBody.markdown.includes('Deployment Guide'));
});
test('queued_no_estimate: eta_seconds is null at every step', async () => {
let cookie;
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=queued_no_estimate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
body: '{}'
});
cookie = extractSessionCookie(joinRes, cookie);
const joinBody = await joinRes.json();
assert.equal(joinBody.status, 'queued');
assert.equal(joinBody.eta_seconds, null);
const poll1Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued_no_estimate`, { headers: { Cookie: cookie } })).json();
assert.equal(poll1Body.eta_seconds, null);
const poll2Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued_no_estimate`, { headers: { Cookie: cookie } })).json();
assert.equal(poll2Body.eta_seconds, null);
const poll3Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued_no_estimate`, { headers: { Cookie: cookie } })).json();
assert.equal(poll3Body.status, 'ready');
});
test('reservation_lost: query 409s once after ready, rejoin succeeds, second query succeeds', async () => {
let cookie;
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=reservation_lost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
body: '{}'
});
cookie = extractSessionCookie(joinRes, cookie);
const joinBody = await joinRes.json();
assert.equal(joinBody.status, 'ready');
const firstQuery = await fetch(`${BASE_URL}/api/v1/query?scenario=reservation_lost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
body: JSON.stringify({ prompt: 'p', credentials: { url: 'https://example.com', pat: 'dummy' } })
});
assert.equal(firstQuery.status, 409);
const firstQueryBody = await firstQuery.json();
assert.equal(firstQueryBody.error.code, 'busy');
const rejoinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=reservation_lost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
body: '{}'
});
const rejoinBody = await rejoinRes.json();
assert.equal(rejoinBody.status, 'ready');
const secondQuery = await fetch(`${BASE_URL}/api/v1/query?scenario=reservation_lost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
body: JSON.stringify({ prompt: 'p', credentials: { url: 'https://example.com', pat: 'dummy' } })
});
assert.equal(secondQuery.status, 200);
});
test('ticket_lost: second status poll answers 404, rejoin answers ready', async () => {
let cookie;
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=ticket_lost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
body: '{}'
});
cookie = extractSessionCookie(joinRes, cookie);
const joinBody = await joinRes.json();
assert.equal(joinBody.status, 'queued');
const poll1Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=ticket_lost`, { headers: { Cookie: cookie } })).json();
assert.equal(poll1Body.status, 'queued');
const poll2 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=ticket_lost`, { headers: { Cookie: cookie } });
assert.equal(poll2.status, 404);
const poll2Body = await poll2.json();
assert.equal(poll2Body.error.code, 'ticket_not_found');
const rejoinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=ticket_lost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
body: '{}'
});
const rejoinBody = await rejoinRes.json();
assert.equal(rejoinBody.status, 'ready');
});
test('GET /api/v1/queue/status with no ticket answers 404 ticket_not_found', async () => {
const res = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=normal`);
assert.equal(res.status, 404);
const body = await res.json();
assert.equal(body.error.code, 'ticket_not_found');
});
test('DELETE /api/v1/queue/ticket always answers 204', async () => {
const res = await fetch(`${BASE_URL}/api/v1/queue/ticket`, {
method: 'DELETE',
headers: { Origin: BASE_URL }
});
assert.equal(res.status, 204);
assert.equal(res.headers.get('cache-control'), 'no-store');
});
test('POST /api/v1/queue/join rejects a body containing credentials or pat', async () => {
const withCredentials = await fetch(`${BASE_URL}/api/v1/queue/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Origin: BASE_URL },
body: JSON.stringify({ credentials: { url: 'https://x', pat: 'y' } })
});
assert.equal(withCredentials.status, 400);
assert.equal((await withCredentials.json()).error.code, 'invalid_input');
const withPat = await fetch(`${BASE_URL}/api/v1/queue/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Origin: BASE_URL },
body: JSON.stringify({ pat: 'y' })
});
assert.equal(withPat.status, 400);
assert.equal((await withPat.json()).error.code, 'invalid_input');
});
test('non-queue scenarios answer join with ready immediately (unchanged flows)', async () => {
const res = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=normal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Origin: BASE_URL },
body: '{}'
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.status, 'ready');
});
});
test('Mock dev toolbar external assets served with correct headers and zero inline script/style', async () => {
// CSS asset
const cssRes = await fetch(`${BASE_URL}/dev/scenario-toolbar.css`);
assert.equal(cssRes.status, 200);
assert.ok(cssRes.headers.get('content-type').includes('text/css'));
assert.equal(cssRes.headers.get('x-content-type-options'), 'nosniff');
// JS asset
const jsRes = await fetch(`${BASE_URL}/dev/scenario-toolbar.js`);
assert.equal(jsRes.status, 200);
assert.ok(jsRes.headers.get('content-type').includes('javascript'));
assert.equal(jsRes.headers.get('x-content-type-options'), 'nosniff');
// Root HTML page must not contain inline scripts or inline style attributes
const htmlRes = await fetch(`${BASE_URL}/`);
const html = await htmlRes.text();
assert.ok(!/<script(?![^>]*src=)[^>]*>/i.test(html), 'Root HTML in dev mode must not contain inline <script>');
assert.ok(!/style\s*=\s*["'][^"']*["']/i.test(html), 'Root HTML in dev mode must not contain inline style attributes');
});
});