feat: add display kiosk host
This commit is contained in:
@@ -8,11 +8,16 @@ const betterAuthMock = vi.hoisted(() => vi.fn());
|
||||
const drizzleAdapterMock = vi.hoisted(() => vi.fn());
|
||||
const serveMock = vi.hoisted(() => vi.fn());
|
||||
const processOnSpy = vi.hoisted(() => vi.fn());
|
||||
const loadDisplayHostAssetsMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("pg", () => ({ Pool: poolConstructor }));
|
||||
vi.mock("better-auth", () => ({ betterAuth: betterAuthMock }));
|
||||
vi.mock("better-auth/adapters/drizzle", () => ({ drizzleAdapter: drizzleAdapterMock }));
|
||||
vi.mock("@hono/node-server", () => ({ serve: serveMock }));
|
||||
vi.mock("../src/display-host-assets.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../src/display-host-assets.js")>();
|
||||
return { ...actual, loadDisplayHostAssets: loadDisplayHostAssetsMock };
|
||||
});
|
||||
// Wraps the real `createApiApp` in a spy rather than replacing it, so every existing test still
|
||||
// gets the real Hono application; only the one test that deliberately forces an app-construction
|
||||
// failure overrides this with `mockImplementationOnce`.
|
||||
@@ -30,6 +35,7 @@ import {
|
||||
import { PermissionRegistryError } from "@ekklesia/core/authorization";
|
||||
import { createApiApp } from "../src/app.js";
|
||||
import { createApiRuntimeConfig } from "../src/config.js";
|
||||
import { DisplayHostAssetError } from "../src/display-host-assets.js";
|
||||
import { installShutdownSignalHandlers, startApi } from "../src/server.js";
|
||||
|
||||
function validConfig() {
|
||||
@@ -82,7 +88,7 @@ async function createFixtureModule(
|
||||
): Promise<void> {
|
||||
const moduleDirectory = path.join(modulesDirectory, id);
|
||||
const manifest = {
|
||||
manifestVersion: 5,
|
||||
manifestVersion: 6,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
@@ -149,6 +155,8 @@ interface WorkingFixtureModuleOptions {
|
||||
* can be exercised deterministically without any sleep. */
|
||||
readonly stopNeverSettles?: boolean;
|
||||
readonly dependencies?: Record<string, unknown>;
|
||||
/** Declares one protected display artifact without importing it. */
|
||||
readonly withDisplay?: boolean;
|
||||
}
|
||||
|
||||
/** Writes a real, working module fixture — unlike `createFixtureModule`, this entrypoint does not
|
||||
@@ -162,12 +170,43 @@ async function createWorkingFixtureModule(
|
||||
options: WorkingFixtureModuleOptions = {},
|
||||
): Promise<void> {
|
||||
const permissionId = `${id}.ping.read`;
|
||||
const displayPermission = `${id}.display.view`;
|
||||
const permissions = [
|
||||
...(options.withRoute
|
||||
? [{ baseline: "public" as const, description: "Ping.", id: permissionId }]
|
||||
: []),
|
||||
...(options.withDisplay
|
||||
? [
|
||||
{
|
||||
baseline: "restricted" as const,
|
||||
description: "View the display.",
|
||||
id: displayPermission,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
await createFixtureModule(modulesDirectory, id, {
|
||||
permissions: options.withRoute
|
||||
? [{ baseline: "public", description: "Ping.", id: permissionId }]
|
||||
: [],
|
||||
entrypoints: {
|
||||
server: "./dist/server/index.js",
|
||||
...(options.withDisplay
|
||||
? {
|
||||
display: {
|
||||
artifact: "./dist/display/index.js",
|
||||
permission: displayPermission,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
permissions,
|
||||
...(options.dependencies ? { dependencies: options.dependencies } : {}),
|
||||
});
|
||||
if (options.withDisplay) {
|
||||
await mkdir(path.join(modulesDirectory, id, "dist/display"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(modulesDirectory, id, "dist/display/index.js"),
|
||||
'throw new Error("display artifact must never execute during server startup");\n',
|
||||
);
|
||||
}
|
||||
const routeSource = options.withRoute
|
||||
? `http.route({
|
||||
method: "GET",
|
||||
@@ -215,7 +254,7 @@ function fakeDiscoveredModule(
|
||||
compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" },
|
||||
entrypoints: { server: "./dist/does-not-exist/index.js" },
|
||||
id,
|
||||
manifestVersion: 5,
|
||||
manifestVersion: 6,
|
||||
name: `${id} module`,
|
||||
permissions,
|
||||
version: "0.1.0",
|
||||
@@ -942,6 +981,7 @@ describe("Ekklesia API module runtime startup and shutdown", () => {
|
||||
betterAuthMock.mockReset();
|
||||
drizzleAdapterMock.mockClear();
|
||||
serveMock.mockReset();
|
||||
loadDisplayHostAssetsMock.mockReset();
|
||||
vi.mocked(createApiApp).mockClear();
|
||||
});
|
||||
|
||||
@@ -954,6 +994,46 @@ describe("Ekklesia API module runtime startup and shutdown", () => {
|
||||
return { poolEnd };
|
||||
}
|
||||
|
||||
it("fails on missing display-host assets before importing module code and still closes startup resources", async () => {
|
||||
const modulesDirectory = await createTemporaryModulesDirectory();
|
||||
const recorder = createEventRecorder();
|
||||
await createWorkingFixtureModule(modulesDirectory, "announcements", recorder, {
|
||||
withDisplay: true,
|
||||
});
|
||||
const { poolEnd } = mockPoolAndAuth();
|
||||
loadDisplayHostAssetsMock.mockRejectedValueOnce(new DisplayHostAssetError());
|
||||
|
||||
const failure = await startApi(validConfig(), {
|
||||
modulesDirectoryOverride: modulesDirectory,
|
||||
}).catch((error: unknown) => error);
|
||||
|
||||
expect(failure).toMatchObject({
|
||||
code: "DISPLAY_HOST_ASSET_UNAVAILABLE",
|
||||
message: "The display host browser assets are unavailable.",
|
||||
name: "DisplayHostAssetError",
|
||||
});
|
||||
expect((failure as Error).stack).toBe(
|
||||
"DisplayHostAssetError: The display host browser assets are unavailable.",
|
||||
);
|
||||
expect(loadDisplayHostAssetsMock).toHaveBeenCalledTimes(1);
|
||||
expect(recorder.events).toEqual([]);
|
||||
expect(poolEnd).toHaveBeenCalledTimes(1);
|
||||
expect(serveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not read display-host assets when the eligible set declares no display", async () => {
|
||||
const modulesDirectory = await createTemporaryModulesDirectory();
|
||||
const recorder = createEventRecorder();
|
||||
await createWorkingFixtureModule(modulesDirectory, "calendar", recorder);
|
||||
mockPoolAndAuth();
|
||||
mockSuccessfulServe();
|
||||
|
||||
const running = await startApi(validConfig(), { modulesDirectoryOverride: modulesDirectory });
|
||||
|
||||
expect(loadDisplayHostAssetsMock).not.toHaveBeenCalled();
|
||||
await running.shutdown();
|
||||
});
|
||||
|
||||
it("starts the module runtime from the exact discovered inventory permission composition used, mounting routes in discovery order", async () => {
|
||||
const modulesDirectory = await createTemporaryModulesDirectory();
|
||||
const recorder = createEventRecorder();
|
||||
|
||||
Reference in New Issue
Block a user