87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
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<Response>((_, 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(45_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(45_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, /Таймаут.*45 секунд/);
|
|
for (let i = 0; i < OVERPASS_ENDPOINTS.length; i++) {
|
|
t.mock.timers.tick(45_000);
|
|
await Promise.resolve();
|
|
}
|
|
await check;
|
|
assert.equal(fetch.mock.callCount(), OVERPASS_ENDPOINTS.length);
|
|
});
|