mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
b82962f95b
* feat(shared): scope plugin action descriptors * feat(plugin-sdk): instance-scoped settings actions * feat(plugins): run instance-scoped actions from the admin settings dialog * feat(admin): instance-scoped plugin actions in the settings dialog * fix(plugins): surface instance actions without instance fields and harden the action gate * docs(plugin-sdk): note that hosts before 4.2.0 ignore action scope * fix(admin): open the dialog for instance actions, explain inactive 404s, guard the save window * docs(wiki): plugin pages for instance actions and the admin panel changes
67 lines
3.6 KiB
TypeScript
67 lines
3.6 KiB
TypeScript
/**
|
|
* PluginUserSettingsController (#plugins): a user's own scope:'user' settings. Thin —
|
|
* PluginsService holds the storage/masking logic (separately tested); here we prove the
|
|
* gates (runtime off / no user / inactive plugin) and the delegation.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { pluginsEnabled, getMock } = vi.hoisted(() => ({
|
|
pluginsEnabled: vi.fn(() => true),
|
|
getMock: vi.fn(() => ({ 1: 1 })), // active by default
|
|
}));
|
|
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
|
|
vi.mock('../../../src/db/database', () => ({ db: { prepare: () => ({ get: getMock }) } }));
|
|
import { db as dbConn } from '../../../src/db/database';
|
|
import { DatabaseService } from '../../../src/nest/database/database.service';
|
|
|
|
import { PluginUserSettingsController } from '../../../src/nest/plugins/plugin-user-settings.controller';
|
|
import type { PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.service';
|
|
import type { PluginsService } from '../../../src/nest/plugins/plugins.service';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
|
|
function ctrl() {
|
|
const svc = {
|
|
userSettingsFields: vi.fn(() => [{ key: 'apiKey', secret: true }]),
|
|
getUserConfig: vi.fn(() => ({ apiKey: '••••••••' })),
|
|
updateUserConfig: vi.fn((_id: string, _uid: number, patch: Record<string, unknown>) => ({ ...patch, apiKey: '••••••••' })),
|
|
} as unknown as PluginsService;
|
|
// The controller now also takes the runtime (for settings-page actions).
|
|
const runtime = { actionsOf: vi.fn(() => []), invokeAction: vi.fn(async () => ({ ok: true })) } as unknown as PluginRuntimeService;
|
|
return { c: new PluginUserSettingsController(svc, runtime, new DatabaseService(dbConn)), svc, runtime };
|
|
}
|
|
|
|
describe('PluginUserSettingsController', () => {
|
|
beforeEach(() => { pluginsEnabled.mockReturnValue(true); getMock.mockReturnValue({ 1: 1 }); });
|
|
|
|
it('GET returns fields + masked config for a bound user; empty when gated', () => {
|
|
const { c } = ctrl();
|
|
expect(c.get('p', req(5))).toEqual({ fields: [{ key: 'apiKey', secret: true }], config: { apiKey: '••••••••' }, actions: [] });
|
|
pluginsEnabled.mockReturnValue(false);
|
|
expect(c.get('p', req(5))).toEqual({ fields: [], config: {}, actions: [] });
|
|
pluginsEnabled.mockReturnValue(true);
|
|
expect(c.get('p', req(undefined))).toEqual({ fields: [], config: {}, actions: [] });
|
|
getMock.mockReturnValue(undefined as never);
|
|
expect(c.get('p', req(5))).toEqual({ fields: [], config: {}, actions: [] });
|
|
});
|
|
|
|
it('POST delegates the patch for a bound user; empty when gated', () => {
|
|
const { c, svc } = ctrl();
|
|
expect(c.update('p', { config: { units: 'metric' } }, req(5))).toEqual({ config: { units: 'metric', apiKey: '••••••••' } });
|
|
expect(svc.updateUserConfig).toHaveBeenCalledWith('p', 5, { units: 'metric' });
|
|
// a non-object body → empty patch (no throw)
|
|
c.update('p', {}, req(5));
|
|
expect(svc.updateUserConfig).toHaveBeenCalledWith('p', 5, {});
|
|
getMock.mockReturnValue(undefined as never);
|
|
expect(c.update('p', { config: { units: 'metric' } }, req(5))).toEqual({ config: {} });
|
|
});
|
|
|
|
it('GET lists USER-scope actions only, and POST runs one as the caller in the user scope', async () => {
|
|
const { c, runtime } = ctrl();
|
|
c.get('p', req(5));
|
|
expect(runtime.actionsOf).toHaveBeenCalledWith('p', 'user');
|
|
expect(await c.runAction('p', 'sync', req(5))).toEqual({ ok: true });
|
|
expect(runtime.invokeAction).toHaveBeenCalledWith('p', 'sync', 5, 'user');
|
|
});
|
|
});
|