Files
2026-08-28 16:41:54 -05:00

107 lines
3.6 KiB
TypeScript

import {
ANONYMOUS_PRINCIPAL,
type CapabilityId,
EMPTY_MODULE_CONFIGURATION,
EMPTY_MODULE_STORAGE,
type ModuleConfiguration,
type ModuleRouteDefinitionInput,
type ModuleRouteHandlerContext,
type ModuleRuntimeServices,
type RegisteredCapability,
} from "@ekklesia/module-sdk";
import moduleRuntime from "../../src/server/index.js";
/**
* The module-layer test harness: everything a Signage unit test needs to exercise the real runtime
* definition without a platform.
*
* Every object here is a `@ekklesia/contracts` interface constructed by hand, which is exactly what
* a module-layer test may legitimately do — a module may never import Core. It also demonstrates the
* portability claim directly: the host producing these objects could be Ekklesia, or a future
* standalone host reading YAML from disk, and Signage cannot tell the difference.
*/
export interface CapturedRegistration {
readonly channels: { channel: string; permission: string }[];
readonly provided: Map<CapabilityId, unknown>;
readonly routes: ModuleRouteDefinitionInput[];
}
export async function registerModule(
config: ModuleConfiguration = EMPTY_MODULE_CONFIGURATION,
): Promise<CapturedRegistration> {
const channels: { channel: string; permission: string }[] = [];
const provided = new Map<CapabilityId, unknown>();
const routes: ModuleRouteDefinitionInput[] = [];
await moduleRuntime.register?.({
capabilities: {
provide(capabilityId, implementation) {
provided.set(capabilityId, implementation);
},
resolve(_capabilityId: CapabilityId): RegisteredCapability | undefined {
return undefined;
},
},
config,
http: {
route(definition) {
routes.push(definition);
},
},
moduleId: "signage",
realtime: {
channel(definition) {
channels.push({ channel: definition.channel, permission: definition.permission });
},
},
});
return { channels, provided, routes };
}
export function findRoute(
registration: CapturedRegistration,
method: string,
path: string,
): ModuleRouteDefinitionInput {
const route = registration.routes.find(
(candidate) => candidate.method === method && candidate.path === path,
);
if (!route) {
throw new Error(`Signage did not register ${method} ${path}.`);
}
return route;
}
export function fakeConfiguration(values: Readonly<Record<string, unknown>>): ModuleConfiguration {
const read = <Value>(key: string, kind: string): Value | undefined => {
const value = values[key];
return typeof value === kind ? (value as Value) : undefined;
};
return Object.freeze({
get: (key: string) => values[key] as never,
getBoolean: (key: string) => read<boolean>(key, "boolean"),
getEnum: (key: string) => read<string>(key, "string"),
getInteger: (key: string) => read<number>(key, "number"),
getSecret: (key: string) => read<string>(key, "string"),
getString: (key: string) => read<string>(key, "string"),
getUrl: (key: string) => read<string>(key, "string"),
has: (key: string) => Object.hasOwn(values, key),
keys: () => Object.freeze(Object.keys(values)),
});
}
export function fakeHandlerContext(
overrides: Partial<ModuleRouteHandlerContext> = {},
): ModuleRouteHandlerContext {
const services = { database: undefined } as unknown as ModuleRuntimeServices;
return Object.freeze({
config: EMPTY_MODULE_CONFIGURATION,
moduleId: "signage",
params: Object.freeze({}),
principal: ANONYMOUS_PRINCIPAL,
realtime: { publish: () => undefined },
services,
storage: EMPTY_MODULE_STORAGE,
...overrides,
});
}