refactor(plugins): async DB-touching methods; transactions through UnitOfWork

This commit is contained in:
jubnl
2026-09-22 00:00:53 +02:00
parent e413308227
commit e28dc6de28
68 changed files with 743 additions and 706 deletions
@@ -22,43 +22,43 @@ import type { PluginsService } from '../../../src/nest/plugins/plugins.service';
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: '••••••••' })),
userSettingsFields: vi.fn(async () => [{ key: 'apiKey', secret: true }]),
getUserConfig: vi.fn(async () => ({ apiKey: '••••••••' })),
updateUserConfig: vi.fn(async (_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;
const runtime = { actionsOf: vi.fn(async () => []), 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', () => {
it('GET returns fields + masked config for a bound user; empty when gated', async () => {
const { c } = ctrl();
expect(c.get('p', req(5))).toEqual({ fields: [{ key: 'apiKey', secret: true }], config: { apiKey: '••••••••' }, actions: [] });
expect(await 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: [] });
expect(await c.get('p', req(5))).toEqual({ fields: [], config: {}, actions: [] });
pluginsEnabled.mockReturnValue(true);
expect(c.get('p', req(undefined))).toEqual({ fields: [], config: {}, actions: [] });
expect(await c.get('p', req(undefined))).toEqual({ fields: [], config: {}, actions: [] });
getMock.mockReturnValue(undefined as never);
expect(c.get('p', req(5))).toEqual({ fields: [], config: {}, actions: [] });
expect(await c.get('p', req(5))).toEqual({ fields: [], config: {}, actions: [] });
});
it('POST delegates the patch for a bound user; empty when gated', () => {
it('POST delegates the patch for a bound user; empty when gated', async () => {
const { c, svc } = ctrl();
expect(c.update('p', { config: { units: 'metric' } }, req(5))).toEqual({ config: { units: 'metric', apiKey: '••••••••' } });
expect(await 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));
await 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: {} });
expect(await 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));
await 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');