mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
d98d597581
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.
77 lines
3.5 KiB
TypeScript
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);
|
|
});
|
|
});
|