| import { describe, expect, it, vi } from "vitest"; |
| import { registerPluginHttpRoute } from "./http-registry.js"; |
| import { createEmptyPluginRegistry } from "./registry.js"; |
|
|
| function expectRouteRegistrationDenied(params: { |
| replaceExisting: boolean; |
| expectedLogFragment: string; |
| }) { |
| const registry = createEmptyPluginRegistry(); |
| const logs: string[] = []; |
|
|
| registerPluginHttpRoute({ |
| path: "/plugins/demo", |
| auth: "plugin", |
| handler: vi.fn(), |
| registry, |
| pluginId: "demo-a", |
| source: "demo-a-src", |
| log: (msg) => logs.push(msg), |
| }); |
|
|
| const unregister = registerPluginHttpRoute({ |
| path: "/plugins/demo", |
| auth: "plugin", |
| ...(params.replaceExisting ? { replaceExisting: true } : {}), |
| handler: vi.fn(), |
| registry, |
| pluginId: "demo-b", |
| source: "demo-b-src", |
| log: (msg) => logs.push(msg), |
| }); |
|
|
| expect(registry.httpRoutes).toHaveLength(1); |
| expect(logs.at(-1)).toContain(params.expectedLogFragment); |
|
|
| unregister(); |
| expect(registry.httpRoutes).toHaveLength(1); |
| } |
|
|
| describe("registerPluginHttpRoute", () => { |
| it("registers route and unregisters it", () => { |
| const registry = createEmptyPluginRegistry(); |
| const handler = vi.fn(); |
|
|
| const unregister = registerPluginHttpRoute({ |
| path: "/plugins/demo", |
| auth: "plugin", |
| handler, |
| registry, |
| }); |
|
|
| expect(registry.httpRoutes).toHaveLength(1); |
| expect(registry.httpRoutes[0]?.path).toBe("/plugins/demo"); |
| expect(registry.httpRoutes[0]?.handler).toBe(handler); |
| expect(registry.httpRoutes[0]?.auth).toBe("plugin"); |
| expect(registry.httpRoutes[0]?.match).toBe("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 = createEmptyPluginRegistry(); |
| const logs: string[] = []; |
| const firstHandler = vi.fn(); |
| const secondHandler = vi.fn(); |
|
|
| const unregisterFirst = registerPluginHttpRoute({ |
| path: "/plugins/synology", |
| auth: "plugin", |
| handler: firstHandler, |
| registry, |
| accountId: "default", |
| pluginId: "synology-chat", |
| log: (msg) => logs.push(msg), |
| }); |
|
|
| const unregisterSecond = registerPluginHttpRoute({ |
| path: "/plugins/synology", |
| auth: "plugin", |
| replaceExisting: true, |
| handler: secondHandler, |
| registry, |
| accountId: "default", |
| pluginId: "synology-chat", |
| log: (msg) => logs.push(msg), |
| }); |
|
|
| 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)', |
| ); |
|
|
| |
| unregisterFirst(); |
| expect(registry.httpRoutes).toHaveLength(1); |
| expect(registry.httpRoutes[0]?.handler).toBe(secondHandler); |
|
|
| unregisterSecond(); |
| expect(registry.httpRoutes).toHaveLength(0); |
| }); |
|
|
| it("rejects conflicting route registrations without replaceExisting", () => { |
| expectRouteRegistrationDenied({ |
| replaceExisting: false, |
| expectedLogFragment: "route conflict", |
| }); |
| }); |
|
|
| it("rejects route replacement when a different plugin owns the route", () => { |
| expectRouteRegistrationDenied({ |
| replaceExisting: true, |
| expectedLogFragment: "route replacement denied", |
| }); |
| }); |
|
|
| it("rejects mixed-auth overlapping routes", () => { |
| const registry = createEmptyPluginRegistry(); |
| const logs: string[] = []; |
|
|
| registerPluginHttpRoute({ |
| path: "/plugin/secure", |
| auth: "gateway", |
| match: "prefix", |
| handler: vi.fn(), |
| registry, |
| pluginId: "demo-gateway", |
| source: "demo-gateway-src", |
| log: (msg) => logs.push(msg), |
| }); |
|
|
| const unregister = registerPluginHttpRoute({ |
| path: "/plugin/secure/report", |
| auth: "plugin", |
| match: "exact", |
| handler: vi.fn(), |
| registry, |
| pluginId: "demo-plugin", |
| source: "demo-plugin-src", |
| log: (msg) => logs.push(msg), |
| }); |
|
|
| expect(registry.httpRoutes).toHaveLength(1); |
| expect(logs.at(-1)).toContain("route overlap denied"); |
|
|
| unregister(); |
| expect(registry.httpRoutes).toHaveLength(1); |
| }); |
| }); |
|
|