From 7907963e036d75ca0da98c6413e5f794f864286a Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 18 Oct 2026 22:35:33 +0000 Subject: [PATCH] test(tours): the region load's failure paths and Geofabrik's size question are tested The paddle obstacles domain held 90 % of its branches, under its 92 % floor, because the new region load and size question had their failure paths untested. Now covered: a notice to the admins that fails never undoes the request, a list out of reach with something other than an error, the smallest of several held regions, a request filed meanwhile that is not announced twice, and sizeOf asked with HEAD, unknown without a usable size or answer, and refused for an address the list could not name. --- .../paddle-obstacles/geofabrik.client.test.ts | 28 +++++++++ .../paddle-region-load.service.test.ts | 57 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/server/tests/unit/nest/paddle-obstacles/geofabrik.client.test.ts b/server/tests/unit/nest/paddle-obstacles/geofabrik.client.test.ts index 90a4671d9..e68707442 100644 --- a/server/tests/unit/nest/paddle-obstacles/geofabrik.client.test.ts +++ b/server/tests/unit/nest/paddle-obstacles/geofabrik.client.test.ts @@ -230,3 +230,31 @@ describe('GeofabrikClient.download', () => { expect((await readFile(path)).toString()).toBe('pbf'); }); }); + +describe('GeofabrikClient.sizeOf', () => { + it('GEOF-013: asks with HEAD and a deadline, and answers the size Geofabrik announces', async () => { + fetchMock.mockResolvedValueOnce(new Response(null, { status: 200, headers: { 'content-length': '83886080' } })); + expect(await client.sizeOf(EXTRACT)).toBe(83_886_080); + expect(fetchMock.mock.calls[0]?.[1]).toMatchObject({ method: 'HEAD', redirect: 'manual' }); + expect(fetchMock.mock.calls[0]?.[1]?.signal).toBeInstanceOf(AbortSignal); + }); + + it('GEOF-014: no size, a size that is no number, or a failed answer reads as unknown', async () => { + fetchMock + .mockResolvedValueOnce(new Response(null, { status: 200 })) + .mockResolvedValueOnce(new Response(null, { status: 200, headers: { 'content-length': 'lots' } })) + .mockResolvedValueOnce(new Response(null, { status: 404, headers: { 'content-length': '42' } })); + expect(await client.sizeOf(EXTRACT)).toBeNull(); + expect(await client.sizeOf(EXTRACT)).toBeNull(); + expect(await client.sizeOf(EXTRACT)).toBeNull(); + }); + + it('GEOF-015: a size that cannot be asked reads as unknown; an address the list could not name is refused unasked', async () => { + fetchMock.mockRejectedValueOnce(new Error('socket hang up')).mockRejectedValueOnce('reset'); + expect(await client.sizeOf(EXTRACT)).toBeNull(); + expect(await client.sizeOf(EXTRACT)).toBeNull(); + fetchMock.mockClear(); + await expect(client.sizeOf('https://evil.example/x-latest.osm.pbf')).rejects.toThrow('Not a Geofabrik extract'); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/server/tests/unit/nest/paddle-obstacles/paddle-region-load.service.test.ts b/server/tests/unit/nest/paddle-obstacles/paddle-region-load.service.test.ts index 7eb05f575..af39d4bde 100644 --- a/server/tests/unit/nest/paddle-obstacles/paddle-region-load.service.test.ts +++ b/server/tests/unit/nest/paddle-obstacles/paddle-region-load.service.test.ts @@ -9,6 +9,7 @@ import { AppSettings } from '../../../../src/db/entities/AppSettings.entity'; import { PaddleRegionRequests } from '../../../../src/db/entities/PaddleRegionRequests.entity'; import { PaddleRegions } from '../../../../src/db/entities/PaddleRegions.entity'; import type { AppSettingsRepository } from '../../../../src/db/repositories/AppSettings.repository'; +import type { PaddleRegionRequestsRepository } from '../../../../src/db/repositories/PaddleRegionRequests.repository'; import { AppConfigModule } from '../../../../src/nest/app-config/app-config.module'; import { DataPathsService } from '../../../../src/nest/app-config/data-paths.service'; import { DemoModule } from '../../../../src/nest/common/demo.module'; @@ -279,4 +280,60 @@ describe('PaddleRegionLoadService', () => { await expect(load.updateSettings({ mode: 'off', max_mb: 1000 })).rejects.toThrow('database is locked'); expect(await load.settings()).toEqual({ mode: 'ask', max_mb: 800 }); }); + + it('PLOAD-014: a notice the admins cannot be sent never undoes the request, whatever it failed with', async () => { + await load.updateSettings({ mode: 'ask', max_mb: 500 }); + send.mockRejectedValueOnce(new Error('smtp down')).mockRejectedValueOnce('no channel'); + expect((await ask(paddler))[0]).toMatchObject({ state: 'requested', reason: 'ask' }); + expect((await ask(paddler, [MUERITZ]))[0]).toMatchObject({ state: 'requested', reason: 'ask' }); + const pending = await load.pending(); + expect(pending).toHaveLength(2); + expect(pending).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'oberpfalz' }), + expect.objectContaining({ id: 'mecklenburg-vorpommern' }), + ]), + ); + expect(log.logError).toHaveBeenCalledWith('[Paddle] telling the admins about oberpfalz: smtp down'); + expect(log.logError).toHaveBeenCalledWith('[Paddle] telling the admins about mecklenburg-vorpommern: no channel'); + }); + + it('PLOAD-015: the list out of reach with something other than an error still reads as unavailable', async () => { + regions.mockRejectedValue('offline'); + expect((await ask(paddler))[0]).toEqual({ state: 'unavailable', region: null, reason: 'catalog_unavailable' }); + expect(log.logError).toHaveBeenCalledWith('[Paddle] Geofabrik index: offline'); + }); + + it('PLOAD-016: of the regions TREK holds around a place the smallest answers, asked without a retry', async () => { + const queued = { status: 'queued' as const, min_lat: 47, max_lat: 51, max_lng: 14 }; + await insertRow(orm(), PaddleRegions, { ...queued, id: 'bayern', name: 'Bayern', min_lng: 9 }); + await insertRow(orm(), PaddleRegions, { + ...queued, + id: 'oberpfalz', + name: 'Oberpfalz', + min_lat: 48.9, + max_lat: 50, + min_lng: 11.5, + max_lng: 13, + }); + await insertRow(orm(), PaddleRegions, { ...queued, id: 'sueddeutschland', name: 'Sueddeutschland', min_lng: 7 }); + const answers = await load.load(paddler, { points: [REGENSBURG] }); + expect(answers).toEqual([ + expect.objectContaining({ state: 'loading', region: { id: 'oberpfalz', name: 'Oberpfalz' } }), + ]); + expect(sizeOf).not.toHaveBeenCalled(); + }); + + it('PLOAD-017: a request another ask filed in the meantime is answered, and not announced a second time', async () => { + await load.updateSettings({ mode: 'ask', max_mb: 500 }); + const requests = t?.get(getRepositoryToken(PaddleRegionRequests)); + if (!requests) throw new Error('No repository'); + vi.spyOn(requests, 'file').mockResolvedValueOnce(false); + expect((await ask(paddler))[0]).toEqual({ + state: 'requested', + region: { id: 'oberpfalz', name: 'Oberpfalz' }, + reason: 'ask', + }); + expect(send).not.toHaveBeenCalled(); + }); });