Files
TREK/server/tests/unit/nest/ferratas-overpass.client.test.ts

107 lines
4.3 KiB
TypeScript

import { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
import { FerratasOverpassClient } from '../../../src/nest/ferratas/ferratas-overpass.client';
import { parseOverpassEndpoints } from '../../../src/nest/maps/overpass.constants';
import { Test } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const integrations = { overpassUrl: undefined as string | undefined, overpassTimeoutMs: 5000 };
let client: FerratasOverpassClient;
beforeEach(async () => {
integrations.overpassUrl = 'https://a.example/api/interpreter,https://b.example/api/interpreter';
integrations.overpassTimeoutMs = 5000;
const moduleRef = await Test.createTestingModule({
providers: [FerratasOverpassClient, { provide: RuntimeEnvService, useValue: { env: () => ({ integrations }) } }],
}).compile();
client = moduleRef.get(FerratasOverpassClient);
vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status });
describe('parseOverpassEndpoints', () => {
it('reads a comma-separated list of http(s) endpoints', () => {
expect(parseOverpassEndpoints(' https://a.example/i , http://b.local/i ')).toEqual([
'https://a.example/i',
'http://b.local/i',
]);
});
it('drops what is not an http(s) URL', () => {
expect(parseOverpassEndpoints('ftp://x/i,not a url,https://ok.example/i')).toEqual(['https://ok.example/i']);
});
it('falls back to the public mirrors when nothing usable is configured', () => {
for (const raw of [undefined, '', 'nope']) {
const list = parseOverpassEndpoints(raw);
expect(list.length).toBeGreaterThan(1);
expect(list).toContain('https://overpass-api.de/api/interpreter');
}
});
});
describe('FerratasOverpassClient.query', () => {
it('POSTs the query with a user agent and a deadline, and answers the elements', async () => {
const fetcher = vi.fn(async (_url: string, _init: RequestInit) => json({ elements: [{ type: 'way', id: 1 }] }));
vi.stubGlobal('fetch', fetcher);
integrations.overpassUrl = 'https://a.example/api/interpreter';
const elements = await client.query('[out:json];way(1);out;');
expect(elements).toEqual([{ type: 'way', id: 1 }]);
const [url, init] = fetcher.mock.calls[0] ?? [];
expect(url).toBe('https://a.example/api/interpreter');
expect(init?.method).toBe('POST');
expect(init?.body).toBe(`data=${encodeURIComponent('[out:json];way(1);out;')}`);
expect((init?.headers as Record<string, string>)['User-Agent']).toMatch(/^TREK Travel Planner/);
expect(init?.signal).toBeInstanceOf(AbortSignal);
});
it('takes the first mirror that answers well and cancels the others', async () => {
const signals: AbortSignal[] = [];
vi.stubGlobal(
'fetch',
vi.fn(async (url: string, init: RequestInit) => {
if (init.signal) signals.push(init.signal);
if (url.startsWith('https://a.')) return json({ elements: [], remark: 'runtime error: timeout' });
return json({ elements: [{ type: 'way', id: 2 }] });
}),
);
expect(await client.query('q')).toEqual([{ type: 'way', id: 2 }]);
expect(signals.every((s) => s.aborted)).toBe(true);
});
it('throws once every mirror failed: an HTTP error, a remark, a body that is not OSM', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async (url: string) => (url.startsWith('https://a.') ? json({}, 504) : json({ elements: 'nope' }))),
);
await expect(client.query('q')).rejects.toThrow('Could not reach any Overpass endpoint');
});
it('throws when a mirror cannot be reached at all', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async () => {
throw new TypeError('fetch failed');
}),
);
await expect(client.query('q')).rejects.toThrow('Could not reach any Overpass endpoint');
});
it('refuses an answer larger than the cap', async () => {
vi.stubGlobal(
'fetch',
vi.fn(
async () =>
new Response('{"elements":[]}', { status: 200, headers: { 'content-length': String(9 * 1024 * 1024) } }),
),
);
await expect(client.query('q')).rejects.toThrow('Could not reach any Overpass endpoint');
});
});