← 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);
})();