Files
TREK/server/tests/unit/nest/tours.service.test.ts
Maurice adcaca3c5f feat(tours): bike tours in the contract and on the server (BIK-01 to BIK-06, RTE-01)
A tour can be a bike tour with a routing profile (touring, road, gravel,
mtb), stored per tour by a new migration. The server grades the ways of a
mountain bike route from OpenStreetMap (mtb:scale) through one shared
Overpass client, offers CyclOSM behind its own admin switch, and exports a
bike tour's GPX with the activity of its profile. A hike's route choices
are now exactly the three the card names: steep climbs, steps and ferries.
2026-10-19 21:33:40 +02:00

624 lines
26 KiB
TypeScript

/**
* ToursService over the real migrated per-worker DB: the tours facet, its
* ordered control points and the owning place are written through the Tours,
* TourWaypoints and Places repositories inside one UnitOfWork transaction, and
* realtime events go out only after that transaction committed.
*
* PlacesService is a broadcast stub here; the GPX import path, which needs the
* real PlacesService, is covered by tours.gpx.atomic.test.ts.
*/
import { db as testDb } from '../../../src/db/database';
import { Places } from '../../../src/db/entities/Places.entity';
import { TourWaypoints } from '../../../src/db/entities/TourWaypoints.entity';
import { Tours } from '../../../src/db/entities/Tours.entity';
import type { PlacesService } from '../../../src/nest/places/places.service';
import { ToursService } from '../../../src/nest/tours/tours.service';
import { createPlace, createTrip, createUser } from '../../helpers/factories';
import { countRows, deleteRows, findRow, findRows, updateRows } from '../../helpers/factories/rows';
import { noPaddleChecks } from '../../helpers/fake-paddle-checks';
import { buildPlaceImportService } from '../../helpers/place-import';
import { resetTestDb } from '../../helpers/test-db';
import { createTestPlacesRepo, createTestUnitOfWork, sharedTestOrm } from '../../helpers/test-uow';
import {
createTestToursRepo,
createTestTourTypesRepo,
createTestTourWaypointsRepo,
createTour,
} from '../../helpers/tours-repos';
import type { EntityClass } from '@mikro-orm/core';
import { BadRequestException, Logger, NotFoundException } from '@nestjs/common';
import { tourCreateRequestSchema, type TourCreateRequest } from '@trek/shared';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../../../src/db/database', async () => {
const { createSnapshotTestDb, buildDbMock } = await import('../../helpers/db-mock');
return buildDbMock(createSnapshotTestDb());
});
const request: TourCreateRequest = {
name: 'Ridge walk',
description: 'A quiet ridge above the lake.',
website: 'https://www.komoot.com/tour/42',
tour_type: 'hike',
route_geometry: [
[48, 11, 600],
[48.01, 11.02, 650],
[48.02, 11.04, 630],
],
waypoints: [
{ lat: 48, lng: 11, role: 'start', sequence: 0 },
{ lat: 48.02, lng: 11.04, role: 'end', sequence: 1 },
],
max_hiking_difficulty: 2,
duration_seconds: 3600,
planned_duration_minutes: 90,
break_additional_minutes: 25,
};
/** Two waypoints on the same sequence: the UNIQUE(place_id, sequence) index rejects the second one. */
const duplicateSequence = {
...request,
waypoints: [request.waypoints[0], { ...request.waypoints[1], sequence: 0 }],
} as TourCreateRequest;
const broadcast = vi.fn();
let service: ToursService;
let tripId: string;
let otherTripId: string;
const count = async <T extends object>(entity: EntityClass<T>) => countRows(await sharedTestOrm(testDb), entity);
/** The first stored place and tour, the only ones most cases leave behind. */
const firstPlace = async () => (await findRows(await sharedTestOrm(testDb), Places, {}, { id: 'asc' }))[0];
const firstTour = async () => (await findRows(await sharedTestOrm(testDb), Tours, {}, { place: 'asc' }))[0];
const placeById = async (id: number) => findRow(await sharedTestOrm(testDb), Places, { id });
beforeAll(async () => {
service = new ToursService(
await createTestUnitOfWork(testDb),
{ broadcast } as unknown as PlacesService,
await createTestToursRepo(testDb),
await createTestTourTypesRepo(testDb),
await createTestTourWaypointsRepo(testDb),
await createTestPlacesRepo(testDb),
buildPlaceImportService(),
noPaddleChecks(),
);
});
beforeEach(async () => {
resetTestDb(testDb);
(await sharedTestOrm(testDb)).clear();
broadcast.mockReset();
const { user } = createUser(testDb);
tripId = String(createTrip(testDb, user.id).id);
otherTripId = String(createTrip(testDb, user.id).id);
});
afterEach(() => {
vi.restoreAllMocks();
});
afterAll(() => {
testDb.close();
});
describe('ToursService planner creation', () => {
it.each([
'http://example.org/route',
'javascript:alert(1)',
'data:text/plain,route',
'file:///route.gpx',
'https://user:secret@example.org/route',
])('rejects invalid informational website %s before creating rows', async (website) => {
const parsed = tourCreateRequestSchema.safeParse({ ...request, website });
expect(parsed.success).toBe(false);
await expect(service.createTour(tripId, { ...request, website } as TourCreateRequest)).rejects.toThrow();
expect(await count(Places)).toBe(0);
expect(await count(Tours)).toBe(0);
});
it('safely reports malformed website input through schema safeParse', () => {
expect(tourCreateRequestSchema.safeParse({ ...request, website: 'not a url' }).success).toBe(false);
});
it('TOURS-SVC-001: atomically stores full geometry, derived metrics, and ordered control points', async () => {
const writeStates: boolean[] = [];
broadcast.mockImplementation(() => {
writeStates.push(testDb.inTransaction);
});
const result = await service.createTour(tripId, request, 'socket-1');
const placeId = result.tour.place_id;
const orm = await sharedTestOrm(testDb);
const place = await findRow(orm, Places, { id: placeId });
const tour = await findRow(orm, Tours, { place: placeId });
const points = (await findRows(orm, TourWaypoints, { place: placeId }, { sequence: 'asc' })).map((w) => ({
role: w.role,
sequence: w.sequence,
}));
if (!place || !tour) throw new Error('createTour should have stored the place and its tour');
expect(place).toMatchObject({
trip_id: Number(tripId),
name: 'Ridge walk',
lat: 48,
lng: 11,
transport_mode: 'walking',
duration_minutes: 60,
});
expect(place).toMatchObject({
description: 'A quiet ridge above the lake.',
website: 'https://www.komoot.com/tour/42',
});
expect(JSON.parse(String(place.route_geometry))).toEqual(request.route_geometry);
expect(tour).toMatchObject({
tour_type: 'hike',
match_confidence: 1,
max_hiking_difficulty: 2,
planned_duration_minutes: 90,
break_additional_minutes: 25,
});
expect(tour.distance).toBeGreaterThan(0);
expect(tour.elevation_gain).toBe(50);
expect(tour.elevation_loss).toBe(20);
expect(tour.duration).toBe(60);
expect(result.tour.duration).toBe(60);
expect(result.tour.planned_duration_minutes).toBe(90);
expect(result.tour.break_additional_minutes).toBe(25);
expect(points).toEqual([
{ role: 'start', sequence: 0 },
{ role: 'end', sequence: 1 },
]);
expect(result.tour).toMatchObject({ name: 'Ridge walk', planned: false, has_waypoints: true, caution: false });
expect(result.tour).toMatchObject({
description: 'A quiet ridge above the lake.',
website: 'https://www.komoot.com/tour/42',
});
expect(result.waypoints).toEqual(request.waypoints);
expect(broadcast).toHaveBeenNthCalledWith(1, tripId, 'tours:changed', { placeIds: [placeId] }, 'socket-1');
expect(broadcast).toHaveBeenNthCalledWith(
2,
tripId,
'place:created',
{ place: expect.objectContaining({ id: placeId }) },
'socket-1',
);
expect(broadcast).toHaveBeenCalledTimes(2);
// Both events go out after the commit, never from inside the write.
expect(writeStates).toEqual([false, false]);
});
it('TOURS-SVC-002: rolls the owning place and facet back when a waypoint insert fails', async () => {
await expect(service.createTour(tripId, duplicateSequence)).rejects.toThrow();
expect(await count(Places)).toBe(0);
expect(await count(Tours)).toBe(0);
expect(await count(TourWaypoints)).toBe(0);
expect(broadcast).not.toHaveBeenCalled();
});
it('TOURS-SVC-003: validates a fractional routed duration before the atomic create', async () => {
const validated = tourCreateRequestSchema.parse({ ...request, duration_seconds: 3599.5 });
const result = await service.createTour(tripId, validated);
expect(await count(Places)).toBe(1);
expect(await count(Tours)).toBe(1);
expect(await count(TourWaypoints)).toBe(2);
expect((await firstTour()).duration).toBe(60);
expect(result.tour).toMatchObject({ name: 'Ridge walk', planned: false });
});
it('TOURS-SVC-003b: stores no duration when the client sends none', async () => {
const { duration_seconds: _omitted, ...withoutDuration } = request;
const result = await service.createTour(tripId, withoutDuration);
expect(result.tour.duration).toBeNull();
});
it('RS-02: leaves planned total empty when omitted and preserves the calculated duration', async () => {
const { planned_duration_minutes: _omitted, ...withoutPlannedDuration } = request;
const result = await service.createTour(tripId, withoutPlannedDuration);
expect(result.tour.duration).toBe(60);
expect(result.tour.planned_duration_minutes).toBeNull();
});
it.each([-1, 1.5, 1441])('RS-02: rejects invalid breaks %s before writing Tour rows', async (breakMinutes) => {
await expect(
service.createTour(tripId, { ...request, break_additional_minutes: breakMinutes } as TourCreateRequest),
).rejects.toThrow();
expect(await count(Places)).toBe(0);
expect(await count(Tours)).toBe(0);
});
it('RS-02: rejects overlong automatic totals but permits a bounded manual override', async () => {
const tooLong = {
...request,
duration_seconds: 1430 * 60,
break_additional_minutes: 11,
planned_duration_minutes: null,
};
await expect(service.createTour(tripId, tooLong)).rejects.toThrow();
const overridden = await service.createTour(tripId, { ...tooLong, planned_duration_minutes: 1200 });
expect(overridden.tour.duration).toBe(1430);
expect(overridden.tour.planned_duration_minutes).toBe(1200);
expect(overridden.tour.break_additional_minutes).toBe(11);
});
it('TOURS-SVC-010: a type the catalogue holds but has not enabled is a 400 without a write', async () => {
// The contract only lets `hike` through today; this pins what happens the
// day it lets more keys in before the planner offers them.
const bike = { ...request, tour_type: 'run' } as unknown as typeof request;
await expect(service.createTour(tripId, bike)).rejects.toBeInstanceOf(BadRequestException);
expect(await count(Places)).toBe(0);
expect(await count(Tours)).toBe(0);
expect(broadcast).not.toHaveBeenCalled();
});
it('TOURS-SVC-011: an edit to a disabled type is a 400 and leaves the saved tour alone', async () => {
const created = await service.createTour(tripId, request);
broadcast.mockReset();
const bike = { ...request, name: 'Renamed', tour_type: 'run' } as unknown as typeof request;
await expect(service.updateTour(tripId, String(created.tour.place_id), bike)).rejects.toBeInstanceOf(
BadRequestException,
);
const storedTour = await firstTour();
expect({ tour_type: storedTour.tour_type, name: (await placeById(Number(storedTour.place_id)))?.name }).toEqual({
tour_type: 'hike',
name: 'Ridge walk',
});
expect(broadcast).not.toHaveBeenCalled();
});
it('keeps a committed create successful when Tours invalidation publication fails', async () => {
const warn = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => {});
broadcast.mockImplementation((_tripId: string, event: string) => {
if (event === 'tours:changed') throw new Error('transport unavailable');
});
const result = await service.createTour(tripId, request);
expect(result.tour.name).toBe('Ridge walk');
expect(await count(Places)).toBe(1);
expect(await count(Tours)).toBe(1);
expect(broadcast).toHaveBeenCalledTimes(2);
expect(warn).toHaveBeenCalledOnce();
});
it('keeps a committed create successful when the place notification fails', async () => {
const warn = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => {});
broadcast.mockImplementation((_tripId: string, event: string) => {
if (event === 'place:created') throw new Error('transport unavailable');
});
await expect(service.createTour(tripId, request)).resolves.toMatchObject({ tour: { name: 'Ridge walk' } });
expect(warn).toHaveBeenCalledOnce();
});
});
describe('ToursService reads', () => {
it('TOURS-SVC-004: loads only a trip-owned tour with its ordered control points', async () => {
const created = await service.createTour(tripId, request);
const placeId = String(created.tour.place_id);
expect(await service.getTour(tripId, placeId)).toEqual(created);
await expect(service.getTour(otherTripId, placeId)).rejects.toThrow('Tour not found');
await deleteRows(await sharedTestOrm(testDb), TourWaypoints, { place: created.tour.place_id });
const legacyGpx = await service.getTour(tripId, placeId);
expect(legacyGpx.tour.has_waypoints).toBe(false);
expect(legacyGpx.waypoints).toEqual([
{ lat: 48, lng: 11, role: 'start', sequence: 0 },
{ lat: 48.02, lng: 11.04, role: 'end', sequence: 1 },
]);
// The fallback is read-only: nothing was backfilled.
expect(await count(TourWaypoints)).toBe(0);
});
it('TOURS-SVC-004b: a plain place, a malformed id and a geometry-less tour', async () => {
const plain = createPlace(testDb, Number(tripId), { name: 'Not a tour' });
await expect(service.getTour(tripId, String(plain.id))).rejects.toBeInstanceOf(NotFoundException);
await expect(service.getTour(tripId, 'abc')).rejects.toThrow('Tour not found');
await expect(service.getTour('abc', String(plain.id))).rejects.toThrow('Tour not found');
const bare = createPlace(testDb, Number(tripId), { name: 'Bare import' });
createTour(testDb, bare.id);
const detail = await service.getTour(tripId, String(bare.id));
expect(detail.tour).toMatchObject({ place_id: bare.id, has_waypoints: false });
expect(detail.waypoints).toEqual([]);
});
it("TOURS-SVC-007: lists only this trip's tours, newest first, with the caution flag", async () => {
const older = createPlace(testDb, Number(tripId), { name: 'Older' });
const newer = createPlace(testDb, Number(tripId), { name: 'Newer' });
const foreign = createPlace(testDb, Number(otherTripId), { name: 'Foreign' });
createPlace(testDb, Number(tripId), { name: 'Plain place' });
createTour(testDb, older.id, { created_at: '2026-01-01 10:00:00', match_confidence: 0.3 });
createTour(testDb, newer.id, { created_at: '2026-02-01 10:00:00', match_confidence: 1 });
createTour(testDb, foreign.id);
const list = await service.listTours(tripId);
expect(list.map((t) => t.name)).toEqual(['Newer', 'Older']);
expect(list[0]).toMatchObject({ caution: false, planned: false, has_waypoints: false, tour_type: 'hike' });
expect(list[1]).toMatchObject({ caution: true, match_confidence: 0.3 });
expect(await service.listTours('not-a-number')).toEqual([]);
});
});
describe('ToursService updates', () => {
const update: TourCreateRequest = {
...request,
name: 'Updated ridge walk',
description: 'Updated description',
website: 'https://alltrails.com/trail/42',
route_geometry: [
[49, 12, 700],
[49.02, 12.04, 760],
],
waypoints: [
{ lat: 49, lng: 12, role: 'start', sequence: 0 },
{ lat: 49.01, lng: 12.02, role: 'via', sequence: 1 },
{ lat: 49.02, lng: 12.04, role: 'end', sequence: 2 },
],
duration_seconds: 2700.5,
planned_duration_minutes: 110,
break_additional_minutes: 40,
};
it('TOURS-SVC-005: atomically updates route data and replaces persisted control points', async () => {
const created = await service.createTour(tripId, request);
await updateRows(await sharedTestOrm(testDb), Places, { id: created.tour.place_id }, { duration_minutes: 35 });
broadcast.mockReset();
const writeStates: boolean[] = [];
broadcast.mockImplementation(() => {
writeStates.push(testDb.inTransaction);
});
const result = await service.updateTour(tripId, String(created.tour.place_id), update, 'socket-2');
expect(result.tour).toMatchObject({ place_id: created.tour.place_id, name: 'Updated ridge walk' });
expect(result.waypoints).toEqual(update.waypoints);
const storedPlace = await firstPlace();
expect({
name: storedPlace.name,
description: storedPlace.description,
website: storedPlace.website,
lat: storedPlace.lat,
lng: storedPlace.lng,
transport_mode: storedPlace.transport_mode,
duration_minutes: storedPlace.duration_minutes,
}).toEqual({
name: 'Updated ridge walk',
description: 'Updated description',
website: 'https://alltrails.com/trail/42',
lat: 49,
lng: 12,
transport_mode: 'walking',
duration_minutes: 35,
});
const storedTour = await firstTour();
expect({
duration: storedTour.duration,
planned_duration_minutes: storedTour.planned_duration_minutes,
elevation_gain: storedTour.elevation_gain,
elevation_loss: storedTour.elevation_loss,
}).toEqual({
duration: 45,
planned_duration_minutes: 110,
elevation_gain: 60,
elevation_loss: 0,
});
expect(broadcast).toHaveBeenNthCalledWith(
1,
tripId,
'tours:changed',
{ placeIds: [created.tour.place_id] },
'socket-2',
);
expect(broadcast).toHaveBeenNthCalledWith(
2,
tripId,
'place:updated',
expect.objectContaining({ place: expect.objectContaining({ id: created.tour.place_id }) }),
'socket-2',
);
expect(writeStates).toEqual([false, false]);
});
it('preserves description and website when an older Tour update omits optional metadata', async () => {
const created = await service.createTour(tripId, request);
const {
description: _description,
website: _website,
planned_duration_minutes: _planned,
break_additional_minutes: _breaks,
...legacyUpdate
} = update;
const result = await service.updateTour(tripId, String(created.tour.place_id), legacyUpdate);
expect(result.tour).toMatchObject({ description: request.description, website: request.website });
expect(result.tour.planned_duration_minutes).toBe(request.planned_duration_minutes);
expect(result.tour.break_additional_minutes).toBe(request.break_additional_minutes);
expect(await findRow(await sharedTestOrm(testDb), Places, { id: created.tour.place_id })).toMatchObject({
description: request.description,
website: request.website,
});
});
it('clears planned total duration only when an update explicitly sends null', async () => {
const created = await service.createTour(tripId, request);
const result = await service.updateTour(tripId, String(created.tour.place_id), {
...update,
planned_duration_minutes: null,
});
expect(result.tour.duration).toBe(45);
expect(result.tour.planned_duration_minutes).toBeNull();
});
it('RS-01: updates informational fields without overwriting Place or Wanderer provenance', async () => {
const created = await service.createTour(tripId, request);
await updateRows(
await sharedTestOrm(testDb),
Places,
{ id: created.tour.place_id },
{ source: 'gpx-import:source-42' },
);
await updateRows(
await sharedTestOrm(testDb),
Tours,
{ place: created.tour.place_id },
{ wanderer_ref: 'wanderer:trail-42' },
);
const metadataUpdate = { ...update, description: 'Updated details', website: 'https://outdooractive.com/route/42' };
await service.updateTour(tripId, String(created.tour.place_id), metadataUpdate);
expect(await findRow(await sharedTestOrm(testDb), Places, { id: created.tour.place_id })).toMatchObject({
description: 'Updated details',
website: 'https://outdooractive.com/route/42',
source: 'gpx-import:source-42',
});
expect(await findRow(await sharedTestOrm(testDb), Tours, { place: created.tour.place_id })).toMatchObject({
wanderer_ref: 'wanderer:trail-42',
});
});
it('TOURS-SVC-006: rolls an invalid waypoint replacement back to the previous saved tour', async () => {
const created = await service.createTour(tripId, request);
broadcast.mockReset();
await expect(
service.updateTour(tripId, String(created.tour.place_id), { ...duplicateSequence, name: 'Must roll back' }),
).rejects.toThrow();
expect((await firstPlace()).name).toBe('Ridge walk');
expect((await service.getTour(tripId, String(created.tour.place_id))).waypoints).toEqual(request.waypoints);
expect(broadcast).not.toHaveBeenCalled();
});
it('TOURS-SVC-008: a tour of another trip is a 404 and stays untouched', async () => {
const created = await service.createTour(otherTripId, request);
broadcast.mockReset();
await expect(service.updateTour(tripId, String(created.tour.place_id), update, 'socket')).rejects.toThrow(
'Tour not found',
);
const untouched = await placeById(Number(created.tour.place_id));
expect({ trip_id: untouched?.trip_id, name: untouched?.name }).toEqual({
trip_id: Number(otherTripId),
name: 'Ridge walk',
});
expect((await service.getTour(otherTripId, String(created.tour.place_id))).waypoints).toEqual(request.waypoints);
expect(broadcast).not.toHaveBeenCalled();
});
it('TOURS-SVC-009: a plain place or a malformed id is a 404 without a write', async () => {
const plain = createPlace(testDb, Number(tripId), { name: 'Plain' });
await expect(service.updateTour(tripId, String(plain.id), update)).rejects.toBeInstanceOf(NotFoundException);
await expect(service.updateTour(tripId, 'abc', update)).rejects.toThrow('Tour not found');
await expect(service.updateTour('abc', String(plain.id), update)).rejects.toThrow('Tour not found');
expect((await placeById(plain.id))?.name).toBe('Plain');
expect(await count(Tours)).toBe(0);
expect(broadcast).not.toHaveBeenCalled();
});
});
describe('ToursService restoring a recorded track (PR1-05B)', () => {
const recorded = [
[47.45, 11.1],
[47.46, 11.11],
[47.47, 11.12],
];
/** A tour planned over in place: waypoints of its own, the import's line kept beside them. */
async function plannedOverImport(
fields: Partial<Pick<Tours, 'wanderer_ref' | 'difficulty' | 'recorded_route_geometry'>>,
) {
const created = await service.createTour(tripId, request);
const placeId = created.tour.place_id;
await updateRows(
await sharedTestOrm(testDb),
Tours,
{ place: placeId },
{
recorded_route_geometry: JSON.stringify(recorded),
...fields,
},
);
(await sharedTestOrm(testDb)).clear();
broadcast.mockReset();
return String(placeId);
}
it('PR1-05B-SVC-001: a GPX import gets its line back without heights or grade, and loses its waypoints', async () => {
const placeId = await plannedOverImport({});
broadcast.mockImplementation((_tripId: string, event: string) => {
if (event === 'place:updated') throw new Error('socket closed');
});
const warn = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
const restored = await service.restoreRecordedTrack(tripId, placeId, 'socket');
// Its stored waypoints are gone; the detail names the two ends from the line, as for any import.
expect(await count(TourWaypoints)).toBe(0);
expect(restored.waypoints.map((point) => point.role)).toEqual(['start', 'end']);
expect(restored.tour).toMatchObject({ max_hiking_difficulty: null, has_recorded_track: false });
const tour = await findRow(await sharedTestOrm(testDb), Tours, { place: Number(placeId) });
expect(tour).toMatchObject({ match_confidence: 0.3, recorded_route_geometry: null });
expect(JSON.parse(String((await placeById(Number(placeId)))?.route_geometry))).toEqual(recorded);
expect(warn).toHaveBeenCalled();
});
it('PR1-05B-SVC-002: a wanderer import gets the grade its source gave, on the SAC scale', async () => {
const placeId = await plannedOverImport({
wanderer_ref: 'https://w.example#trail1',
difficulty: 'difficult',
recorded_route_geometry: JSON.stringify(recorded.map(([lat, lng], i) => [lat, lng, 700 + i * 50])),
});
const restored = await service.restoreRecordedTrack(tripId, placeId);
expect(restored.tour).toMatchObject({ max_hiking_difficulty: 3 });
expect(await findRow(await sharedTestOrm(testDb), Tours, { place: Number(placeId) })).toMatchObject({
match_confidence: 1,
});
expect(broadcast).toHaveBeenCalledWith(tripId, 'tours:changed', { placeIds: [Number(placeId)] }, undefined);
});
it('PR1-05B-SVC-003: no kept track, or an empty one, is a 409 that changes nothing', async () => {
const created = await service.createTour(tripId, request);
const placeId = String(created.tour.place_id);
const refusal = { status: 409, details: { code: 'no_recorded_track' } };
await expect(service.restoreRecordedTrack(tripId, placeId)).rejects.toMatchObject(refusal);
await updateRows(
await sharedTestOrm(testDb),
Tours,
{ place: created.tour.place_id },
{ recorded_route_geometry: '[]' },
);
(await sharedTestOrm(testDb)).clear();
await expect(service.restoreRecordedTrack(tripId, placeId)).rejects.toMatchObject(refusal);
expect((await service.getTour(tripId, placeId)).waypoints).toEqual(request.waypoints);
});
it('PR1-05B-SVC-004: a plain place, a tour of another trip or a malformed id is a 404', async () => {
const plain = createPlace(testDb, Number(tripId), { name: 'Plain' });
const foreign = await service.createTour(otherTripId, request);
await expect(service.restoreRecordedTrack(tripId, String(plain.id))).rejects.toBeInstanceOf(NotFoundException);
await expect(service.restoreRecordedTrack(tripId, String(foreign.tour.place_id))).rejects.toBeInstanceOf(
NotFoundException,
);
await expect(service.restoreRecordedTrack('abc', '1')).rejects.toBeInstanceOf(NotFoundException);
await expect(service.restoreRecordedTrack(tripId, 'abc')).rejects.toBeInstanceOf(NotFoundException);
});
});