diff --git a/src/index.ts b/src/index.ts index 6633449..e2ca4b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -692,6 +692,14 @@ export interface RouteHandler { module?: string; } +/** + * Complete registered route handler and its identifier. + */ +export interface RegisteredRouteHandler { + id: string; + handler: RouteHandler; +} + /** * Registered route handlers indexed by their proxy id, mirroring the entries * held by {@link routesProxy}. Pruned by {@link RoutesProxy} so stale handlers @@ -699,6 +707,15 @@ export interface RouteHandler { */ const routesList = new Map(); +/** + * Retrieves complete registered route handlers. + * + * @returns Snapshot of the current route registry. + */ +export function getRegisteredRouteHandlers(): RegisteredRouteHandler[] { + return Array.from(routesList, ([id, handler]) => ({ id, handler })); +} + /** * RegisteringProxy that also prunes {@link routesList} on the same lifecycle * events that remove the real routes (single unregister or module unload), so diff --git a/src/tests/route-lifecycle.test.ts b/src/tests/route-lifecycle.test.ts index c068935..2d7437c 100644 --- a/src/tests/route-lifecycle.test.ts +++ b/src/tests/route-lifecycle.test.ts @@ -1,12 +1,23 @@ import assert from "node:assert"; import { + type ComputedParameter, + getRegisteredRouteHandlers, getRegisteredRoutes, + HandlerPriority, RegisterRoute, type RouteHandler, routesProxy, UnregisterRoute, } from "../index"; +interface ProxyCall { + handler?: RouteHandler; + id: string; + kind: "register" | "unregister"; +} + +const proxyCalls: ProxyCall[] = []; + function handlerAt(location: string): RouteHandler { return { location, @@ -19,6 +30,25 @@ function handlerAt(location: string): RouteHandler { } as unknown as RouteHandler; } +function completeHandlerAt(location: string): RouteHandler { + const parameter: ComputedParameter = { + provider: () => "parameter", + modifiers: [], + }; + return { + location, + method: "POST", + mode: "prefix", + parameters: [parameter, null], + properties: { property: parameter }, + proto: { controller: location }, + callback: function completeRouteCallback() { + return location; + }, + priority: HandlerPriority.HIGH, + }; +} + function routesAt(location: string): number { return getRegisteredRoutes().filter((route) => route.location === location) .length; @@ -29,11 +59,9 @@ describe("Route lifecycle", () => { // test harness (the api module binds the harness-distributed copy). A // recording provider both keeps the stub-mode proxy from throwing and lets // the cases assert what reaches the real registry. - const proxyCalls: Array<{ kind: "register" | "unregister"; id: string }> = []; - before(() => { - routesProxy.onRegister((id: string) => { - proxyCalls.push({ kind: "register", id }); + routesProxy.onRegister((id: string, handler: RouteHandler) => { + proxyCalls.push({ kind: "register", id, handler }); }, true); routesProxy.onUnregister((id: string) => { proxyCalls.push({ kind: "unregister", id }); @@ -81,3 +109,73 @@ describe("Route lifecycle", () => { assert.equal(getRegisteredRoutes().length, before); }); }); + +describe("getRegisteredRouteHandlers", () => { + const routeIds: number[] = []; + + function register(handler: RouteHandler): number { + const id = RegisterRoute(handler); + routeIds.push(id); + return id; + } + + afterEach(() => { + for (const id of routeIds.splice(0)) { + UnregisterRoute(id); + } + }); + + it("returns complete registered handlers without replacing the provider", () => { + const original = completeHandlerAt("/handlers/complete"); + const id = register(original); + + const registered = getRegisteredRouteHandlers().find( + (registered) => registered.id === id.toString(), + ); + assert(registered); + assert.equal(registered.handler.mode, original.mode); + assert.strictEqual(registered.handler.callback, original.callback); + assert.strictEqual(registered.handler.proto, original.proto); + assert.strictEqual(registered.handler.parameters, original.parameters); + assert.strictEqual(registered.handler.properties, original.properties); + assert.equal(registered.handler.priority, original.priority); + assert(Object.hasOwn(registered.handler, "module")); + assert.equal(typeof registered.handler.module, "string"); + const providerCall = proxyCalls.at(-1); + assert.equal(providerCall?.kind, "register"); + assert.equal(providerCall?.id, id.toString()); + assert.strictEqual(providerCall?.handler, registered.handler); + assert.equal(Object.hasOwn(original, "module"), false); + }); + + it("excludes explicitly unregistered routes", () => { + const id = register(handlerAt("/handlers/unregister")); + + UnregisterRoute(id); + + assert.equal( + getRegisteredRouteHandlers().some( + (registered) => registered.id === id.toString(), + ), + false, + ); + }); + + it("excludes routes removed during module unload", () => { + const id = register(handlerAt("/handlers/module-unload")); + const registered = getRegisteredRouteHandlers().find( + (entry) => entry.id === id.toString(), + ); + assert(registered); + const module = registered.handler.module; + assert(module); + + routesProxy.unregisterModule(module); + + assert.equal( + getRegisteredRouteHandlers().some((entry) => entry.id === id.toString()), + false, + ); + assert.equal(routesAt("/handlers/module-unload"), 0); + }); +});