#!/usr/bin/env node /** * Integration test for the Content Editor /logout endpoint. * Spawns the real server on an ephemeral port and verifies: * 1. /logout always answers 401 + WWW-Authenticate (invalidates cached Basic Auth) * 2. /logout is exempt from the auth rate limiter (logging out never locks the user out) * 3. the auth rate limiter still works for real failed logins (429 after the limit) */ const assert = require('assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); const PORT = 4123; const BASE = `http://127.0.0.1:${PORT}`; const ROOT = path.join(__dirname, '..'); const AUDIT_FILE = path.join(os.tmpdir(), `content-editor-audit-test-${process.pid}.jsonl`); const env = { ...process.env, CONTENT_EDITOR_PORT: String(PORT), CONTENT_EDITOR_AUDIT_FILE: AUDIT_FILE, CMS_USER: 'logout-test-user', CMS_PASS: 'logout-test-pass', CMS_DEPLOY_ENV: 'staging', }; const child = spawn('node', ['content-editor.js'], { cwd: ROOT, env, stdio: 'ignore' }); async function waitForServer(timeoutMs = 10000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { // Poll /logout (rate-limit-free) so the readiness probe itself never // consumes a failed-login attempt from the auth rate limiter. await fetch(`${BASE}/logout`); return; } catch { await new Promise(r => setTimeout(r, 200)); } } throw new Error('server did not start'); } async function main() { await waitForServer(); // 1. /logout answers 401 without a Basic challenge (Safari would pop its // native auth dialog on challenged fetch calls) const logoutRes = await fetch(`${BASE}/logout`); assert.equal(logoutRes.status, 401); assert.equal(logoutRes.headers.get('www-authenticate'), null); // 2. /logout is exempt from the auth rate limiter: many logout calls must not // consume the failed-login budget. for (let i = 0; i < 10; i++) { const res = await fetch(`${BASE}/logout`); assert.equal(res.status, 401); } // A failed real login right after the logout flood must still be 401, not 429. const failed = await fetch(`${BASE}/`, { headers: { 'Authorization': 'Basic ' + Buffer.from('logout-test-user:wrong').toString('base64') }, }); assert.equal(failed.status, 401); // 3. The limiter still engages after repeated real failures (5 allowed, 6th → 429) for (let i = 0; i < 4; i++) { const res = await fetch(`${BASE}/`, { headers: { 'Authorization': 'Basic ' + Buffer.from('logout-test-user:wrong').toString('base64') }, }); assert.equal(res.status, 401); } const limited = await fetch(`${BASE}/`, { headers: { 'Authorization': 'Basic ' + Buffer.from('logout-test-user:wrong').toString('base64') }, }); assert.equal(limited.status, 429); // Valid credentials would now also be throttled — that is expected limiter behavior. console.log('Content Editor logout endpoint test: OK'); } main() .catch(err => { console.error('❌', err.message); process.exitCode = 1; }) .finally(() => { child.kill('SIGTERM'); try { fs.unlinkSync(AUDIT_FILE); } catch { /* already gone */ } });