mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
fe35e26313
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.
82 lines
4.3 KiB
TypeScript
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
|
|
});
|
|
});
|