← back to Studio Zero
test/auth.spec.js
67 lines
// Regression guard for the optional Basic-Auth gate (SZ_BASIC_AUTH, DTD-B refinement, 2026-07-26).
// The gate MUST be default-OFF (byte-for-byte open, so it never affects the internal :9761
// service or the /deploy /api/config smoke test) and, when flipped on, MUST reject missing/wrong
// credentials with 401 + a WWW-Authenticate challenge and admit the correct ones.
//
// Pure-HTTP, zero dependencies (no browser/playwright). Spawns its own studio-zero instances.
// Run: node test/auth.spec.js
const http = require('http');
const { spawn } = require('child_process');
const path = require('path');
function get(port, pathname, creds) {
return new Promise((resolve, reject) => {
const headers = {};
if (creds) headers.Authorization = 'Basic ' + Buffer.from(creds).toString('base64');
const r = http.get({ host: '127.0.0.1', port, path: pathname, headers }, res => {
res.resume();
resolve({ code: res.statusCode, wwwAuth: res.headers['www-authenticate'] || '' });
});
r.on('error', reject);
});
}
function waitReady(port, ms) {
const t0 = Date.now();
return new Promise((resolve, reject) => {
(function ping() {
const r = http.get({ host: '127.0.0.1', port, path: '/' }, res => { res.resume(); resolve(); });
r.on('error', () => { if (Date.now() - t0 > ms) reject(new Error('server never came up on ' + port)); else setTimeout(ping, 120); });
})();
});
}
function boot(port, extraEnv) {
return spawn('node', ['server.js'], {
cwd: path.join(__dirname, '..'),
env: { ...process.env, PORT: String(port), SZ_PROVIDER: 'demo', ...extraEnv },
stdio: 'ignore',
});
}
(async () => {
const log = []; const ok = (c, m) => log.push((c ? 'PASS ' : 'FAIL ') + m);
const OFF = 4188, ON = 4189;
const s1 = boot(OFF, {}); // default: flag unset → OPEN
const s2 = boot(ON, { SZ_BASIC_AUTH: '1' }); // gated
try {
await Promise.all([waitReady(OFF, 8000), waitReady(ON, 8000)]);
// --- default OFF: unchanged, fully open ---
ok((await get(OFF, '/api/config')).code === 200, 'default (flag unset): /api/config open (200, unchanged)');
ok((await get(OFF, '/')).code === 200, 'default (flag unset): / open (200, unchanged)');
// --- flag ON: gated ---
const noCreds = await get(ON, '/api/config');
ok(noCreds.code === 401, 'gated: no creds → 401');
ok(/^Basic /i.test(noCreds.wwwAuth), 'gated: 401 carries a Basic WWW-Authenticate challenge');
ok((await get(ON, '/api/config', 'admin:wrongpass')).code === 401, 'gated: wrong creds → 401');
ok((await get(ON, '/api/config', 'admin:DW2024!')).code === 200, 'gated: correct creds → 200');
ok((await get(ON, '/', 'admin:DW2024!')).code === 200, 'gated: correct creds admit the app too');
} catch (e) {
ok(false, 'HARNESS: ' + e.message);
} finally {
s1.kill('SIGKILL'); s2.kill('SIGKILL');
}
console.log(log.join('\n'));
process.exit(log.some(l => l.startsWith('FAIL')) ? 1 : 0);
})();