Files
TREK/server/tests/unit/nest/journey-recordings.controller.test.ts
T
Maurice 94c369b913 feat(journey): prepare the recorded days of a journey's trips in the background, switched on per journey, with each day's state
Journey Settings get "Prepare recorded days automatically" under GPX Tracks,
off by default and set by the owner. With it on, the Dawarich background
check also reads the linked trips of the journey for the owner and every
editor with a connection of their own, within the same 400 day reach and
windows as the trip sync, and only for new and changed stays: a trip the
person travels on is not asked twice, and the stays of a trip they are not
on are filed under no trip. Nothing is written into the journey; the stays
wait at their day, and a stay taken over or left out is never offered again.

Under the switch the caller sees when their stays were last fetched, or why
not (no connection, no dated trip, background check off, the reason of the
last failure beside the state still shown), with a "Fetch now" that the
switch also runs when it is turned on. A day of the trips that is not final
yet carries a dot on its take-over button, on the phone in the stay sheet,
and the tooltip says whether it is still running, was fetched before it
ended, was not fetched yet or shows an older state after a failed refresh.
The day review says so for today too.

Server: migration 046180 adds journeys.auto_prepare_recordings and
journey_recording_prep (the last attempt, last success and last error per
journey and person). JourneyRecordingPrepService in the journey domain lists
the prepared trips and keeps the status; DawarichSyncService walks those
trips in the same loop as the person's own and records each journey's
outcome. GET /api/journeys/:id/recordings/prep answers the caller's status
under a @trek/shared contract, the MCP tool get_journey_recording_prep reads
the same, and update_journey takes auto_prepare_recordings like the PUT
contract. The column stays out of the plugin output. All 27 locales carry
the strings; the Journey, Dawarich and MCP tool wiki pages describe it.
2026-10-21 17:39:33 +02:00

103 lines
4.7 KiB
TypeScript

/**
* JourneyRecordingsController (JRN-01): the routes of the recorded days a
* journey keeps, their guards, the date check it makes before the service is
* asked, and what it hands the service and answers.
*/
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 { JourneyRecordingsController } from '../../../src/nest/journey-recordings/journey-recordings.controller';
import type { JourneyRecordingsService } from '../../../src/nest/journey-recordings/journey-recordings.service';
import type { User } from '../../../src/types';
import { HttpException, RequestMethod } from '@nestjs/common';
import { GUARDS_METADATA, HTTP_CODE_METADATA, METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants';
import { describe, expect, it, vi } from 'vitest';
const user = { id: 4 } as User;
const HASH = 'f'.repeat(64);
function controllerWith() {
const service = {
list: vi.fn(async () => []),
preview: vi.fn(async () => ({ date: '2026-05-14' })),
save: vi.fn(async () => ({ outcome: 'created' })),
remove: vi.fn(async () => undefined),
prepStatus: vi.fn(async () => ({ enabled: true })),
};
return {
service,
controller: new JourneyRecordingsController(service as unknown as JourneyRecordingsService),
};
}
describe('JourneyRecordingsController', () => {
it('JRNREC-CTL-001: sits behind the Journey addon before auth, with the preview and the removal answering 200', () => {
expect(Reflect.getMetadata(PATH_METADATA, JourneyRecordingsController)).toBe('api/journeys/:id/recordings');
expect(Reflect.getMetadata(GUARDS_METADATA, JourneyRecordingsController)).toEqual([AddonGuard, JwtAuthGuard]);
expect(Reflect.getMetadata(REQUIRE_ADDON, JourneyRecordingsController)).toEqual({
addonId: 'journey',
label: 'Journey',
});
const routes = [
['list', RequestMethod.GET, '/', undefined],
['prep', RequestMethod.GET, 'prep', undefined],
['preview', RequestMethod.POST, 'preview', 200],
['save', RequestMethod.PUT, ':date', undefined],
['remove', RequestMethod.DELETE, ':date', 200],
] as const;
for (const [handler, method, path, code] of routes) {
const fn = JourneyRecordingsController.prototype[handler];
expect(Reflect.getMetadata(METHOD_METADATA, fn)).toBe(method);
expect(Reflect.getMetadata(PATH_METADATA, fn)).toBe(path);
expect(Reflect.getMetadata(HTTP_CODE_METADATA, fn)).toBe(code);
}
});
it('JRNREC-CTL-002: hands the caller, the journey, the day, the body and the socket id to the service', async () => {
const { service, controller } = controllerWith();
expect(await controller.list(user, '9')).toEqual({ days: [] });
expect(service.list).toHaveBeenCalledExactlyOnceWith(4, 9);
const preview = { date: '2026-05-14', utcOffsetMinutes: 120, useSourceOffset: true };
expect(await controller.preview(user, '9', preview)).toEqual({ date: '2026-05-14' });
expect(service.preview).toHaveBeenCalledExactlyOnceWith(4, 9, preview);
const save = { utcOffsetMinutes: 120, expectedHash: HASH, replace: true };
expect(await controller.save(user, '9', '2026-05-14', save, 'sock-1')).toEqual({ outcome: 'created' });
expect(service.save).toHaveBeenCalledExactlyOnceWith(4, 9, '2026-05-14', save, 'sock-1');
expect(await controller.remove(user, '9', '2026-05-14', 'sock-1')).toEqual({ success: true });
expect(service.remove).toHaveBeenCalledExactlyOnceWith(4, 9, '2026-05-14', 'sock-1');
});
it('JRNREC-CTL-003: a day that is no calendar day is a 400 before the service is asked', async () => {
const { service, controller } = controllerWith();
const refusals = await Promise.all(
['2026-02-30', '14.05.2026', 'today'].map((date) =>
controller.remove(user, '9', date).catch((err: unknown) => err),
),
);
for (const err of refusals) {
expect(err).toBeInstanceOf(HttpException);
expect((err as HttpException).getStatus()).toBe(400);
expect((err as HttpException).getResponse()).toEqual({ error: 'Invalid date' });
}
await expect(
controller.save(user, '9', 'nope', { utcOffsetMinutes: 0, expectedHash: HASH }),
).rejects.toBeInstanceOf(HttpException);
expect(service.remove).not.toHaveBeenCalled();
expect(service.save).not.toHaveBeenCalled();
});
it('JRNREC-CTL-004: asks the service how the journey is prepared for the caller (JRN-03)', async () => {
const { service, controller } = controllerWith();
expect(await controller.prep(user, '9')).toEqual({ enabled: true });
expect(service.prepStatus).toHaveBeenCalledExactlyOnceWith(4, 9);
});
});