107 lines
3.6 KiB
TypeScript
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,
|
|
});
|
|
}
|