mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 11:21:07 +00:00
232 lines
6.2 KiB
TypeScript
232 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|