Files
TREK/server/tests/unit/plugins/trip-warnings.controller.test.ts
T
Maurice fe35e26313 refactor(trip-membership): domains ask TripAccessService who may see a trip
Thirty services, controllers, MCP tools and RPC handlers outside the trips
domain injected TripsRepository for one call, findAccessible. They now
inject TripAccessService from the trip-membership leaf, in the same
constructor slot under the same field name; it answers with the same
access row (or undefined) and binds the id exactly as given, so each
caller keeps its own coercion and its own 404, 403 or tool error.
2026-10-10 20:16:00 +02:00

82 lines
4.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
const { canAccessTrip, pluginsEnabled } = vi.hoisted(() => ({
canAccessTrip: vi.fn(async (tripId: number, userId: number) => (tripId === 1 && userId === 5 ? { id: 1 } : undefined)),
pluginsEnabled: vi.fn(() => true),
}));
vi.mock('../../../src/db/database', () => ({ db: {}, canAccessTrip }));
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { TripWarningsController } from '../../../src/nest/plugins/contributions/trip-warnings.controller';
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
import type { TripsRepository } from '../../../src/db/repositories/Trips.repository';
import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
function controller(over: Partial<PluginHooks> = {}) {
const runtime = {
providersOf: vi.fn(() => ['p1', 'p2']),
tripWarnings: vi.fn(async (id: string) =>
id === 'p2' ? [{ level: 'error', message: 'Day 3 is overpacked', dayId: 3 }] : [{ level: 'warning', message: 'Museum closed Mon', placeId: 7 }]),
...over,
} as unknown as PluginHooks;
return { c: new TripWarningsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime };
}
describe('TripWarningsController', () => {
beforeEach(() => { pluginsEnabled.mockReturnValue(true); canAccessTrip.mockResolvedValue({ id: 1 } as never); });
it('returns [] when disabled / no user / no access', async () => {
pluginsEnabled.mockReturnValue(false);
expect(await controller().c.get('1', req(5))).toEqual({ warnings: [] });
pluginsEnabled.mockReturnValue(true);
expect(await controller().c.get('1', req(undefined))).toEqual({ warnings: [] });
canAccessTrip.mockResolvedValue(undefined as never);
expect(await controller().c.get('1', req(5))).toEqual({ warnings: [] });
});
it('merges provider warnings, tags each with the plugin id, defaults an odd level to warning', async () => {
const { c } = controller({
tripWarnings: vi.fn(async (id: string) =>
id === 'p1' ? [{ level: 'bogus', message: 'Check timings' }, { message: '' }] : [{ level: 'info', message: 'Rainy week' }]) as unknown as PluginHooks['tripWarnings'],
});
const res = await c.get('1', req(5));
expect(res.warnings).toEqual([
{ pluginId: 'p1', level: 'warning', message: 'Check timings', dayId: undefined, placeId: undefined }, // unknown level → warning; empty-message row dropped
{ pluginId: 'p2', level: 'info', message: 'Rainy week', dayId: undefined, placeId: undefined },
]);
});
it('skips a provider that throws (graceful) and calls the hook with the trip + acting user', async () => {
const { c, runtime } = controller({
tripWarnings: vi.fn(async (id: string) => { if (id === 'p2') throw new Error('slow'); return [{ level: 'warning', message: 'ok' }]; }) as unknown as PluginHooks['tripWarnings'],
});
const res = await c.get('1', req(5));
expect(res.warnings).toEqual([{ pluginId: 'p1', level: 'warning', message: 'ok', dayId: undefined, placeId: undefined }]);
expect(runtime.tripWarnings).toHaveBeenCalledWith('p1', 1, 5);
});
it('strips emojis from the warning message (rendered natively in TREK chrome)', async () => {
const { c } = controller({
providersOf: vi.fn(() => ['p1']),
tripWarnings: vi.fn(async () => [{ level: 'error', message: '🔥 Overbooked!' }]) as unknown as PluginHooks['tripWarnings'],
});
const res = await c.get('1', req(5));
expect(res.warnings[0].message).toBe('Overbooked!');
});
it('caps a flooding provider at 20 warnings and truncates an oversized message', async () => {
const { c } = controller({
providersOf: vi.fn(() => ['flood']),
tripWarnings: vi.fn(async () => [
{ level: 'warning', message: 'z'.repeat(1000) },
...Array.from({ length: 50 }, (_v, i) => ({ level: 'info', message: `w${i}` })),
]) as unknown as PluginHooks['tripWarnings'],
});
const res = await c.get('1', req(5));
expect(res.warnings).toHaveLength(20); // per-provider count cap
expect(res.warnings[0].message).toHaveLength(300); // message length cap
});
});