mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
1e5314d847
- 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.
184 lines
7.0 KiB
TypeScript
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([]);
|
|
});
|
|
});
|