← back to Costa Rica

test/apple-route-timeout.test.js

65 lines

'use strict';
// Route-level proof (Cody gate, cycle 13) matching the plaid-routes precedent: a
// jwks() PROVIDER_TIMEOUT during POST /auth/apple must return 401 AND touch NO DB
// (the account link/create only runs after verification succeeds). Unlike
// apple-route.test.js (which stubs verifyIdentityToken out entirely) this drives the
// REAL verifier through a faked-stalled fetch so the timeout actually flows through
// the route's try/catch. Own process -> apple's JWKS cache is cold, so jwks() fetches.

const { test, before, after } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const express = require('express');

const db = require('../lib/db');
const { router } = require('../routes/app');

let calls = [];
const origQuery = db.pool.query;
const realFetch = global.fetch;
let server, base;

before(async () => {
  db.pool.query = async (sql, args) => { calls.push({ sql, args }); return { rows: [], rowCount: 0 }; };
  const app = express();
  app.use(express.json());
  app.use('/api/app', router);
  await new Promise(r => { server = app.listen(0, r); });
  base = `http://127.0.0.1:${server.address().port}`;
});
after(() => { db.pool.query = origQuery; global.fetch = realFetch; server && server.close(); });

function post(path, body) {
  const data = JSON.stringify(body);
  return new Promise((resolve, reject) => {
    const r = http.request(base + path, { method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } },
      res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode, json: JSON.parse(b || '{}') })); });
    r.on('error', reject); r.end(data);
  });
}

// A token with valid base64url header+payload so verifyIdentityToken reaches jwks()
// (header {alg,kid}, payload {sub}) rather than throwing on JSON parse first. Built
// from parts (not a literal) so a secret-scanner doesn't flag a JWT-shaped constant.
const b64u = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
const TOKEN = `${b64u({ alg: 'RS256', kid: 'any' })}.${b64u({ sub: 'x' })}.sig`;

test('POST /auth/apple: a jwks() timeout -> 401 and NO DB write (account link/create never runs)', async () => {
  process.env.PROVIDER_HTTP_TIMEOUT_MS = '40';
  calls = [];
  global.fetch = (url, opts) => Promise.resolve({
    ok: true, status: 200,
    json: () => new Promise((_resolve, reject) => {
      opts.signal.addEventListener('abort', () => { const e = new Error('aborted'); e.name = 'AbortError'; reject(e); });
    }),
  });
  try {
    const r = await post('/api/app/auth/apple', { identity_token: TOKEN });
    assert.equal(r.status, 401, 'a verification timeout is a clean 401, not a hang/500');
    assert.equal(calls.length, 0, 'no app_users SELECT/UPDATE/INSERT runs when verification fails');
  } finally {
    global.fetch = realFetch;
    delete process.env.PROVIDER_HTTP_TIMEOUT_MS;
  }
});