Files
TREK/server/tests/e2e/maps-photon.e2e.test.ts
T
Maurice 1e5314d847 maps, routing, i18n: settle what the checks found
- The lead is Photon's own row first; a row of its name inside the lead
  only stands in where Photon's row went as the same place. Rows without a
  line below the name are never folded as reading the same.
- A miss of Photon where it is the geocoder no longer asks it a second time
  on the way to an empty answer.
- The transport words of the identity match use a Unicode word boundary
  (straße, estación) and match 駅 anywhere.
- Opening an OSM place reads the coordinate it was placed at explicitly.
- The ferry spans of a Valhalla leg move to valhallaFerries.ts, and a day
  cut into pieces is asked with one options object.
- "Limit" on the allowance tiles is "Daily limit", which no locale left
  as an English copy.
- Tests follow the new behaviour: no Nominatim behind Photon, zoom 13,
  and the expectations of the merge and edit-distance cases.
2026-10-30 16:36:22 +01:00

184 lines
7.0 KiB
TypeScript

/**
* /api/maps search and reverse e2e with Photon as the geocoder of an
* installation with a key, Nominatim of one without: the real
* JwtAuthGuard, Zod pipe and response contracts over MapsModule on a migrated
* temp database, the TREK Places index switched off so the OpenStreetMap half
* answers alone.
*
* Replaced: the global fetch, routed by host to a fake Photon and a fake
* Nominatim, and the installation identity, a fake holding a key or none.
*/
import { servicesConfig } from '../../src/nest/app-config/tokens';
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
import { ZodValidationPipe } from '../../src/nest/common/zod-validation.pipe';
import { MapsModule } from '../../src/nest/maps/maps.module';
import { serviceIdentitySlot } from '../../src/nest/service-identity/service-identity.interface';
import { createSnapshotTestDb } from '../helpers/db-mock';
import { makeUser } from '../helpers/factories/users';
import { FakeServiceIdentity, gateFeature, photonAnswer } from '../helpers/photon';
import { createTestMikroOrmModule, createTestOrm, type TestOrm } from '../helpers/test-orm';
import { TestUnitOfWorkModule } from '../helpers/test-uow';
import { sessionCookie } from './harness';
import { Test } from '@nestjs/testing';
import cookieParser from 'cookie-parser';
import request from 'supertest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
const db = createSnapshotTestDb();
const USER = 1;
const SERVICES = {
mode: 'auto',
key: null,
hosts: ['places.liketrek.com', 'photon.liketrek.com', 'valhalla.liketrek.com'],
photonUrl: 'https://photon.liketrek.com',
};
const NOMINATIM_HIT = {
osm_type: 'node',
osm_id: '42',
class: 'amenity',
type: 'cafe',
name: 'Café Nominatim',
display_name: 'Café Nominatim, Pariser Platz, Berlin, Deutschland',
lat: '52.516',
lon: '13.378',
};
let orm: TestOrm;
let app: Awaited<ReturnType<typeof build>>;
let identity: FakeServiceIdentity;
let photonReply: () => Response;
const asked: URL[] = [];
async function build() {
const moduleRef = await Test.createTestingModule({
imports: [await TestUnitOfWorkModule.forRoot(db), await createTestMikroOrmModule(db), MapsModule],
})
.overrideProvider(servicesConfig.KEY)
.useValue(SERVICES)
.compile();
const nest = moduleRef.createNestApplication();
nest.use(cookieParser());
nest.useGlobalFilters(new TrekExceptionFilter());
nest.useGlobalPipes(new ZodValidationPipe());
await nest.init();
return nest;
}
const hostsAsked = () => asked.map((url) => url.hostname);
const as = (userId: number) => ({ Cookie: sessionCookie(userId) });
beforeAll(async () => {
orm = await createTestOrm(db);
await makeUser(orm, { id: USER, username: 'traveler', email: 'traveler@example.test' });
app = await build();
});
afterAll(async () => {
await app.close();
await orm.close();
});
beforeEach(() => {
identity = new FakeServiceIdentity();
serviceIdentitySlot.install(identity);
photonReply = () => photonAnswer([gateFeature()]);
asked.length = 0;
vi.stubEnv('TREK_PLACES_ENABLED', 'false');
vi.stubEnv('NOMINATIM_URL', '');
vi.stubGlobal(
'fetch',
vi.fn(async (input: string | URL) => {
const url = new URL(String(input));
asked.push(url);
if (url.hostname === 'photon.liketrek.com') return photonReply();
const body = url.pathname.endsWith('/reverse')
? { display_name: NOMINATIM_HIT.display_name, name: NOMINATIM_HIT.name, address: {} }
: [NOMINATIM_HIT];
return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
}),
);
});
afterEach(() => {
serviceIdentitySlot.release(identity);
vi.unstubAllGlobals();
vi.unstubAllEnvs();
});
describe('POST /api/maps/search', () => {
it('MAPS-PHOTON-E2E-001: answers from Photon with the key, in the reader language and around the bias', async () => {
const res = await request(app.getHttpServer())
.post('/api/maps/search?lang=de')
.set(as(USER))
.send({ query: 'Brandenburger Tor', locationBias: { lat: 52.5, lng: 13.4 } });
expect(res.status).toBe(200);
expect(res.body.source).toBe('openstreetmap');
expect(res.body.places[0]).toMatchObject({
osm_id: 'way:518071791',
name: 'Brandenburger Tor',
address: 'Brandenburger Tor, Pariser Platz, Mitte, Berlin, 10117, Deutschland',
lat: 52.5163,
lng: 13.3777,
wikidata: 'Q82425',
});
expect(hostsAsked()).toEqual(['photon.liketrek.com']);
expect(Object.fromEntries(asked[0].searchParams)).toMatchObject({ lang: 'de', lat: '52.5', lon: '13.4' });
expect(identity.observed).toEqual([{ host: 'photon.liketrek.com', status: 200 }]);
});
it('MAPS-PHOTON-E2E-002: a Photon that refuses is an empty answer, Nominatim is not asked in its place', async () => {
photonReply = () => photonAnswer([], 429);
const res = await request(app.getHttpServer()).post('/api/maps/search').set(as(USER)).send({ query: 'Café' });
expect(res.status).toBe(200);
expect(res.body.places).toEqual([]);
expect(hostsAsked()).toEqual(['photon.liketrek.com']);
});
it('MAPS-PHOTON-E2E-003: never asks Photon without a key', async () => {
identity.bearer = null;
const res = await request(app.getHttpServer()).post('/api/maps/search').set(as(USER)).send({ query: 'Café' });
expect(res.status).toBe(200);
expect(hostsAsked()).toEqual(['nominatim.openstreetmap.org']);
});
it("MAPS-PHOTON-E2E-004: keeps an operator's own Nominatim", async () => {
vi.stubEnv('NOMINATIM_URL', 'http://nominatim.internal:8080');
const res = await request(app.getHttpServer()).post('/api/maps/search').set(as(USER)).send({ query: 'Café' });
expect(res.status).toBe(200);
expect(hostsAsked()).toEqual(['nominatim.internal']);
});
});
describe('GET /api/maps/reverse', () => {
it('MAPS-PHOTON-E2E-005: names the point from Photon', async () => {
const res = await request(app.getHttpServer())
.get('/api/maps/reverse')
.set(as(USER))
.query({ lat: '52.5163', lng: '13.3777', lang: 'de' });
expect(res.status).toBe(200);
expect(res.body).toEqual({
name: 'Brandenburger Tor',
address: 'Brandenburger Tor, Pariser Platz, Mitte, Berlin, 10117, Deutschland',
});
expect(asked[0].pathname).toBe('/reverse');
});
it('MAPS-PHOTON-E2E-006: a Photon that is down names nothing, Nominatim is not asked in its place', async () => {
photonReply = () => photonAnswer([], 503);
const res = await request(app.getHttpServer())
.get('/api/maps/reverse')
.set(as(USER))
.query({ lat: '52.5163', lng: '13.3777' });
expect(res.body).toEqual({ name: null, address: null });
expect(hostsAsked()).toEqual(['photon.liketrek.com']);
});
it('MAPS-PHOTON-E2E-007: answers 401 without a session and asks nobody', async () => {
const res = await request(app.getHttpServer()).get('/api/maps/reverse').query({ lat: '52.5', lng: '13.4' });
expect(res.status).toBe(401);
expect(asked).toEqual([]);
});
});