mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
77dddd5e8a
Release branch for 4.0.0.
84 lines
4.3 KiB
TypeScript
84 lines
4.3 KiB
TypeScript
/**
|
|
* MapMarkersController (#plugins, #587): bounded markers plugins overlay on the trip
|
|
* map via the mapMarkerProvider hook. Mirrors the view-contributions hardening —
|
|
* gate + fail-safe + server-side normalization: coordinate range checks, a URL-scheme
|
|
* allowlist (no click-XSS), length + per-provider count caps, tone enum default.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { canAccessTrip, pluginsEnabled } = vi.hoisted(() => ({
|
|
canAccessTrip: vi.fn((tripId: number, userId: number) => (tripId === 1 && userId === 5 ? { id: 1 } : undefined)),
|
|
pluginsEnabled: vi.fn(() => true),
|
|
}));
|
|
vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: () => undefined }) }, canAccessTrip }));
|
|
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
|
|
|
|
import { MapMarkersController } from '../../../src/nest/plugins/contributions/map-markers.controller';
|
|
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service';
|
|
import type { DatabaseService } from '../../../src/nest/database/database.service';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
|
|
function controller(invoke: (id: string) => unknown, providers = ['p1']) {
|
|
const runtime = {
|
|
providersOf: vi.fn(() => providers),
|
|
mapMarkers: vi.fn(async (id: string) => invoke(id)),
|
|
} as unknown as PluginHooks;
|
|
return { c: new MapMarkersController(runtime, { canAccessTrip } as unknown as DatabaseService), runtime };
|
|
}
|
|
const mk = (over: Record<string, unknown> = {}) => ({ id: 'm1', lat: 48.85, lng: 2.35, ...over });
|
|
|
|
describe('MapMarkersController', () => {
|
|
beforeEach(() => { pluginsEnabled.mockReturnValue(true); canAccessTrip.mockReturnValue({ id: 1 } as never); });
|
|
|
|
it('gates: disabled / no user / non-member all return [] (no plugin calls on the first)', async () => {
|
|
pluginsEnabled.mockReturnValue(false);
|
|
let x = controller(() => [mk()]);
|
|
expect(await x.c.get('1', req(5))).toEqual({ markers: [] });
|
|
expect(x.runtime.providersOf).not.toHaveBeenCalled();
|
|
pluginsEnabled.mockReturnValue(true);
|
|
|
|
expect((await controller(() => [mk()]).c.get('1', req(undefined))).markers).toEqual([]);
|
|
canAccessTrip.mockReturnValue(undefined as never);
|
|
expect((await controller(() => [mk()]).c.get('1', req(5))).markers).toEqual([]);
|
|
});
|
|
|
|
it('keeps a valid marker, stamps pluginId + default tone, keeps http/https/mailto urls', async () => {
|
|
const { c } = controller(() => [mk({ label: 'Hotel', popupText: 'Check-in 15:00', url: 'https://ok.example', tone: 'success' })]);
|
|
const out = (await c.get('1', req(5))).markers;
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0]).toMatchObject({ pluginId: 'p1', id: 'm1', lat: 48.85, lng: 2.35, label: 'Hotel', tone: 'success', url: 'https://ok.example' });
|
|
});
|
|
|
|
it('drops out-of-range coordinates, missing id, and non-objects', async () => {
|
|
const { c } = controller(() => [
|
|
mk({ lat: 200 }), // lat out of range
|
|
mk({ lng: -999 }), // lng out of range
|
|
mk({ id: '' }), // no id
|
|
mk({ lat: 'nope' }), // non-numeric
|
|
null, // non-object
|
|
mk({ id: 'good' }), // the only survivor
|
|
]);
|
|
const out = (await c.get('1', req(5))).markers;
|
|
expect(out.map(m => m.id)).toEqual(['good']);
|
|
});
|
|
|
|
it('drops javascript:/data: urls, defaults a bad tone, caps lengths', async () => {
|
|
const { c } = controller(() => [
|
|
mk({ id: 'x', url: 'javascript:alert(1)', tone: 'evil', label: 'L'.repeat(500), popupText: 'P'.repeat(500) }),
|
|
]);
|
|
const out = (await c.get('1', req(5))).markers;
|
|
expect(out[0].url).toBeUndefined(); // click-XSS scheme dropped
|
|
expect(out[0].tone).toBe('default'); // unknown tone -> default
|
|
expect(out[0].label!.length).toBe(80); // capped
|
|
expect(out[0].popupText!.length).toBe(280);
|
|
});
|
|
|
|
it('caps the marker count at 200 per provider and skips a failing provider', async () => {
|
|
const many = Array.from({ length: 250 }, (_, i) => mk({ id: `m${i}` }));
|
|
const { c } = controller((id) => (id === 'bad' ? (() => { throw new Error('boom'); })() : many), ['good', 'bad']);
|
|
const out = (await c.get('1', req(5))).markers;
|
|
expect(out).toHaveLength(200); // good capped to 200; bad contributes nothing
|
|
});
|
|
});
|