import assert from 'node:assert/strict'; import { test } from 'node:test'; import { fetchOverpass, OVERPASS_ENDPOINTS, type BBox } from '../src/osm/overpass'; const bbox: BBox = [35.3, -112.9, 35.31, -112.89]; const elements = [{ type: 'node', id: 1 }]; const ok = () => Response.json({ elements }); const pending = (signal: AbortSignal) => new Promise((_, reject) => { signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true }); }); test('starts with Mail.ru and falls back after an HTTP error', async (t) => { const urls: string[] = []; t.mock.method(globalThis, 'fetch', async (url: string) => { urls.push(url); return urls.length === 1 ? new Response('', { status: 406 }) : ok(); }); assert.deepEqual(await fetchOverpass(bbox), elements); assert.match(urls[0], /maps\.mail\.ru/); assert.deepEqual(urls, OVERPASS_ENDPOINTS.slice(0, 2)); }); test('times out a hanging request and tries the next server', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); let calls = 0; t.mock.method(globalThis, 'fetch', (_url: string, init: RequestInit) => { return ++calls === 1 ? pending(init.signal!) : Promise.resolve(ok()); }); const result = fetchOverpass(bbox); t.mock.timers.tick(90_000); assert.deepEqual(await result, elements); assert.equal(calls, 2); }); test('timeout also covers downloading the response body', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); let calls = 0; t.mock.method(globalThis, 'fetch', async (_url: string, init: RequestInit) => { if (++calls > 1) return ok(); return { ok: true, status: 200, json: () => pending(init.signal!) }; }); const result = fetchOverpass(bbox); await Promise.resolve(); t.mock.timers.tick(90_000); assert.deepEqual(await result, elements); assert.equal(calls, 2); }); test('user cancellation stops the active request without fallback', async (t) => { const controller = new AbortController(); const fetch = t.mock.method(globalThis, 'fetch', (_url: string, init: RequestInit) => pending(init.signal!)); const result = fetchOverpass(bbox, undefined, controller.signal); controller.abort(); await assert.rejects(result, { name: 'AbortError' }); assert.equal(fetch.mock.callCount(), 1); }); test('user cancellation interrupts the retry delay', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); const controller = new AbortController(); const fetch = t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 429 })); const result = fetchOverpass(bbox, undefined, controller.signal); await Promise.resolve(); controller.abort(); await assert.rejects(result, { name: 'AbortError' }); assert.equal(fetch.mock.callCount(), 1); }); test('an already cancelled operation makes no requests', async (t) => { const fetch = t.mock.method(globalThis, 'fetch', async () => ok()); await assert.rejects(fetchOverpass(bbox, undefined, AbortSignal.abort()), { name: 'AbortError' }); assert.equal(fetch.mock.callCount(), 0); }); test('reports failure when every server times out', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); const fetch = t.mock.method(globalThis, 'fetch', (_url: string, init: RequestInit) => pending(init.signal!)); const result = fetchOverpass(bbox); const check = assert.rejects(result, /Таймаут.*90 секунд/); for (let i = 0; i < OVERPASS_ENDPOINTS.length; i++) { t.mock.timers.tick(90_000); await Promise.resolve(); } await check; assert.equal(fetch.mock.callCount(), OVERPASS_ENDPOINTS.length); }); test('server identifies itself to Overpass rather than using the generic Node user agent', async (t) => { t.mock.method(globalThis, 'fetch', async (_url: string, init: RequestInit) => { assert.match(new Headers(init.headers).get('User-Agent')!, /citygen/); return ok(); }); assert.deepEqual(await fetchOverpass(bbox), elements); });