mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
168 lines
6.9 KiB
TypeScript
168 lines
6.9 KiB
TypeScript
/**
|
|
* PluginsFeedController (#plugins): the authenticated active-plugin feed. Focused
|
|
* on the parts that re-validate hand-editable DB JSON: capability-derived fields
|
|
* must never surface values the manifest parser would have rejected, and
|
|
* routeProfiles must only appear for plugins whose hook:route-provider grant is
|
|
* actually recorded — a declared-but-ungranted provider can never serve a route,
|
|
* so offering its profiles in the picker would produce dead buttons.
|
|
*/
|
|
import type { PluginsRepository } from '../../../src/db/repositories/Plugins.repository';
|
|
import { PluginsFeedController } from '../../../src/nest/plugins/plugins-feed.controller';
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { rows, pluginsEnabled } = vi.hoisted(() => ({
|
|
rows: { value: [] as Array<Record<string, unknown>> },
|
|
pluginsEnabled: vi.fn(() => true),
|
|
}));
|
|
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
|
|
|
|
const row = (over: Record<string, unknown> = {}) => ({
|
|
id: 'p1',
|
|
name: 'P',
|
|
type: 'integration',
|
|
icon: null,
|
|
capabilities: '{}',
|
|
granted_permissions: '[]',
|
|
...over,
|
|
});
|
|
|
|
describe('PluginsFeedController', () => {
|
|
// PFC1 (Plan 3j Task 5) — the feed read is now Plugins.repository.ts#findActiveFeedRows.
|
|
const plugins = { findActiveFeedRows: vi.fn(async () => rows.value) } as unknown as PluginsRepository;
|
|
const c = new PluginsFeedController(plugins);
|
|
beforeEach(() => {
|
|
pluginsEnabled.mockReturnValue(true);
|
|
rows.value = [];
|
|
});
|
|
|
|
it('marks a plugin as a search provider only alongside the recorded hook:search-provider grant (#2221)', async () => {
|
|
rows.value = [
|
|
row({ id: 'index', granted_permissions: JSON.stringify(['hook:search-provider']) }),
|
|
row({ id: 'asked-only', permissions: JSON.stringify(['hook:search-provider']) }),
|
|
row({ id: 'poi-only', granted_permissions: JSON.stringify(['hook:poi-category-provider']) }),
|
|
];
|
|
const { plugins } = await c.list();
|
|
expect(plugins.find((p) => p.id === 'index')?.searchProvider).toBe(true);
|
|
expect(plugins.find((p) => p.id === 'asked-only')).not.toHaveProperty('searchProvider');
|
|
expect(plugins.find((p) => p.id === 'poi-only')).not.toHaveProperty('searchProvider');
|
|
});
|
|
|
|
it('returns an empty feed when the runtime is disabled', async () => {
|
|
pluginsEnabled.mockReturnValue(false);
|
|
rows.value = [row()];
|
|
expect(await c.list()).toEqual({ plugins: [] });
|
|
});
|
|
|
|
it('serves routeProfiles only alongside the recorded hook:route-provider grant', async () => {
|
|
rows.value = [
|
|
row({
|
|
id: 'granted',
|
|
capabilities: JSON.stringify({ routeProfiles: [{ id: 'ev', label: 'EV' }] }),
|
|
granted_permissions: JSON.stringify(['hook:route-provider']),
|
|
}),
|
|
row({ id: 'ungranted', capabilities: JSON.stringify({ routeProfiles: [{ id: 'ev', label: 'EV' }] }) }),
|
|
];
|
|
const { plugins } = await c.list();
|
|
expect(plugins.find((p) => p.id === 'granted')?.routeProfiles).toEqual([{ id: 'ev', label: 'EV' }]);
|
|
expect(plugins.find((p) => p.id === 'ungranted')?.routeProfiles).toBeUndefined();
|
|
});
|
|
|
|
it('re-validates hand-edited routeProfiles rows (bad ids dropped, labels capped, max 3)', async () => {
|
|
rows.value = [
|
|
row({
|
|
id: 'edited',
|
|
capabilities: JSON.stringify({
|
|
routeProfiles: [
|
|
{ id: '../up', label: 'bad id' },
|
|
{ id: 'ok', label: ' L '.padEnd(60, 'x') },
|
|
{ id: 'a', label: 'A' },
|
|
{ id: 'b', label: 'B' },
|
|
{ id: 'c', label: 'C' },
|
|
],
|
|
}),
|
|
granted_permissions: JSON.stringify(['hook:route-provider']),
|
|
}),
|
|
];
|
|
const profiles = (await c.list()).plugins[0].routeProfiles!;
|
|
expect(profiles.every((p) => /^[a-z][a-z0-9-]{0,23}$/.test(p.id))).toBe(true);
|
|
expect(profiles.length).toBeLessThanOrEqual(3);
|
|
expect(profiles.every((p) => p.label.length <= 40)).toBe(true);
|
|
});
|
|
|
|
describe('poiCategories (#1781)', () => {
|
|
const cat = {
|
|
id: 'trailheads',
|
|
label: 'Trailheads',
|
|
labels: { de: 'Wanderparkplätze' },
|
|
icon: 'Signpost',
|
|
color: '#2f855a',
|
|
};
|
|
const granted = JSON.stringify(['hook:poi-category-provider']);
|
|
|
|
it('serves them only alongside the recorded hook:poi-category-provider grant', async () => {
|
|
rows.value = [
|
|
row({ id: 'granted', capabilities: JSON.stringify({ poiCategories: [cat] }), granted_permissions: granted }),
|
|
row({ id: 'ungranted', capabilities: JSON.stringify({ poiCategories: [cat] }) }),
|
|
row({
|
|
id: 'searchOnly',
|
|
capabilities: JSON.stringify({ poiCategories: [cat] }),
|
|
granted_permissions: JSON.stringify(['hook:search-provider']),
|
|
}),
|
|
];
|
|
const { plugins } = await c.list();
|
|
expect(plugins.find((p) => p.id === 'granted')?.poiCategories).toEqual([cat]);
|
|
expect(plugins.find((p) => p.id === 'ungranted')?.poiCategories).toBeUndefined();
|
|
expect(plugins.find((p) => p.id === 'searchOnly')?.poiCategories).toBeUndefined();
|
|
});
|
|
|
|
it('re-validates a hand-edited row: bad colour, icon or id dropped, duplicates and the fifth one gone', async () => {
|
|
rows.value = [
|
|
row({
|
|
capabilities: JSON.stringify({
|
|
poiCategories: [
|
|
{ ...cat, color: '#fff;background:url(https://tracker.example)' },
|
|
{ ...cat, id: 'x', icon: 'Skull' },
|
|
{ ...cat, id: '../x' },
|
|
{ ...cat, id: 'ok', label: 'Water \u{1F6B0}' },
|
|
{ ...cat, id: 'ok' },
|
|
{ ...cat, id: 'late' },
|
|
],
|
|
}),
|
|
granted_permissions: granted,
|
|
}),
|
|
];
|
|
// The first four entries are the ones read (the cap), and only one of them survives.
|
|
expect((await c.list()).plugins[0].poiCategories).toEqual([{ ...cat, id: 'ok', label: 'Water' }]);
|
|
});
|
|
|
|
it('omits the field when nothing survives', async () => {
|
|
rows.value = [
|
|
row({
|
|
capabilities: JSON.stringify({ poiCategories: [{ ...cat, color: 'red' }] }),
|
|
granted_permissions: granted,
|
|
}),
|
|
];
|
|
expect((await c.list()).plugins[0]).not.toHaveProperty('poiCategories');
|
|
});
|
|
});
|
|
|
|
it('flags geolocation only when the grant is recorded', async () => {
|
|
rows.value = [
|
|
row({ id: 'granted', granted_permissions: JSON.stringify(['geolocation:read']) }),
|
|
row({ id: 'ungranted' }),
|
|
];
|
|
const { plugins } = await c.list();
|
|
expect(plugins.find((p) => p.id === 'granted')?.geolocation).toBe(true);
|
|
expect(plugins.find((p) => p.id === 'ungranted')?.geolocation).toBeUndefined();
|
|
});
|
|
|
|
it('survives malformed JSON blobs without dropping the plugin', async () => {
|
|
rows.value = [row({ capabilities: '{not json', granted_permissions: 'also not' })];
|
|
const { plugins } = await c.list();
|
|
expect(plugins).toHaveLength(1);
|
|
expect(plugins[0].routeProfiles).toBeUndefined();
|
|
expect(plugins[0].slot).toBe('sidebar');
|
|
});
|
|
});
|