← back to Dw Signup Fulfillment

scripts/tk11114-logging-test.js

97 lines

'use strict';
// TK-11114 focused test: prove the un-swallow fix.
//  - On a George FAILURE (401), verify.startVerification returns ok:false with a REASON
//    (reason:'send_failed', status, error) AND the [email]/[verify] logs surface the status
//    — but NEVER leak the Basic-auth password or the send token (credential-safe).
//  - On George SUCCESS (200), it returns ok:true and logs "sent via George".
// Self-contained: a stub George on 127.0.0.1 (no network, no real email, no Shopify).

const http = require('http');
const assert = require('assert');

const SECRET_PASS = 'SUPERSECRETPASS_do_not_log_123';
const SECRET_TOKEN = 'SENDTOKEN_do_not_log_456';

function withStub(statusCode, body, run) {
  return new Promise((resolve, reject) => {
    const srv = http.createServer((req, res) => {
      let d = ''; req.on('data', c => d += c);
      req.on('end', () => { res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); });
    });
    srv.listen(0, '127.0.0.1', async () => {
      const port = srv.address().port;
      try { const r = await run(port); srv.close(() => resolve(r)); }
      catch (e) { srv.close(() => reject(e)); }
    });
  });
}

// Capture console output for the duration of one call.
function captureConsole(fn) {
  const lines = [];
  const origLog = console.log, origWarn = console.warn, origErr = console.error;
  console.log = (...a) => lines.push(a.join(' '));
  console.warn = (...a) => lines.push(a.join(' '));
  console.error = (...a) => lines.push(a.join(' '));
  return Promise.resolve()
    .then(fn)
    .then((v) => { console.log = origLog; console.warn = origWarn; console.error = origErr; return { value: v, out: lines.join('\n') }; })
    .catch((e) => { console.log = origLog; console.warn = origWarn; console.error = origErr; throw e; });
}

async function loadVerifyPointedAt(port) {
  // Set env BEFORE requiring config so it resolves to the stub + a live (DRY_RUN=0) send path.
  process.env.DRY_RUN = '0';
  process.env.GEORGE_URL = `http://127.0.0.1:${port}`;
  process.env.DW_SIGNUP_VERIFY_SECRET = 'tk11114-test-secret';
  process.env.GEORGE_BASIC_AUTH = `testuser:${SECRET_PASS}`;
  process.env.GEORGE_EXTERNAL_SEND_TOKEN = SECRET_TOKEN;
  // Fresh module instances so the env above is read at load.
  for (const m of ['../lib/config', '../lib/email', '../lib/verify']) delete require.cache[require.resolve(m)];
  return require('../lib/verify');
}

async function main() {
  let failures = 0;
  const fail = (msg) => { console.error('  FAIL:', msg); failures++; };
  const pass = (msg) => console.log('  PASS:', msg);

  // ---- Case 1: George 401 (the real bug shape) ----
  await withStub(401, { ok: false, error: 'unauthorized' }, async (port) => {
    const { value: res, out } = await captureConsole(async () => {
      const verify = await loadVerifyPointedAt(port);
      return verify.startVerification({ email: 'buyer@example.com', customerId: '123', firstName: 'Test' });
    });
    try {
      assert.strictEqual(res.ok, false, 'result.ok should be false on 401'); pass('401 → ok:false');
      assert.strictEqual(res.reason, 'send_failed', 'reason should be send_failed'); pass('401 → reason:send_failed (propagated, not swallowed)');
      assert.strictEqual(res.status, 401, 'status should be 401'); pass('401 → status:401 surfaced');
      assert.ok(/SEND FAILED via George/.test(out), 'email.js must log SEND FAILED'); pass('[email] logged SEND FAILED');
      assert.ok(/status=401/.test(out), 'log must include status=401'); pass('log includes status=401');
      assert.ok(/verify-email SEND FAILED/.test(out), 'verify.js must warn'); pass('[verify] logged the failure reason');
      // CREDENTIAL SAFETY — the secret pass + token must NOT appear anywhere in the logs.
      assert.ok(!out.includes(SECRET_PASS), 'Basic-auth password must NOT be logged'); pass('no Basic-auth password in logs');
      assert.ok(!out.includes(SECRET_TOKEN), 'send token must NOT be logged'); pass('no send token in logs');
      assert.ok(!/verify\?token=/.test(out), 'verify token URL must NOT be logged'); pass('no verify token URL in logs');
    } catch (e) { fail(e.message); }
  });

  // ---- Case 2: George 200 (happy path) ----
  await withStub(200, { ok: true, id: 'msg_1' }, async (port) => {
    const { value: res, out } = await captureConsole(async () => {
      const verify = await loadVerifyPointedAt(port);
      return verify.startVerification({ email: 'buyer@example.com', customerId: '123', firstName: 'Test' });
    });
    try {
      assert.strictEqual(res.ok, true, 'result.ok should be true on 200'); pass('200 → ok:true');
      assert.ok(/sent via George/.test(out), 'should log a success line'); pass('[email] logged success');
      assert.ok(!out.includes(SECRET_PASS) && !out.includes(SECRET_TOKEN), 'no secrets on success path'); pass('no secrets in success logs');
    } catch (e) { fail(e.message); }
  });

  if (failures) { console.error(`\nTK-11114 logging test: ${failures} FAILURE(S)`); process.exit(1); }
  console.log('\nTK-11114 logging test: ALL PASS');
}

main().catch((e) => { console.error('test crashed:', e); process.exit(1); });