mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
f72aba0c91
The last 20 wire methods move onto the decorators: db.* and the three unconditional methods (DbRpc), meta.* (MetaRpc) and the host-mediated surface of users/broadcasts/notify/ai/oauth/scheduler (HostSurfaceRpc). That closes the ledger at 113/113, so the legacy has() blocks in rpc-host.ts are gone and the router is down to what was never domain-specific: build the granted subset of the dispatch map, dispatch, audit, map errors. PluginHostDepsFactory was the 26-argument wiring sheet behind those blocks. With the handlers in the domains it has two dependencies left, so it is now PluginRpcHostFactory: the plugin's own sqlite handle, the inter-plugin router bound with this plugin as the caller, and the audit sink. The 18 host-to-plugin hook calls get the same treatment. They used to spell out the fn name and the timeout at each controller, which meant nothing checked that a hook:* grant on the consent screen had a consumer behind it, that the fn matched what the SDK documents, or that two call sites agreed on the budget. PluginHooks declares all 18 with @PluginHook and performs them, and the controllers call it by name. With both halves complete, requireTotalCoverage goes on: a KNOWN_METHOD without a handler, or a hook without a consumer, now fails app boot instead of surfacing as a PERMISSION_DENIED nobody can explain.
106 lines
4.7 KiB
TypeScript
106 lines
4.7 KiB
TypeScript
/**
|
|
* AtlasLayersController (#plugins): country tint layers plugins draw over the
|
|
* Atlas world map via the atlasLayerProvider hook. USER-SCOPED — no tripId, the
|
|
* acting user is host-bound and the hook takes no target parameter. Mirrors the
|
|
* map-markers hardening — gate + fail-safe + server-side normalization: ISO
|
|
* alpha-2 code validation (uppercase-coerced), tone enum default, length caps,
|
|
* layer/country count caps.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { pluginsEnabled } = vi.hoisted(() => ({
|
|
pluginsEnabled: vi.fn(() => true),
|
|
}));
|
|
vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: () => undefined }) }, canAccessTrip: vi.fn() }));
|
|
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
|
|
|
|
import { AtlasLayersController } from '../../../src/nest/plugins/atlas-layers.controller';
|
|
import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.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),
|
|
atlasLayers: vi.fn(async (id: string) => invoke(id)),
|
|
} as unknown as PluginHooks;
|
|
return { c: new AtlasLayersController(runtime), runtime };
|
|
}
|
|
const layer = (over: Record<string, unknown> = {}) => ({ id: 'l1', countries: [{ code: 'FR' }], ...over });
|
|
|
|
describe('AtlasLayersController', () => {
|
|
beforeEach(() => { pluginsEnabled.mockReturnValue(true); });
|
|
|
|
it('gates: disabled / no user return [] (no plugin calls on the first)', async () => {
|
|
pluginsEnabled.mockReturnValue(false);
|
|
const x = controller(() => [layer()]);
|
|
expect(await x.c.get(req(5))).toEqual({ layers: [] });
|
|
expect(x.runtime.providersOf).not.toHaveBeenCalled();
|
|
pluginsEnabled.mockReturnValue(true);
|
|
|
|
expect((await controller(() => [layer()]).c.get(req(undefined))).layers).toEqual([]);
|
|
});
|
|
|
|
it('keeps a valid layer, stamps pluginId, invokes the hook with NO target parameter', async () => {
|
|
const { c, runtime } = controller(() => [
|
|
layer({ name: 'Wishlist', countries: [{ code: 'FR', tone: 'success', label: 'Paris trip' }] }),
|
|
]);
|
|
const out = (await c.get(req(5))).layers;
|
|
expect(out).toEqual([{
|
|
pluginId: 'p1',
|
|
id: 'l1',
|
|
name: 'Wishlist',
|
|
countries: [{ code: 'FR', tone: 'success', label: 'Paris trip' }],
|
|
}]);
|
|
// user-scoped: empty args, the acting user is bound host-side
|
|
expect(runtime.atlasLayers).toHaveBeenCalledWith('p1', 5);
|
|
});
|
|
|
|
it('uppercase-coerces codes, drops anything that is not ISO alpha-2, defaults a bad tone', async () => {
|
|
const { c } = controller(() => [
|
|
layer({
|
|
countries: [
|
|
{ code: 'de' }, // lowercased -> DE
|
|
{ code: 'DEU' }, // alpha-3 -> dropped
|
|
{ code: '1x' }, // digits -> dropped
|
|
{ code: '' }, // empty -> dropped
|
|
null, // non-object -> dropped
|
|
{ code: 'JP', tone: 'evil', label: 'L'.repeat(200) },
|
|
],
|
|
}),
|
|
]);
|
|
const out = (await c.get(req(5))).layers[0].countries;
|
|
expect(out.map((x) => x.code)).toEqual(['DE', 'JP']);
|
|
expect(out[0].tone).toBe('default');
|
|
expect(out[0].label).toBeUndefined();
|
|
expect(out[1].tone).toBe('default'); // unknown tone -> default
|
|
expect(out[1].label!.length).toBe(80); // capped
|
|
});
|
|
|
|
it('drops id-less layers and non-objects, caps the name, tolerates non-array results', async () => {
|
|
const { c } = controller(() => [
|
|
layer({ id: '' }), // no id -> can't key it
|
|
null, // non-object
|
|
layer({ id: 'ok', name: 'N'.repeat(200), countries: 'not an array' }),
|
|
]);
|
|
const out = (await c.get(req(5))).layers;
|
|
expect(out).toHaveLength(1);
|
|
expect(out[0].id).toBe('ok');
|
|
expect(out[0].name!.length).toBe(60);
|
|
expect(out[0].countries).toEqual([]);
|
|
|
|
expect((await controller(() => 'garbage').c.get(req(5))).layers).toEqual([]);
|
|
});
|
|
|
|
it('caps at 3 layers per plugin and 300 countries per layer, and skips a failing provider', async () => {
|
|
const many = Array.from({ length: 5 }, (_, i) => layer({
|
|
id: `l${i}`,
|
|
countries: Array.from({ length: 350 }, () => ({ code: 'US' })),
|
|
}));
|
|
const { c } = controller((id) => (id === 'bad' ? (() => { throw new Error('boom'); })() : many), ['good', 'bad']);
|
|
const out = (await c.get(req(5))).layers;
|
|
expect(out).toHaveLength(3); // good capped to 3; bad contributes nothing
|
|
expect(out[0].countries).toHaveLength(300);
|
|
});
|
|
});
|