Files
TREK/server/tests/unit/nest/tour-activities.controller.test.ts
T
Maurice d0cc6fc2f4 feat(tours): link the track a tour was actually done with, and show its own values (ACT-01)
A planned tour can now be given the track it was actually done with. In the
tour's detail, "Add actual track" opens a dialog that asks for the day the
tour was done on (or none) and a recording of the trip: an import nobody
planned over, or a new one through the existing GPX import, its preview
preset to the tour's type. What the choice measures is shown before anything
is written, and only "Link" writes. The linked track keeps its own name and
source; the detail shows its distance, heights, profile and, only where the
file carried time stamps, the time it took, marked as actual values next to
the untouched plan. A track can be changed or unlinked again.

Server: a tour_activities table points a tour's execution (a day assignment,
or none) at a recording of the same trip, and the tours GPX import keeps the
first and last time of a track whose every point carries one. The values are
read from the recording's own line on every read, so a changed recording
updates them while the plan's line, measures and revision stay as they were.
Every check runs inside the write: the recording must be an import of this
trip not planned over, documenting no other tour; the day must be one of this
tour; a refusal names its reason and changes nothing. PUT and DELETE
/api/trips/:tripId/tours/:placeId/activities answer the detail, which now
carries the actual tracks; the list names them per tour and per recording.
MCP: assign_tour_track and unassign_tour_track over the same service and
checks, get_tour_plan reads the values without the line.

A recording linked as a tour's actual track is no longer counted as a tour of
its own in the trip's tour summary, and the GPX preview imports a city walk
as one instead of as a hike.
2026-10-20 15:22:55 +02:00

73 lines
3.7 KiB
TypeScript

/**
* TourActivitiesController (ACT-01): route metadata and delegation. The
* checks themselves live in TourActivitiesService, which the MCP tools call
* too, so REST and MCP refuse the same assignments with the same codes; the
* service suite and the e2e suite hold those.
*/
import 'reflect-metadata';
import { AddonGuard } from '../../../src/nest/addons/addon.guard';
import { REQUIRE_ADDON } from '../../../src/nest/addons/require-addon.decorator';
import { JwtAuthGuard } from '../../../src/nest/auth-core/jwt-auth.guard';
import { TRIP_PERMISSION_KEY, TripAccessGuard } from '../../../src/nest/permissions/trip-access.guard';
import { TourActivitiesController } from '../../../src/nest/tours/tour-activities.controller';
import type { TourActivitiesService } from '../../../src/nest/tours/tour-activities.service';
import type { User } from '../../../src/types';
import { RequestMethod } from '@nestjs/common';
import { GUARDS_METADATA, HTTP_CODE_METADATA, METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants';
import { tourActivityAssignRequestSchema } from '@trek/shared';
import { describe, expect, it, vi } from 'vitest';
vi.mock('../../../src/nest/permissions/permissions.service', () => ({ PermissionsService: class {} }));
vi.mock('../../../src/nest/addons/addons.service', () => ({ AddonsService: class {} }));
vi.mock('../../../src/nest/tours/tour-activities.service', () => ({ TourActivitiesService: class {} }));
vi.mock('../../../src/nest/auth-core/jwt-verify', () => ({ extractToken: vi.fn(), verifyJwtAndLoadUser: vi.fn() }));
const detail = { tour: { place_id: 4 }, waypoints: [], activities: [] };
describe('TourActivitiesController', () => {
it('ACT-CTL-001: sits behind the addon, auth and trip guards, under the tour', () => {
expect(Reflect.getMetadata(PATH_METADATA, TourActivitiesController)).toBe(
'api/trips/:tripId/tours/:placeId/activities',
);
expect(Reflect.getMetadata(GUARDS_METADATA, TourActivitiesController)).toEqual([
AddonGuard,
JwtAuthGuard,
TripAccessGuard,
]);
expect(Reflect.getMetadata(REQUIRE_ADDON, TourActivitiesController)).toEqual({ addonId: 'tours', label: 'Tours' });
});
it('ACT-CTL-002: both routes change a tour, so both need place_edit and answer 200', () => {
const routes = [
['assign', RequestMethod.PUT, '/'],
['unassign', RequestMethod.DELETE, ':activityId'],
] as const;
for (const [handler, method, path] of routes) {
const fn = TourActivitiesController.prototype[handler];
expect(Reflect.getMetadata(METHOD_METADATA, fn)).toBe(method);
expect(Reflect.getMetadata(PATH_METADATA, fn)).toBe(path);
expect(Reflect.getMetadata(TRIP_PERMISSION_KEY, fn)).toBe('place_edit');
expect(Reflect.getMetadata(HTTP_CODE_METADATA, fn)).toBeUndefined();
}
});
it('ACT-CTL-003: PUT hands the validated body, the user and the socket id to the service', async () => {
const service = { assign: vi.fn().mockResolvedValue(detail) } as unknown as TourActivitiesService;
const body = tourActivityAssignRequestSchema.parse({ track_place_id: 9, assignment_id: 12 });
expect(await new TourActivitiesController(service).assign('7', '4', body, { id: 3 } as User, 'socket-1')).toBe(
detail,
);
expect(service.assign).toHaveBeenCalledWith('7', '4', body, 3, 'socket-1');
});
it('ACT-CTL-004: DELETE hands the assignment and the socket id to the service', async () => {
const service = { unassign: vi.fn().mockResolvedValue(detail) } as unknown as TourActivitiesService;
expect(await new TourActivitiesController(service).unassign('7', '4', 5, 'socket-2')).toBe(detail);
expect(service.unassign).toHaveBeenCalledWith('7', '4', 5, 'socket-2');
});
});