mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
7acf7f5655
Admin > Settings > Google options gets a daily limit with today's count beside it. Every call through the Places and Routes clients is counted per UTC day; once the limit is reached the Google key reads as absent until the next day, so search, details and photos fall back to OpenStreetMap the way a keyless install does, and the admin sees that the day is used up. (#1582)
151 lines
7.6 KiB
TypeScript
151 lines
7.6 KiB
TypeScript
/**
|
|
* MAPS-GERS-001..007 — the details of a place that came from the index.
|
|
*
|
|
* The case that started this: L'Osteria Steinstrasse showed opening hours while
|
|
* it was being added and none on its card afterwards. The index is not the only
|
|
* source for a place it holds, and this is where the rest gets filled in.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { mockById } = vi.hoisted(() => ({
|
|
mockById: vi.fn(async (_gers: string): Promise<unknown> => null),
|
|
}));
|
|
vi.mock('../../../src/nest/maps/trek-places.client', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../../src/nest/maps/trek-places.client')>()),
|
|
trekPlacesById: mockById,
|
|
}));
|
|
|
|
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 { noGoogleQuota } from '../../helpers/google-quota';
|
|
|
|
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 as { osm: string } | null,
|
|
};
|
|
|
|
function make(osmTags: Record<string, string> | null) {
|
|
const database = { get: vi.fn(() => undefined) } as unknown as DatabaseService;
|
|
const svc = new MapsService(database, {} as PlacePhotoCacheService, noGoogleQuota);
|
|
vi.spyOn(svc, 'resolveOsmIdentity').mockResolvedValue(
|
|
osmTags ? { tags: osmTags, osmUrl: 'https://www.openstreetmap.org/node/1', matchedName: "L'Osteria" } : null,
|
|
);
|
|
return svc;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockById.mockReset();
|
|
mockById.mockResolvedValue({ ...PLACE });
|
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
});
|
|
|
|
describe('MapsService.getPlaceDetails for a gers: id', () => {
|
|
it('MAPS-GERS-001: prefers the hours OpenStreetMap has for the same building', async () => {
|
|
// OSM describes this exact object and gets corrected by people who walked
|
|
// past. A chain's website often carries one set of hours for every branch.
|
|
mockById.mockResolvedValue({ ...PLACE, hours: { osm: 'Mo-Su 12:00-22:00' } });
|
|
const out = await make({ opening_hours: 'Mo-Su 11:30-23:00' }).getPlaceDetails(1, 'gers:abc-123');
|
|
// Expanded to weekday lines, which is the shape the card reads.
|
|
expect(out.place?.opening_hours).toEqual(expect.arrayContaining(['Monday: 11:30-23:00']));
|
|
});
|
|
|
|
it('MAPS-GERS-002: falls back to the hours the operator publishes', async () => {
|
|
// OpenStreetMap has hours for 27.5 percent of gastronomy, measured across
|
|
// seven countries. This is what covers part of the rest.
|
|
mockById.mockResolvedValue({ ...PLACE, hours: { osm: 'Mo-Su 12:00-22:00' } });
|
|
const out = await make({ cuisine: 'italian' }).getPlaceDetails(1, 'gers:abc-123');
|
|
expect(out.place?.opening_hours).toEqual(expect.arrayContaining(['Monday: 12:00-22:00']));
|
|
});
|
|
|
|
it('MAPS-GERS-003: still answers when OpenStreetMap knows nothing about it', async () => {
|
|
mockById.mockResolvedValue({ ...PLACE, hours: { osm: 'Mo-Su 12:00-22:00' } });
|
|
const out = await make(null).getPlaceDetails(1, 'gers:abc-123');
|
|
expect(out.place?.name).toBe("L'Osteria");
|
|
// Same shape whether the hours came from OSM or from the operator's site.
|
|
expect(out.place?.opening_hours).toEqual(expect.arrayContaining(['Monday: 12:00-22:00']));
|
|
});
|
|
|
|
it('MAPS-GERS-004: a place nobody has hours for shows none, not an empty string', async () => {
|
|
const out = await make(null).getPlaceDetails(1, 'gers:abc-123');
|
|
expect(out.place?.opening_hours ?? null).toBeNull();
|
|
});
|
|
|
|
it('MAPS-GERS-005: the index wins on the fields the user actually picked', async () => {
|
|
const out = await make({ name: 'Etwas anderes', opening_hours: 'Mo-Fr 09:00-17:00' })
|
|
.getPlaceDetails(1, 'gers:abc-123');
|
|
expect(out.place).toMatchObject({ name: "L'Osteria", osm_id: 'gers:abc-123', source: 'trek-places' });
|
|
});
|
|
|
|
it('MAPS-GERS-006: an unknown id is null rather than a thrown request', async () => {
|
|
mockById.mockResolvedValue(null);
|
|
expect(await make(null).getPlaceDetails(1, 'gers:nope')).toEqual({ place: null });
|
|
});
|
|
|
|
it('MAPS-GERS-007: a failing lookup is a null, not a 500', async () => {
|
|
mockById.mockRejectedValue(new Error('places api down'));
|
|
expect(await make(null).getPlaceDetails(1, 'gers:abc-123')).toEqual({ place: null });
|
|
});
|
|
|
|
it('MAPS-GERS-008: Overpass being down costs the OSM half, not the whole place', async () => {
|
|
// The index has already answered by the time OSM is asked; the second lookup
|
|
// only adds what OSM knows about the same building. Letting its failure
|
|
// through would turn a working answer into an error for the one user whose
|
|
// details request happened to land while Overpass was unreachable.
|
|
const database = { get: vi.fn(() => undefined) } as unknown as DatabaseService;
|
|
const svc = new MapsService(database, {} as PlacePhotoCacheService, noGoogleQuota);
|
|
vi.spyOn(svc, 'resolveOsmIdentity').mockRejectedValue(new Error('overpass down'));
|
|
mockById.mockResolvedValue({ ...PLACE, hours: { osm: 'Mo-Su 12:00-22:00' } });
|
|
|
|
const out = await svc.getPlaceDetails(1, 'gers:abc-123');
|
|
|
|
expect(out.place).toMatchObject({
|
|
name: "L'Osteria",
|
|
osm_id: 'gers:abc-123',
|
|
source: 'trek-places',
|
|
website: 'https://losteria.net/',
|
|
});
|
|
// The index's own hours survive: they came with the record, not from OSM.
|
|
expect(out.place?.opening_hours).toBeTruthy();
|
|
});
|
|
|
|
// #2483: the place from the issue, whose index record carries its website
|
|
// without a scheme. Both halves of the merge hand it over completed.
|
|
it('MAPS-GERS-010: the merged website has its scheme, from the index or else from OSM', async () => {
|
|
const site = 'fr.wikipedia.org/wiki/Chapelle_Sainte-Barbe_du_Faouët';
|
|
mockById.mockResolvedValue({ ...PLACE, gers: 'ceba0e62-172b-4343-bb3b-78b915a18383', contact: { ...PLACE.contact, website: site } });
|
|
expect((await make(null).getPlaceDetails(1, 'gers:ceba0e62-172b-4343-bb3b-78b915a18383')).place?.website).toBe(`https://${site}`);
|
|
expect((await make({ website: 'www.example.fr' }).getPlaceDetails(1, 'gers:ceba0e62-172b-4343-bb3b-78b915a18383')).place?.website).toBe(`https://${site}`);
|
|
|
|
mockById.mockResolvedValue({ ...PLACE, contact: { ...PLACE.contact, website: 'javascript:alert(1)' } });
|
|
expect((await make({ website: 'www.example.fr' }).getPlaceDetails(1, 'gers:abc-123')).place?.website).toBe('https://www.example.fr');
|
|
expect((await make(null).getPlaceDetails(1, 'gers:abc-123')).place?.website).toBeNull();
|
|
});
|
|
|
|
it('MAPS-GERS-009: with the index switched off, opening a saved place asks nobody', async () => {
|
|
// TREK_PLACES_ENABLED=false is the operator's word that nothing leaves for
|
|
// the index. Search, autocomplete and the area download honoured it; the
|
|
// lookup by id did not, so every saved place that came from the index was
|
|
// still sent out, with the instance token, on every open.
|
|
const svc = make({ opening_hours: 'Mo-Su 11:30-23:00' });
|
|
vi.spyOn(svc, 'trekPlacesEnabled').mockReturnValue(false);
|
|
|
|
await expect(svc.getPlaceDetails(1, 'gers:abc-123')).resolves.toEqual({ place: null });
|
|
|
|
expect(mockById).not.toHaveBeenCalled();
|
|
expect(svc.resolveOsmIdentity).not.toHaveBeenCalled();
|
|
});
|
|
});
|