mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
6763648c45
SettingsRepository (getForUser/upsertForUser/getOne/deleteForUser) replaces the per-user settings table's raw SQL; SettingsService drops DatabaseService entirely. instance-api-keys.ts's readInstanceApiKey/writeInstanceApiKey/ resolveApiKey take an explicit AppSettingsRepository/UsersRepository instead of a raw connection. An earlier version resolved those two repositories itself from the active MikroORM request context, so none of its six callers needed to change. That worked in production and e2e (always inside a real request) but broke service construction in plain unit tests across every calling domain (addons, auth x2, maps, transit, unsplash - 185 failures on a full run), since none of them wrap a request context. Each of those six services now injects its own AppSettingsRepository/UsersRepository and passes them through explicitly, matching how every other converted repository in this migration is consumed; their own SQL is untouched (outside Plan 3a).
110 lines
4.6 KiB
TypeScript
110 lines
4.6 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 { 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) }));
|
|
|
|
import { MapsService } from '../../../src/nest/maps/maps.service';
|
|
import type { DatabaseService } from '../../../src/nest/database/database.service';
|
|
import type { PlacePhotoCacheService } from '../../../src/nest/place-photos/place-photo-cache.service';
|
|
import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository';
|
|
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
|
|
|
// 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';
|
|
const database = { get: vi.fn(() => undefined) } as unknown as DatabaseService;
|
|
return new MapsService(database, {} as PlacePhotoCacheService, noAppSettings, noUsers);
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|