Files
TREK/server/tests/unit/plugins/plugins-controller-devlink.test.ts
T
Maurice 97f4784790 refactor(server): keep three suppressed lines whole so Prettier cannot move them past their directive
Prettier wraps the emoji regex onto the line after its declaration and
spreads two any-cast lines in the plugin tests over several lines, which
would leave the casts outside the eslint-disable-next-line above them. The
regex directive now sits on the line right before the literal, the dev-link
controller takes never instead of any for the collaborators it ignores, and
the supervisor test reaches the private kill/onExit through one cast.
2026-10-10 20:52:57 +02:00

63 lines
3.3 KiB
TypeScript

/**
* The dev-link controller endpoints (#plugins): POST /link and POST /:id/reload.
* Verifies both are gated by the kill-switch AND the dev-only TREK_PLUGINS_DEV_LINK
* flag, that link validates its body, and that reload maps a re-consent error to 409
* exactly like activate does. The runtime is mocked — this is the HTTP glue only.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { HttpException } from '@nestjs/common';
import { PluginsController } from '../../../src/nest/plugins/plugins.controller';
import { PluginConsentRequired } from '../../../src/nest/plugins/plugin-runtime.service';
import type { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ctrl = (runtime: any) => new PluginsController({} as never, runtime, {} as never, { isManaged: () => false } as unknown as RuntimeEnvService);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const status = async (p: Promise<any>): Promise<number> =>
p.then(() => -1, (e) => (e instanceof HttpException ? e.getStatus() : 0));
describe('PluginsController dev-link endpoints', () => {
beforeEach(() => { process.env.TREK_PLUGINS_ENABLED = 'true'; });
afterEach(() => { delete process.env.TREK_PLUGINS_DEV_LINK; delete process.env.TREK_PLUGINS_ENABLED; });
it('POST /link is 403 unless dev-link is enabled, 400 without a path, and delegates when enabled', async () => {
const runtime = { link: vi.fn(async () => ({ id: 'x', version: '1.0.0', replaced: false })) };
delete process.env.TREK_PLUGINS_DEV_LINK;
expect(await status(ctrl(runtime).link({ path: '/abs/dir' }))).toBe(403);
expect(runtime.link).not.toHaveBeenCalled();
process.env.TREK_PLUGINS_DEV_LINK = '1';
expect(await status(ctrl(runtime).link({}))).toBe(400); // no path
expect(await ctrl(runtime).link({ path: ' /abs/dir ' })).toMatchObject({ id: 'x' });
expect(runtime.link).toHaveBeenCalledWith('/abs/dir'); // trimmed
});
it('POST /link surfaces a runtime error as 400', async () => {
process.env.TREK_PLUGINS_DEV_LINK = '1';
const runtime = { link: vi.fn(async () => { throw new Error('no build'); }) };
expect(await status(ctrl(runtime).link({ path: '/abs' }))).toBe(400);
});
it('POST /:id/reload is 403 unless dev-link is enabled', async () => {
const runtime = { reload: vi.fn(async () => {}), isActive: () => true };
delete process.env.TREK_PLUGINS_DEV_LINK;
expect(await status(ctrl(runtime).reload('p'))).toBe(403);
expect(runtime.reload).not.toHaveBeenCalled();
});
it('POST /:id/reload maps a re-consent error to 409 and otherwise returns the live status', async () => {
process.env.TREK_PLUGINS_DEV_LINK = '1';
const consent = {
reload: vi.fn(async () => { throw new PluginConsentRequired('needs consent', ['db:read:trips'], []); }),
isActive: () => false,
};
expect(await status(ctrl(consent).reload('p'))).toBe(409);
const ok = { reload: vi.fn(async () => {}), isActive: () => true };
expect(await ctrl(ok).reload('p')).toEqual({ status: 'active' });
const bad = { reload: vi.fn(async () => { throw new Error('boom'); }), isActive: () => false };
expect(await status(ctrl(bad).reload('p'))).toBe(400);
});
});