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 = {}) { 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 }); });