← back to Costa Rica

test/async-harden.test.js

101 lines

'use strict';
// harden()/asyncWrap() — Express 4 async-error safety (Cody gate, cycle 11, TK-10346).
// Proves an uncaught throw/rejection in a hardened route reaches error middleware
// (clean 500, no hang, no crash), a normal route is unaffected, and a route that
// catches its own error keeps its own response (try/catch still wins).

const { test, before, after } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const express = require('express');
const { asyncWrap, harden } = require('../lib/async-harden');

// --- unit: asyncWrap forwards both sync throws and async rejections ---
test('asyncWrap: a synchronous throw is forwarded to next(err)', () => {
  let forwarded = null;
  const wrapped = asyncWrap(() => { throw new Error('sync-boom'); });
  wrapped({}, {}, (e) => { forwarded = e; });
  assert.ok(forwarded && /sync-boom/.test(forwarded.message));
});

test('asyncWrap: an async rejection is forwarded to next(err)', async () => {
  let forwarded = null;
  const wrapped = asyncWrap(async () => { throw new Error('async-boom'); });
  wrapped({}, {}, (e) => { forwarded = e; });
  await new Promise(r => setImmediate(r)); // let the rejection settle
  assert.ok(forwarded && /async-boom/.test(forwarded.message));
});

test('asyncWrap: a handler that resolves normally does NOT call next with an error', async () => {
  let nextErr = 'unset';
  const wrapped = asyncWrap(async (req, res) => { res.ok = true; });
  const res = {};
  wrapped({}, res, (e) => { nextErr = e; });
  await new Promise(r => setImmediate(r));
  assert.equal(res.ok, true);
  assert.equal(nextErr, 'unset', 'next(err) not called on success');
});

test('harden: is idempotent — a second call does not double-wrap', () => {
  const router = express.Router();
  router.get('/x', async (_req, res) => res.end());
  harden(router);
  const h1 = router.stack.find(l => l.route && l.route.path === '/x').route.stack[0].handle;
  harden(router);
  const h2 = router.stack.find(l => l.route && l.route.path === '/x').route.stack[0].handle;
  assert.strictEqual(h1, h2, 'handler is wrapped once, not re-wrapped');
  assert.equal(h1.__hardened, true);
});

// --- E2E: a hardened sub-router behind a global error handler ---
let server, base;
before(async () => {
  const router = express.Router();
  router.get('/boom', async () => { throw new Error('kaboom — uncaught in an async route'); });
  router.get('/boom-sync', (_req, _res) => { throw new Error('sync kaboom'); });
  router.get('/ok', async (_req, res) => res.json({ ok: true, hi: 'there' }));
  router.get('/caught', async (_req, res) => {
    try { throw new Error('handled internally'); }
    catch { res.status(400).json({ ok: false, caught: true }); }
  });
  harden(router);

  const app = express();
  app.use('/api', router);
  // Global error handler mounted last, mirroring server.js.
  app.use((err, req, res, _next) => { res.status(500).json({ ok: false, error: 'internal error', path: req.path }); });
  await new Promise(r => { server = app.listen(0, r); });
  base = `http://127.0.0.1:${server.address().port}`;
});
after(() => server && server.close());

function get(path) {
  return new Promise((resolve, reject) => {
    http.get(base + path, res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode, json: JSON.parse(b || '{}') })); }).on('error', reject);
  });
}

test('E2E: an uncaught ASYNC throw -> clean 500 (not a hang, not a crash)', async () => {
  const r = await get('/api/boom');
  assert.equal(r.status, 500);
  assert.equal(r.json.ok, false);
  assert.equal(r.json.error, 'internal error', 'generic message, no internal detail leaked');
});

test('E2E: an uncaught SYNC throw -> clean 500 too', async () => {
  const r = await get('/api/boom-sync');
  assert.equal(r.status, 500);
});

test('E2E: a normal route is unaffected by harden', async () => {
  const r = await get('/api/ok');
  assert.equal(r.status, 200);
  assert.deepEqual(r.json, { ok: true, hi: 'there' });
});

test('E2E: a route that catches its own error keeps its own response (try/catch still wins, not a 500)', async () => {
  const r = await get('/api/caught');
  assert.equal(r.status, 400, 'the handler self-handled -> its 400, not the error handler 500');
  assert.equal(r.json.caught, true);
});