Files
TREK/server/tests/unit/nest/maps.area.test.ts
T
2026-10-06 21:50:59 +02:00

109 lines
4.5 KiB
TypeScript

/**
* MAPS-AREA-001..006 — the trip's area, handed to the client for offline use.
*
* The one maps call that is not driven by something a user just typed: it runs
* inside an offline sync, which is why every failure path here ends in null
* rather than an exception.
*/
import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository';
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import { MapsService } from '../../../src/nest/maps/maps.service';
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
import { noGoogleQuota } from '../../helpers/google-quota';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
const { mockArea } = vi.hoisted(() => ({
mockArea: vi.fn(async (): Promise<unknown> => ({ results: [], truncated: false, count: 0 })),
}));
vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => ({
...(await importOriginal<typeof import('../../../src/nest/maps/trek-places.client')>()),
trekPlacesArea: mockArea,
}));
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'test-secret', ENCRYPTION_KEY: '0'.repeat(64) }));
// keyedProvider/resolveMapsKey (maps.service.ts) go through instance-api-keys.ts
// on every call now — none of these cases configure a key, so the stubs just
// answer "unset" the way the fake database.get(() => undefined) already did.
const noAppSettings = { getValue: async () => null } as unknown as AppSettingsRepository;
const noUsers = { getApiKeyColumn: async () => null } as unknown as UsersRepository;
const BOX = { minLat: 54, minLng: 12, maxLat: 54.2, maxLng: 12.3 };
const PLACE = {
gers: 'abc-123',
name: "L'Osteria",
lat: 54.0879,
lng: 12.1408,
category: 'restaurant',
categoryPath: 'eat_and_drink>restaurant',
confidence: 1,
address: { freeform: 'Steinstrasse 9', locality: 'Rostock', postcode: '18055', region: null, country: 'DE' },
contact: { website: 'https://losteria.net/', phone: null, email: null, socials: null },
brand: null,
source: 'overture',
hours: null,
};
const ORIGINAL_TREK_PLACES = process.env.TREK_PLACES_ENABLED;
afterEach(() => {
if (ORIGINAL_TREK_PLACES === undefined) delete process.env.TREK_PLACES_ENABLED;
else process.env.TREK_PLACES_ENABLED = ORIGINAL_TREK_PLACES;
});
// The index is an operator switch, not an admin row, so switching it off means
// moving the env rather than stubbing a settings lookup. The service reads the
// flag per call, which is why the restore above belongs in afterEach.
function make(enabled = true) {
if (enabled) delete process.env.TREK_PLACES_ENABLED;
else process.env.TREK_PLACES_ENABLED = 'false';
return new MapsService({} as PlacePhotoCacheService, noAppSettings, noUsers, {} as never, {} as never, noGoogleQuota);
}
beforeEach(() => {
mockArea.mockReset();
mockArea.mockResolvedValue({ results: [PLACE], truncated: false, count: 1 });
vi.spyOn(console, 'warn').mockImplementation(() => {});
});
describe('MapsService.placesInArea', () => {
it('MAPS-AREA-001: hands back records in the shape the client already reads', async () => {
const out = await make().placesInArea(BOX, 500);
expect(mockArea).toHaveBeenCalledWith(BOX, 500);
expect(out?.results[0]).toMatchObject({
osm_id: 'gers:abc-123',
name: "L'Osteria",
source: 'trek-places',
});
});
it('MAPS-AREA-002: passes truncation through instead of smoothing it over', async () => {
// A client that believed it had the whole area would search offline and
// quietly miss places.
mockArea.mockResolvedValue({ results: [PLACE], truncated: true, count: 1 });
expect((await make().placesInArea(BOX))?.truncated).toBe(true);
});
it('MAPS-AREA-003: returns null when the operator switched the index off', async () => {
expect(await make(false).placesInArea(BOX)).toBeNull();
expect(mockArea).not.toHaveBeenCalled();
});
it('MAPS-AREA-004: a service failure is a null, not a thrown sync', async () => {
mockArea.mockRejectedValue(new Error('places api down'));
await expect(make().placesInArea(BOX)).resolves.toBeNull();
});
it('MAPS-AREA-005: an empty area is an answer, not a failure', async () => {
mockArea.mockResolvedValue({ results: [], truncated: false, count: 0 });
expect(await make().placesInArea(BOX)).toEqual({ results: [], truncated: false });
});
it('MAPS-AREA-006: leaves the limit to the caller when none is given', async () => {
await make().placesInArea(BOX);
expect(mockArea).toHaveBeenCalledWith(BOX, undefined);
});
});