← back to La Socrata Ingester

test/arcgis-retry.test.mjs

66 lines

// Regression test for TK-10851: ArcGIS tunnels errors as HTTP 200 with an
// {error:{code}} body, so the HTTP-level retry never sees them. fetchArcgis must
// RETRY transient codes (499/CONT_0001, 429, 5xx) and FAIL FAST on permanent ones (400).
// Run: node test/arcgis-retry.test.mjs   (zero deps; ~3s due to real backoff)
import { arcgisPages } from '../src/adapters/arcgis.js';

const src = { endpoint: 'http://fake/FeatureServer/0', paginate: 'oid', oidField: 'OBJECTID', geometry: false };
let fail = 0;

// 1) transient 499 twice, then success — must retry through, not throw
let calls = 0;
globalThis.fetch = async () => {
  calls++;
  const body = calls <= 2
    ? { error: { code: 499, messageCode: 'CONT_0001', message: 'Item does not exist or is inaccessible.' } }
    : { features: [{ attributes: { OBJECTID: 1, APN: '1234' } }] };
  return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
};
try {
  let rows = 0;
  for await (const page of arcgisPages(src, { maxRows: 1 })) rows += page.rows.length;
  const ok = rows === 1 && calls === 3;
  console.log(`TRANSIENT: ${ok ? 'PASS (retried through 2x 499, then succeeded)' : `FAIL (rows=${rows} calls=${calls})`}`);
  if (!ok) fail++;
} catch (e) {
  console.log(`TRANSIENT: FAIL (threw instead of retrying: ${e.message}, calls=${calls})`);
  fail++;
}

// 2) permanent 400 — must throw immediately, no backoff
let calls2 = 0;
globalThis.fetch = async () => {
  calls2++;
  const b = { error: { code: 400, message: 'Invalid query parameters' } };
  return { ok: true, status: 200, json: async () => b, text: async () => JSON.stringify(b) };
};
try {
  for await (const _ of arcgisPages(src, { maxRows: 1 })) { /* consume */ }
  console.log('PERMANENT: FAIL (did not throw on 400)');
  fail++;
} catch {
  const ok = calls2 === 1;
  console.log(`PERMANENT: ${ok ? 'PASS (400 threw immediately, no retry)' : `FAIL (retried a permanent error, calls=${calls2})`}`);
  if (!ok) fail++;
}

// 3) bare 499 "Token Required" (no CONT_0001) — a permanent auth failure, must fail fast
let calls3 = 0;
globalThis.fetch = async () => {
  calls3++;
  const b = { error: { code: 499, messageCode: 'GWM_0003', message: 'Token Required' } };
  return { ok: true, status: 200, json: async () => b, text: async () => JSON.stringify(b) };
};
try {
  for await (const _ of arcgisPages(src, { maxRows: 1 })) { /* consume */ }
  console.log('AUTH-499: FAIL (retried/masked a Token-Required auth failure)');
  fail++;
} catch {
  const ok = calls3 === 1;
  console.log(`AUTH-499: ${ok ? 'PASS (bare 499 threw immediately, not masked)' : `FAIL (retried a 499 auth error, calls=${calls3})`}`);
  if (!ok) fail++;
}

process.exitCode = fail ? 1 : 0;
console.log(fail ? `\n${fail} check(s) FAILED` : '\nAll checks PASS');