import { afterEach, describe, expect, it, vi } from "vitest" ;
import { registerPluginHttpRoute } from "./http-registry.js" ;
import { createEmptyPluginRegistry } from "./registry-empty.js" ;
import {
pinActivePluginHttpRouteRegistry,
releasePinnedPluginHttpRouteRegistry,
resetPluginRuntimeStateForTest,
setActivePluginRegistry,
} from "./runtime.js" ;
function expectRouteRegistrationDenied(params: {
replaceExisting: boolean ;
expectedLogFragment: string;
}) {
const { registry, logs, register } = createLoggedRouteHarness();
register({
path: "/plugins/demo" ,
auth: "plugin" ,
pluginId: "demo-a" ,
source: "demo-a-src" ,
});
const unregister = register({
path: "/plugins/demo" ,
auth: "plugin" ,
...(params.replaceExisting ? { replaceExisting: true } : {}),
pluginId: "demo-b" ,
source: "demo-b-src" ,
});
expect(registry.httpRoutes).toHaveLength(1 );
expect(logs.at(-1 )).toContain(params.expectedLogFragment);
unregister();
expect(registry.httpRoutes).toHaveLength(1 );
}
function expectRegisteredRouteShape(
registry: ReturnType<typeof createEmptyPluginRegistry>,
params: {
path: string;
handler?: unknown;
auth: "plugin" | "gateway" ;
match?: "exact" | "prefix" ;
},
) {
expect(registry.httpRoutes).toHaveLength(1 );
expect(registry.httpRoutes[0 ]).toEqual(
expect.objectContaining({
path: params.path,
auth: params.auth,
...(params.match ? { match: params.match } : {}),
...(params.handler ? { handler: params.handler } : {}),
}),
);
}
function createLoggedRouteHarness() {
const registry = createEmptyPluginRegistry();
const logs: string[] = [];
return {
registry,
logs,
register: (
params: Omit<
Parameters<typeof registerPluginHttpRoute>[0 ],
"registry" | "handler" | "log"
> & {
handler?: Parameters<typeof registerPluginHttpRoute>[0 ]["handler" ];
},
) =>
registerPluginHttpRoute({
...params,
handler: params.handler ?? vi.fn(),
registry,
log: (msg) => logs.push(msg),
}),
};
}
describe("registerPluginHttpRoute" , () => {
afterEach(() => {
releasePinnedPluginHttpRouteRegistry();
resetPluginRuntimeStateForTest();
});
it("registers route and unregisters it" , () => {
const registry = createEmptyPluginRegistry();
const handler = vi.fn();
const unregister = registerPluginHttpRoute({
path: "/plugins/demo" ,
auth: "plugin" ,
handler,
registry,
});
expectRegisteredRouteShape(registry, {
path: "/plugins/demo" ,
handler,
auth: "plugin" ,
match: "exact" ,
});
unregister();
expect(registry.httpRoutes).toHaveLength(0 );
});
it("returns noop unregister when path is missing" , () => {
const registry = createEmptyPluginRegistry();
const logs: string[] = [];
const unregister = registerPluginHttpRoute({
path: "" ,
auth: "plugin" ,
handler: vi.fn(),
registry,
accountId: "default" ,
log: (msg) => logs.push(msg),
});
expect(registry.httpRoutes).toHaveLength(0 );
expect(logs).toEqual(['plugin: webhook path missing for account "default"' ]);
expect(() => unregister()).not.toThrow();
});
it("replaces stale route on same path when replaceExisting=true" , () => {
const { registry, logs, register } = createLoggedRouteHarness();
const firstHandler = vi.fn();
const secondHandler = vi.fn();
const unregisterFirst = register({
path: "/plugins/synology" ,
auth: "plugin" ,
handler: firstHandler,
accountId: "default" ,
pluginId: "synology-chat" ,
});
const unregisterSecond = register({
path: "/plugins/synology" ,
auth: "plugin" ,
replaceExisting: true ,
handler: secondHandler,
accountId: "default" ,
pluginId: "synology-chat" ,
});
expect(registry.httpRoutes).toHaveLength(1 );
expect(registry.httpRoutes[0 ]?.handler).toBe(secondHandler);
expect(logs).toContain(
'plugin: replacing stale webhook path /plugins/synology (exact) for account "default" (synology-chat)' ,
);
// Old unregister must not remove the replacement route.
unregisterFirst();
expect(registry.httpRoutes).toHaveLength(1 );
expect(registry.httpRoutes[0 ]?.handler).toBe(secondHandler);
unregisterSecond();
expect(registry.httpRoutes).toHaveLength(0 );
});
it.each([
{
name: "rejects conflicting route registrations without replaceExisting" ,
replaceExisting: false ,
expectedLogFragment: "route conflict" ,
},
{
name: "rejects route replacement when a different plugin owns the route" ,
replaceExisting: true ,
expectedLogFragment: "route replacement denied" ,
},
] as const )("$name" , ({ replaceExisting, expectedLogFragment }) => {
expectRouteRegistrationDenied({
replaceExisting,
expectedLogFragment,
});
});
it("rejects mixed-auth overlapping routes" , () => {
const { registry, logs, register } = createLoggedRouteHarness();
register({
path: "/plugin/secure" ,
auth: "gateway" ,
match: "prefix" ,
pluginId: "demo-gateway" ,
source: "demo-gateway-src" ,
});
const unregister = register({
path: "/plugin/secure/report" ,
auth: "plugin" ,
match: "exact" ,
pluginId: "demo-plugin" ,
source: "demo-plugin-src" ,
});
expect(registry.httpRoutes).toHaveLength(1 );
expect(logs.at(-1 )).toContain("route overlap denied" );
unregister();
expect(registry.httpRoutes).toHaveLength(1 );
});
it("uses the pinned route registry when the active registry changes later" , () => {
const startupRegistry = createEmptyPluginRegistry();
const laterActiveRegistry = createEmptyPluginRegistry();
setActivePluginRegistry(startupRegistry);
pinActivePluginHttpRouteRegistry(startupRegistry);
setActivePluginRegistry(laterActiveRegistry);
const unregister = registerPluginHttpRoute({
path: "/bluebubbles-webhook" ,
auth: "plugin" ,
handler: vi.fn(),
});
expectRegisteredRouteShape(startupRegistry, {
path: "/bluebubbles-webhook" ,
auth: "plugin" ,
});
expect(laterActiveRegistry.httpRoutes).toHaveLength(0 );
unregister();
expect(startupRegistry.httpRoutes).toHaveLength(0 );
});
});
Messung V0.5 in Prozent C=100 H=98 G=98
¤ Dauer der Verarbeitung: 0.3 Sekunden
¤
*© Formatika GbR, Deutschland