Files
TREK/server/tests/unit/nest/place-enrichment.controller.test.ts
T
Maurice d98d597581 Make the detail column useful for places no encyclopaedia describes
A restaurant got nothing: no wiki article, and a coordinate search around it
returns whatever was photographed nearby rather than the place itself. Three
changes, all on free sources:

OpenStreetMap facts. A tagged restaurant carries its cuisine, its opening hours,
a link to its menu, outdoor seating, step-free access and diet tags. Those
arrive with the details lookup the dialog already makes, so showing them as
chips costs nothing and is the single biggest gain for exactly the places that
were empty before. Only positive facts are listed - "no outdoor seating" is
noise.

Commons categories. Where a place has a wikimedia_commons tag, its category is
pictures OF it; geosearch falls back in when the category is empty.

Wikivoyage before Wikipedia. Same API, same licence, but it describes a place
for someone about to visit rather than opening with area in square kilometres.
Two sentences instead of three, because this sits beside a form.

The column also follows the section-heading idiom Vacay and the dashboard share
(uppercase overline, wide tracking), its header matches the Saved places panel
beside it, and a lead picture with a row underneath replaces the two-column
grid that left a hole on every odd count. Credits wrap over two lines instead
of truncating - a cut-off name credits nobody.

Saved places stops after ten with a Show more button; search and filters still
run over everything, not just the visible page.

Form spacing is one rhythm (12px between groups, 8px inside, 4px label to
field) and Notes matches Description in height. The OSM notice fits on one line
and no longer claims there are no photos, which stopped being true when the
column started fetching them from Wikimedia. The clipboard hint is gone; paste
still works.
2026-08-10 12:24:15 +02:00

77 lines
3.5 KiB
TypeScript

/**
* Unit tests for PlaceEnrichmentController — ENRICH-040 through ENRICH-046.
* Service and rate limiter are stubbed; this covers the rate-limit gate, the
* pass-through and the deliberate swallowing of provider failures.
*/
import { describe, it, expect, vi } from 'vitest';
import { HttpException } from '@nestjs/common';
import type { Request } from 'express';
import { PlaceEnrichmentController } from '../../../src/nest/place-enrichment/place-enrichment.controller';
import type { PlaceEnrichmentService } from '../../../src/nest/place-enrichment/place-enrichment.service';
import { RateLimitService } from '../../../src/nest/common/rate-limit.service';
import type { User } from '../../../src/types';
const USER = { id: 7 } as User;
const REQ = {} as Request;
const BODY = { lat: 50.9, lng: 6.96, name: 'Museum Ludwig', placeId: 'ChIJmuseum' };
const RESULT = { photos: [], description: null, facts: [] };
function make(over: Partial<PlaceEnrichmentService> = {}, rl = new RateLimitService()) {
const service = { enrich: vi.fn(async () => RESULT), ...over } as unknown as PlaceEnrichmentService;
return { controller: new PlaceEnrichmentController(service, rl), service, rl };
}
describe('PlaceEnrichmentController', () => {
it('ENRICH-040: forwards the validated body with the signed-in user id', async () => {
const { controller, service } = make();
await expect(controller.enrich(USER, REQ, BODY)).resolves.toEqual(RESULT);
expect(service.enrich).toHaveBeenCalledWith(7, BODY);
});
it('ENRICH-041: answers 429 once a user passes sixty calls a minute', async () => {
const { controller, service } = make();
for (let i = 0; i < 60; i++) await controller.enrich(USER, REQ, BODY);
await expect(controller.enrich(USER, REQ, BODY)).rejects.toBeInstanceOf(HttpException);
expect(service.enrich).toHaveBeenCalledTimes(60);
});
it('ENRICH-042: carries the 429 status and message', async () => {
const rl = new RateLimitService();
const { controller } = make({}, rl);
vi.spyOn(rl, 'check').mockReturnValue(false);
const err = await controller.enrich(USER, REQ, BODY).catch((e: HttpException) => e);
expect((err as HttpException).getStatus()).toBe(429);
expect((err as HttpException).getResponse()).toEqual({ error: 'Too many requests' });
});
it('ENRICH-043: buckets per user, so one busy account cannot lock out another', async () => {
const { controller } = make();
for (let i = 0; i < 60; i++) await controller.enrich(USER, REQ, BODY);
await expect(controller.enrich(USER, REQ, BODY)).rejects.toBeInstanceOf(HttpException);
await expect(controller.enrich({ id: 8 } as User, REQ, BODY)).resolves.toEqual(RESULT);
});
it('ENRICH-044: turns a provider failure into an empty result, not a 500', async () => {
// The column is an aid, not a step — adding a place must not break because
// Wikimedia is down.
const { controller } = make({ enrich: vi.fn(async () => { throw new Error('commons down'); }) });
const err = vi.spyOn(console, 'error').mockImplementation(() => {});
await expect(controller.enrich(USER, REQ, BODY)).resolves.toEqual({ photos: [], description: null, facts: [] });
expect(err).toHaveBeenCalled();
err.mockRestore();
});
it('ENRICH-045: passes the disabled envelope through untouched', async () => {
const disabled = { photos: [], description: null, facts: [], disabled: true };
const { controller } = make({ enrich: vi.fn(async () => disabled) });
await expect(controller.enrich(USER, REQ, BODY)).resolves.toEqual(disabled);
});
});