diff --git a/AGENTS.md b/AGENTS.md index 05048c70..8453be7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -283,11 +283,17 @@ bun run changeset **Before committing:** ```bash -bun run test # Run all tests +bun run test # Run all tests (root script — NOT `bun test`, which uses Bun's native runner) bun typecheck # Type check all packages bun lint # Run linting ``` +Host tests specifically use vitest via the workspace script: +```bash +bun run --cwd host test # NODE_ENV=production BOS_CONFIG_PATH=../bos.config.json vitest run +``` +Always use `bun run test` / `bun run --cwd host test` — never `bun test`, which invokes Bun's built-in runner and produces different (and misleading) results. + ## Common Patterns ### Authentication Check diff --git a/host/src/middleware/static-proxy.ts b/host/src/middleware/static-proxy.ts index 2cbb2b38..27a323ce 100644 --- a/host/src/middleware/static-proxy.ts +++ b/host/src/middleware/static-proxy.ts @@ -1,4 +1,12 @@ +import type { Context } from "hono"; import { proxy } from "hono/proxy"; +import type { AuthVariables } from "../lib/auth"; +import { STATIC_ASSET_PATTERN } from "../middleware/security"; +import type { RuntimeConfig } from "../services/config"; +import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "../services/tenant-runtime"; +import { logger } from "../utils/logger"; + +type HonoEnv = { Variables: AuthVariables }; export async function proxyRequest( req: Request, @@ -83,3 +91,34 @@ export async function proxyStaticAssetRequest(req: Request, targetBase: string): return response; } + +export function createStaticAssetProxyHandler(config: RuntimeConfig) { + return async (c: Context, next: () => Promise) => { + const { pathname } = new URL(c.req.url); + + if ( + pathname === "/" || + pathname === "/api" || + pathname.startsWith("/api/") || + pathname === "/health" + ) { + return next(); + } + + const lastSegment = pathname.split("/").pop() ?? ""; + if (!STATIC_ASSET_PATTERN.test(lastSegment)) { + return next(); + } + + try { + const runtime = await resolveRequestRuntime(config, c.req.raw, { + verification: "stale-while-revalidate", + }); + return await proxyStaticAssetRequest(c.req.raw, runtime.config.ui.url); + } catch (error) { + const { message, status } = getTenantRuntimeErrorResponse(error); + logger.error(`[Proxy Asset] ${c.req.method} ${c.req.path} — ${message}`); + return c.text(message, { status: status as 404 | 500 | 502 }); + } + }; +} diff --git a/host/src/program.ts b/host/src/program.ts index da9644d4..8c9b8c3d 100644 --- a/host/src/program.ts +++ b/host/src/program.ts @@ -8,128 +8,24 @@ import { Layer, ManagedRuntime, } from "every-plugin/effect"; -import { getBaseStyles, getHydrateScript, getThemeInitScript } from "everything-dev/ui/head"; import { type Context, Hono } from "hono"; import type { AuthVariables } from "./lib/auth"; -import { getCspStrict, SecurityMiddleware, STATIC_ASSET_PATTERN } from "./middleware/security"; -import { proxyStaticAssetRequest } from "./middleware/static-proxy"; +import { getCspStrict, SecurityMiddleware } from "./middleware/security"; +import { createStaticAssetProxyHandler } from "./middleware/static-proxy"; import { setupApiRoutes } from "./routes/api"; import type { HealthLoadingState } from "./routes/health"; -import { buildPluginContext, createSessionMiddleware, registerAuthHandler } from "./services/auth"; -import { type ClientRuntimeConfig, ConfigService, type RuntimeConfig } from "./services/config"; -import { loadRouterModule, resetFederationInstance } from "./services/federation.server"; +import { createSsrFallbackHandler } from "./routes/ssr"; +import { createSessionMiddleware, registerAuthHandler } from "./services/auth"; +import { ConfigService, type RuntimeConfig } from "./services/config"; +import { resetFederationInstance } from "./services/federation.server"; import { startIntegrityMonitor } from "./services/integrity-monitor"; import { closeMcpServer } from "./services/mcp"; -import { createPluginsClient, type PluginResult, PluginsService } from "./services/plugins"; -import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "./services/tenant-runtime"; -import type { RouterModule, RuntimePlugin } from "./types"; +import { PluginsService } from "./services/plugins"; import { extractErrorDetails } from "./utils/errors"; import { logger } from "./utils/logger"; -import { normalizeUrl } from "./utils/normalize"; type HonoEnv = { Variables: AuthVariables }; -type ActiveRuntimeState = NonNullable; - -type RuntimeClientConfig = ClientRuntimeConfig & { runtime?: ActiveRuntimeState }; - -function getFallbackGatewayId(config: RuntimeConfig) { - if (config.domain) { - return config.domain; - } - - return normalizeUrl(config.host?.url)?.replace(/^https?:\/\//, "") ?? "runtime"; -} - -function resolveActiveRuntime(config: RuntimeConfig, request: Request) { - const url = new URL(request.url); - const fallbackGatewayId = getFallbackGatewayId(config); - return { - accountId: config.account, - gatewayId: fallbackGatewayId, - runtimeBasePath: "/", - title: config.title ?? config.account, - description: config.description ?? null, - hostUrl: url.origin, - } satisfies ActiveRuntimeState; -} - -function buildRuntimeClientConfig( - config: RuntimeConfig, - request: Request, - activeRuntime: ActiveRuntimeState, - plugins: PluginResult, -): RuntimeClientConfig { - const requestUrl = new URL(request.url); - const uiConfig = config.ui; - - if (!uiConfig) { - throw new Error("UI config is required to build the runtime client config"); - } - - return { - env: config.env, - account: activeRuntime.accountId, - networkId: config.account.endsWith(".testnet") ? "testnet" : "mainnet", - hostUrl: requestUrl.origin, - assetsUrl: uiConfig.url, - apiBase: "/api", - rpcBase: "/api/rpc", - authAvailable: plugins.auth !== null, - repository: config.repository, - ui: { - name: uiConfig.name, - url: uiConfig.url, - entry: uiConfig.entry, - integrity: uiConfig.integrity, - }, - api: config.api - ? { - name: config.api.name, - url: config.api.url, - entry: config.api.entry, - integrity: config.api.integrity, - ...(config.api.variables ? { variables: config.api.variables } : {}), - } - : undefined, - auth: config.auth - ? { - name: config.auth.name, - url: config.auth.url, - entry: config.auth.entry, - integrity: config.auth.integrity, - ...(config.auth.variables ? { variables: config.auth.variables } : {}), - } - : undefined, - plugins: Object.fromEntries( - (Object.entries(config.plugins ?? {}) as Array<[string, RuntimePlugin]>).map( - ([key, plugin]) => [ - key, - { - name: plugin.name, - url: plugin.url, - entry: plugin.entry, - integrity: plugin.integrity, - ...(plugin.variables ? { variables: plugin.variables } : {}), - ...(plugin.ui - ? { - ui: { - name: plugin.ui.name, - url: plugin.ui.url, - entry: plugin.ui.entry, - source: plugin.ui.source, - integrity: plugin.ui.integrity, - }, - } - : {}), - }, - ], - ), - ), - runtime: activeRuntime, - } as RuntimeClientConfig; -} - export const createStartServer = (onReady?: () => void) => Effect.gen(function* () { const port = Number(process.env.PORT) || 3000; @@ -171,73 +67,7 @@ export const createStartServer = (onReady?: () => void) => ssrEnabled: Boolean(uiConfig.ssrUrl), }; - const renderClientShell = ( - ctx: Context, - runtimeSourceConfig: RuntimeConfig, - runtimeConfig: ClientRuntimeConfig, - error?: Error | null, - ) => { - const nonce = CSP_STRICT ? ctx.get("secureHeadersNonce") : undefined; - const uiIntegrity = runtimeSourceConfig.ui.integrity; - const assetsUrl = runtimeConfig.assetsUrl.replace(/\/$/, ""); - const nonceAttr = nonce ? ` nonce="${nonce}"` : ""; - const sriAttr = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : ""; - const uiVersion = uiIntegrity ? `?v=${encodeURIComponent(uiIntegrity)}` : ""; - - const baseStyles = ` - ${getBaseStyles()} - .shell { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; } - .fade { animation: fadeIn 0.3s ease-in; } - @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } - .error { color: #fca5a5; } - `.trim(); - - const themeScript = `${(getThemeInitScript() as { children?: string }).children ?? ""}`; - - const shellBody = `
${ - error - ? `

SSR unavailable, showing client app.

${error.message}

` - : `

Loading...

` - }
`; - - const title = - runtimeConfig.runtime?.title ?? runtimeSourceConfig.title ?? runtimeSourceConfig.account; - const hydrateScript = - ( - getHydrateScript( - runtimeConfig as Partial, - undefined, - undefined, - nonce, - ) as { children?: string } - ).children ?? ""; - - return ctx.html( - ` - - - - - ${title} - - - - ${themeScript} - - ${hydrateScript} - - ${shellBody} - `, - 200, - ); - }; - - const proxyUiAssetRequest = async (c: Context) => { - const runtime = await resolveRequestRuntime(config, c.req.raw, { - verification: "stale-while-revalidate", - }); - return await proxyStaticAssetRequest(c.req.raw, runtime.config.ui.url); - }; + app.on(["GET", "HEAD"], "*", createStaticAssetProxyHandler(config)); const sessionMiddleware = createSessionMiddleware(plugins); @@ -256,107 +86,9 @@ export const createStartServer = (onReady?: () => void) => setupApiRoutes(app, config, plugins, sessionMiddleware, loadingState), ); - app.on(["GET", "HEAD"], "*", async (c: Context, next) => { - const { pathname } = new URL(c.req.url); - - if ( - pathname === "/" || - pathname === "/api" || - pathname.startsWith("/api/") || - pathname === "/health" - ) { - return next(); - } - - const lastSegment = pathname.split("/").pop() ?? ""; - if (!STATIC_ASSET_PATTERN.test(lastSegment)) { - return next(); - } - - try { - return await proxyUiAssetRequest(c); - } catch (error) { - const { message, status } = getTenantRuntimeErrorResponse(error); - logger.error(`[Proxy Asset] ${c.req.method} ${c.req.path} — ${message}`); - return c.text(message, { status: status as 404 | 500 | 502 }); - } - }); - app.use("/*", sessionMiddleware); - app.get("*", async (c: Context) => { - if (c.req.path === "/api" || c.req.path.startsWith("/api/")) { - return c.notFound(); - } - - let resolvedRuntime: Awaited>; - try { - resolvedRuntime = await resolveRequestRuntime(config, c.req.raw, { - verification: "blocking", - }); - } catch (error) { - const { message, status } = getTenantRuntimeErrorResponse(error); - logger.error(`[SSR] ${c.req.method} ${c.req.path} — ${message}`); - return c.text(message, { status: status as 404 | 500 | 502 }); - } - - const effectiveConfig = resolvedRuntime.config; - const activeRuntime = await resolveActiveRuntime(effectiveConfig, c.req.raw); - const nonce = CSP_STRICT ? c.get("secureHeadersNonce") : undefined; - const runtimeConfig = buildRuntimeClientConfig( - effectiveConfig, - c.req.raw, - activeRuntime, - plugins, - ); - - let ssrRouterModule: RouterModule | null = null; - let moduleLoadError: Error | null = null; - - if (effectiveConfig.ui.ssrUrl) { - const result = await Effect.runPromise( - loadRouterModule(effectiveConfig).pipe(Effect.either), - ); - if (result._tag === "Right") { - ssrRouterModule = result.right; - } else { - moduleLoadError = result.left; - logger.error("[SSR] Failed to load Router module:", moduleLoadError); - } - } - - if (ssrRouterModule && effectiveConfig.ui.ssrUrl) { - try { - const pluginContext = buildPluginContext(c); - const ssrApiClient = createPluginsClient(plugins, pluginContext); - - const render = () => - ssrRouterModule.renderToStream(c.req.raw, { - session: c.get("session") ? { session: c.get("session"), user: c.get("user") } : null, - basepath: runtimeConfig.runtime?.runtimeBasePath, - runtimeConfig, - apiClient: ssrApiClient, - cspNonce: nonce, - }); - - const result = await render(); - const responseHeaders = new Headers(result?.headers); - const cspHeader = c.res.headers.get("Content-Security-Policy"); - if (cspHeader) { - responseHeaders.set("Content-Security-Policy", cspHeader); - } - return new Response(result?.stream, { - status: result?.statusCode, - headers: responseHeaders, - }); - } catch (error) { - logger.error("[SSR] Streaming error:", error); - moduleLoadError = error as Error; - } - } - - return renderClientShell(c, effectiveConfig, runtimeConfig, moduleLoadError); - }); + app.get("*", createSsrFallbackHandler(config, plugins, CSP_STRICT)); const startHttpServer = () => { const hostname = process.env.HOST || "0.0.0.0"; diff --git a/host/src/routes/html.ts b/host/src/routes/html.ts new file mode 100644 index 00000000..a1b82347 --- /dev/null +++ b/host/src/routes/html.ts @@ -0,0 +1,67 @@ +import { getBaseStyles, getHydrateScript, getThemeInitScript } from "everything-dev/ui/head"; +import type { Context } from "hono"; +import type { AuthVariables } from "../lib/auth"; +import type { ClientRuntimeConfig, RuntimeConfig } from "../services/config"; + +type HonoEnv = { Variables: AuthVariables }; + +export function renderClientShell( + ctx: Context, + nonce: string | undefined, + runtimeSourceConfig: RuntimeConfig, + runtimeConfig: ClientRuntimeConfig, + error?: Error | null, +) { + const uiIntegrity = runtimeSourceConfig.ui.integrity; + const assetsUrl = runtimeConfig.assetsUrl.replace(/\/$/, ""); + const nonceAttr = nonce ? ` nonce="${nonce}"` : ""; + const sriAttr = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : ""; + const uiVersion = uiIntegrity ? `?v=${encodeURIComponent(uiIntegrity)}` : ""; + + const baseStyles = ` + ${getBaseStyles()} + .shell { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; } + .fade { animation: fadeIn 0.3s ease-in; } + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + .error { color: #fca5a5; } + `.trim(); + + const themeScript = `${(getThemeInitScript() as { children?: string }).children ?? ""}`; + + const shellBody = `
${ + error + ? `

SSR unavailable, showing client app.

${error.message}

` + : `

Loading...

` + }
`; + + const title = + runtimeConfig.runtime?.title ?? runtimeSourceConfig.title ?? runtimeSourceConfig.account; + const hydrateScript = + ( + getHydrateScript( + runtimeConfig as Partial, + undefined, + undefined, + nonce, + ) as { children?: string } + ).children ?? ""; + + return ctx.html( + ` + + + + + ${title} + + + + ${themeScript} + + ${hydrateScript} + + ${shellBody} + `, + 200, + ); +} diff --git a/host/src/routes/ssr.ts b/host/src/routes/ssr.ts new file mode 100644 index 00000000..4512d4ca --- /dev/null +++ b/host/src/routes/ssr.ts @@ -0,0 +1,95 @@ +import { Effect } from "every-plugin/effect"; +import type { Context } from "hono"; +import type { AuthVariables } from "../lib/auth"; +import { buildPluginContext } from "../services/auth"; +import { + buildRuntimeClientConfig, + type RuntimeConfig, + resolveActiveRuntime, +} from "../services/config"; +import { loadRouterModule } from "../services/federation.server"; +import { createPluginsClient, type PluginResult } from "../services/plugins"; +import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "../services/tenant-runtime"; +import type { RouterModule } from "../types"; +import { logger } from "../utils/logger"; +import { renderClientShell } from "./html"; + +type HonoEnv = { Variables: AuthVariables }; + +export function createSsrFallbackHandler( + config: RuntimeConfig, + plugins: PluginResult, + CSP_STRICT: boolean, +) { + return async (c: Context) => { + if (c.req.path === "/api" || c.req.path.startsWith("/api/")) { + return c.notFound(); + } + + let resolvedRuntime: Awaited>; + try { + resolvedRuntime = await resolveRequestRuntime(config, c.req.raw, { + verification: "blocking", + }); + } catch (error) { + const { message, status } = getTenantRuntimeErrorResponse(error); + logger.error(`[SSR] ${c.req.method} ${c.req.path} — ${message}`); + return c.text(message, { status: status as 404 | 500 | 502 }); + } + + const effectiveConfig = resolvedRuntime.config; + const activeRuntime = resolveActiveRuntime(effectiveConfig, c.req.raw); + const nonce = CSP_STRICT ? c.get("secureHeadersNonce") : undefined; + const runtimeConfig = buildRuntimeClientConfig( + effectiveConfig, + c.req.raw, + activeRuntime, + plugins.auth !== null, + ); + + let ssrRouterModule: RouterModule | null = null; + let moduleLoadError: Error | null = null; + + if (effectiveConfig.ui.ssrUrl) { + const result = await Effect.runPromise(loadRouterModule(effectiveConfig).pipe(Effect.either)); + if (result._tag === "Right") { + ssrRouterModule = result.right; + } else { + moduleLoadError = result.left; + logger.error("[SSR] Failed to load Router module:", moduleLoadError); + } + } + + if (ssrRouterModule && effectiveConfig.ui.ssrUrl) { + try { + const pluginContext = buildPluginContext(c); + const ssrApiClient = createPluginsClient(plugins, pluginContext); + + const render = () => + ssrRouterModule.renderToStream(c.req.raw, { + session: c.get("session") ? { session: c.get("session"), user: c.get("user") } : null, + basepath: runtimeConfig.runtime?.runtimeBasePath, + runtimeConfig, + apiClient: ssrApiClient, + cspNonce: nonce, + }); + + const result = await render(); + const responseHeaders = new Headers(result?.headers); + const cspHeader = c.res.headers.get("Content-Security-Policy"); + if (cspHeader) { + responseHeaders.set("Content-Security-Policy", cspHeader); + } + return new Response(result?.stream, { + status: result?.statusCode, + headers: responseHeaders, + }); + } catch (error) { + logger.error("[SSR] Streaming error:", error); + moduleLoadError = error as Error; + } + } + + return renderClientShell(c, nonce, effectiveConfig, runtimeConfig, moduleLoadError); + }; +} diff --git a/host/src/services/config.ts b/host/src/services/config.ts index 34243d53..59826803 100644 --- a/host/src/services/config.ts +++ b/host/src/services/config.ts @@ -5,6 +5,8 @@ import type { SharedConfig, SourceMode, } from "everything-dev/types"; +import type { RuntimePlugin } from "../types"; +import { normalizeUrl } from "../utils/normalize"; export type { ClientRuntimeConfig, RuntimeConfig, SharedConfig, SourceMode }; @@ -20,3 +22,103 @@ export function readCorsOrigins(): Effect.Effect { Effect.withConfigProvider(ConfigProvider.fromEnv()), ); } + +export type ActiveRuntimeState = NonNullable; + +export type RuntimeClientConfig = ClientRuntimeConfig & { runtime?: ActiveRuntimeState }; + +function getFallbackGatewayId(config: RuntimeConfig) { + if (config.domain) { + return config.domain; + } + return normalizeUrl(config.host?.url)?.replace(/^https?:\/\//, "") ?? "runtime"; +} + +export function resolveActiveRuntime(config: RuntimeConfig, request: Request) { + const url = new URL(request.url); + const fallbackGatewayId = getFallbackGatewayId(config); + return { + accountId: config.account, + gatewayId: fallbackGatewayId, + runtimeBasePath: "/", + title: config.title ?? config.account, + description: config.description ?? null, + hostUrl: url.origin, + } satisfies ActiveRuntimeState; +} + +export function buildRuntimeClientConfig( + config: RuntimeConfig, + request: Request, + activeRuntime: ActiveRuntimeState, + authAvailable: boolean, +): RuntimeClientConfig { + const requestUrl = new URL(request.url); + const uiConfig = config.ui; + + if (!uiConfig) { + throw new Error("UI config is required to build the runtime client config"); + } + + return { + env: config.env, + account: activeRuntime.accountId, + networkId: config.account.endsWith(".testnet") ? "testnet" : "mainnet", + hostUrl: requestUrl.origin, + assetsUrl: uiConfig.url, + apiBase: "/api", + rpcBase: "/api/rpc", + authAvailable, + repository: config.repository, + ui: { + name: uiConfig.name, + url: uiConfig.url, + entry: uiConfig.entry, + integrity: uiConfig.integrity, + }, + api: config.api + ? { + name: config.api.name, + url: config.api.url, + entry: config.api.entry, + integrity: config.api.integrity, + ...(config.api.variables ? { variables: config.api.variables } : {}), + } + : undefined, + auth: config.auth + ? { + name: config.auth.name, + url: config.auth.url, + entry: config.auth.entry, + integrity: config.auth.integrity, + ...(config.auth.variables ? { variables: config.auth.variables } : {}), + } + : undefined, + plugins: Object.fromEntries( + (Object.entries(config.plugins ?? {}) as Array<[string, RuntimePlugin]>).map( + ([key, plugin]) => [ + key, + { + name: plugin.name, + url: plugin.url, + entry: plugin.entry, + integrity: plugin.integrity, + ...(plugin.variables ? { variables: plugin.variables } : {}), + ...(plugin.ui + ? { + ui: { + name: plugin.ui.name, + url: plugin.ui.url, + entry: plugin.ui.entry, + source: plugin.ui.source, + integrity: plugin.ui.integrity, + }, + } + : {}), + }, + ], + ), + ), + runtime: activeRuntime, + } as RuntimeClientConfig; +} diff --git a/host/tests/e2e/runtime-remote.spec.ts b/host/tests/e2e/runtime-remote.spec.ts index 70fd99e1..05aac168 100644 --- a/host/tests/e2e/runtime-remote.spec.ts +++ b/host/tests/e2e/runtime-remote.spec.ts @@ -148,7 +148,15 @@ for (const scenario of scenarios) { () => performance.getEntriesByType("navigation").length, ); - await expect(page.getByRole("link", { name: /^Skill$/ })).toBeVisible(); + try { + await expect(page.getByRole("link", { name: /^Skill$/ })).toBeVisible(); + } catch (e) { + const html = await page.content(); + console.error("[Diagnostic] Page HTML (first 3000 chars):", html.slice(0, 3000)); + console.error("[Diagnostic] Page errors:", JSON.stringify(pageErrors)); + console.error("[Diagnostic] Console errors:", JSON.stringify(consoleErrors)); + throw e; + } await page.getByRole("link", { name: /^Skill$/ }).dispatchEvent("click"); await page.waitForURL(/\/skill$/); @@ -176,7 +184,10 @@ for (const scenario of scenarios) { document.documentElement.classList.contains("dark"), ); - await page.getByRole("button", { name: "Toggle theme" }).click(); + await page + .getByRole("button", { name: /Switch to .* theme/ }) + .first() + .click(); await expect .poll(async () => diff --git a/host/tests/helpers/json-proxy-target.ts b/host/tests/helpers/json-proxy-target.ts index 83975486..50137ccf 100644 --- a/host/tests/helpers/json-proxy-target.ts +++ b/host/tests/helpers/json-proxy-target.ts @@ -23,6 +23,20 @@ export async function startJsonProxyTarget(): Promise { return; } + if (req.url?.startsWith("/api/auth/")) { + res.statusCode = 200; + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify(null)); + return; + } + + if (req.method === "POST" && req.url?.startsWith("/api/rpc/")) { + res.statusCode = 200; + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ json: null, meta: [] })); + return; + } + res.statusCode = 404; res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ error: "Not Found" })); diff --git a/host/tests/helpers/runtime-config.ts b/host/tests/helpers/runtime-config.ts index 55b7110c..9abd6774 100644 --- a/host/tests/helpers/runtime-config.ts +++ b/host/tests/helpers/runtime-config.ts @@ -25,6 +25,21 @@ export async function loadTestRuntimeConfig(): Promise { return config; } +export function createMockSession() { + return { + user: { + id: "test-user-id", + name: "Test User", + email: "test@everything.dev", + role: "user", + }, + session: { + id: "test-session-id", + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + }; +} + export function createMockAuthClient(): AuthClient { const noOp = (): Promise<{ data: null; error: null }> => Promise.resolve({ data: null, error: null }); @@ -111,11 +126,12 @@ export function buildTestRenderOptions( config: RuntimeConfig, apiClient: ApiClient, authClient?: AuthClient, + session = createMockSession(), ): RenderOptionsWithApi { return { runtimeConfig: buildTestClientRuntimeConfig(config), apiClient, - session: null, + session, authClient, } satisfies RenderOptionsWithApi; } diff --git a/host/tests/integration/csp-nonce.test.ts b/host/tests/integration/csp-nonce.test.ts index dcad904b..7dfe0e29 100644 --- a/host/tests/integration/csp-nonce.test.ts +++ b/host/tests/integration/csp-nonce.test.ts @@ -6,6 +6,7 @@ import { loadBundledRouterModule } from "../helpers/bundled-ssr-module"; import { buildTestClientRuntimeConfig, createMockAuthClient, + createMockSession, loadTestRuntimeConfig, } from "../helpers/runtime-config"; @@ -24,6 +25,7 @@ async function consumeStream(stream: ReadableStream): Promise { const mockApiClient = createTestApiClient({}); const mockAuthClient = createMockAuthClient(); +const mockSession = createMockSession(); describe("CSP Nonce Regression Tests", () => { let routerModule: RouterModule; @@ -49,7 +51,7 @@ describe("CSP Nonce Regression Tests", () => { const renderOptions: RenderOptionsWithApi = { runtimeConfig: buildTestClientRuntimeConfig(config), apiClient: mockApiClient, - session: null, + session: mockSession, authClient: mockAuthClient, cspNonce: testNonce, }; @@ -77,7 +79,7 @@ describe("CSP Nonce Regression Tests", () => { const renderOptions: RenderOptionsWithApi = { runtimeConfig: buildTestClientRuntimeConfig(config), apiClient: mockApiClient, - session: null, + session: mockSession, authClient: mockAuthClient, cspNonce: testNonce, }; @@ -105,7 +107,7 @@ describe("CSP Nonce Regression Tests", () => { const renderOptions: RenderOptionsWithApi = { runtimeConfig: buildTestClientRuntimeConfig(config), apiClient: mockApiClient, - session: null, + session: mockSession, authClient: mockAuthClient, cspNonce: testNonce, }; @@ -126,7 +128,7 @@ describe("CSP Nonce Regression Tests", () => { const renderOptions: RenderOptionsWithApi = { runtimeConfig: buildTestClientRuntimeConfig(config), apiClient: mockApiClient, - session: null, + session: mockSession, authClient: mockAuthClient, }; @@ -161,7 +163,7 @@ describe("CSP Nonce Regression Tests", () => { rpcBase: "/rpc", }, apiClient: mockApiClient, - session: null, + session: mockSession, authClient: mockAuthClient, cspNonce: "typed-nonce-without-cast", }; @@ -180,7 +182,7 @@ describe("CSP Nonce Regression Tests", () => { rpcBase: "/rpc", }, apiClient: mockApiClient, - session: null, + session: mockSession, authClient: mockAuthClient, }; diff --git a/host/tests/integration/ssr-bundled-runtime.test.ts b/host/tests/integration/ssr-bundled-runtime.test.ts index 1f16465c..d26b3177 100644 --- a/host/tests/integration/ssr-bundled-runtime.test.ts +++ b/host/tests/integration/ssr-bundled-runtime.test.ts @@ -57,7 +57,7 @@ describe("bundled host SSR runtime", () => { const html = await response.text(); expect(response.status).toBe(200); - expect(html).toContain('