feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -7,6 +7,13 @@ import { resolveI18n } from "@wrnexus/i18n";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
function conditionalBranches(html: string) {
|
||||
const reference = /data-wrn-if="(@[A-Za-z0-9-]+:[0-9]+)"/.exec(html)?.[1];
|
||||
expect(reference).toBeTruthy();
|
||||
const registry = /data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)?.[1];
|
||||
expect(registry).toBeTruthy();
|
||||
return JSON.parse(registry!)[Number(reference!.split(":")[1])];
|
||||
}
|
||||
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||
|
||||
test("a component nested in another component's slot keeps its boundary", async () => {
|
||||
@@ -83,9 +90,7 @@ test("a component in an initially hidden client branch is server-prepared for in
|
||||
upgrade: () => false,
|
||||
});
|
||||
const html = await response!.text();
|
||||
const encoded = /data-wrn-if="([A-Za-z0-9+/=]+)"/.exec(html)?.[1];
|
||||
expect(encoded).toBeTruthy();
|
||||
const branches = JSON.parse(Buffer.from(encoded!, "base64").toString("utf8"));
|
||||
const branches = conditionalBranches(html);
|
||||
expect(branches[0].body).toContain('<input value="ready">');
|
||||
expect(branches[0].body).not.toContain("data-component=");
|
||||
});
|
||||
@@ -135,8 +140,7 @@ test("typed components in client branches use defaults until reactive props are
|
||||
});
|
||||
const html = await response!.text();
|
||||
expect(observed).toEqual({});
|
||||
const encoded = /data-wrn-if="([A-Za-z0-9+/=]+)"/.exec(html)?.[1];
|
||||
const branches = JSON.parse(Buffer.from(encoded!, "base64").toString("utf8"));
|
||||
const branches = conditionalBranches(html);
|
||||
expect(branches[0].body).toContain("<section");
|
||||
expect(branches[0].body).toContain("data-wrn-prop-bind-0");
|
||||
expect(branches[0].body).not.toContain("data-component=");
|
||||
@@ -154,16 +158,21 @@ test("SSR carries a parent prop binding onto the rendered child scope", async ()
|
||||
"component Child { props { value = 0 } view { <b>{value}</b> } }\n",
|
||||
);
|
||||
const marker = "["value","{count}"]";
|
||||
const locals = Buffer.from(JSON.stringify({ field: { id: "answer" } })).toString("base64");
|
||||
const childState = Buffer.from(JSON.stringify({ value: 1 })).toString("base64");
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Child.wrn"
|
||||
? { render: () => '<div data-scope="value: 1" data-wrn-scope="e30="><b>1</b></div>' }
|
||||
? {
|
||||
render: () =>
|
||||
`<div data-scope="value: 1" data-wrn-scope="${childState}"><b>1</b></div>`,
|
||||
}
|
||||
: {
|
||||
default: () =>
|
||||
`<div data-component="Child" value="1" data-wrn-prop-bind-0="${marker}"></div>`,
|
||||
`<div data-component="Child" value="1" data-wrn-prop-bind-0="${marker}" data-wrn-loop-locals="${locals}"></div>`,
|
||||
},
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
@@ -173,6 +182,17 @@ test("SSR carries a parent prop binding onto the rendered child scope", async ()
|
||||
});
|
||||
const html = await response!.text();
|
||||
expect(html).toContain(`data-wrn-prop-bind-0="${marker}"`);
|
||||
const child = /<div data-scope=""[^>]*>/.exec(html)?.[0];
|
||||
expect(child).toBeTruthy();
|
||||
expect(child).toContain(`data-wrn-prop-bind-0="${marker}"`);
|
||||
const registry = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
const metadata = (attribute: string) => {
|
||||
const reference = new RegExp(`${attribute}="@[^"]+:([0-9]+)"`).exec(child!)?.[1];
|
||||
expect(reference).toBeDefined();
|
||||
return registry[Number(reference)].value;
|
||||
};
|
||||
expect(metadata("data-wrn-loop-locals")).toEqual({ field: { id: "answer" } });
|
||||
expect(metadata("data-wrn-scope")).toEqual({ value: 1 });
|
||||
expect(html).not.toContain('data-component="Child"');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
import { createHandlers } from "../src/runtime.ts";
|
||||
import { encodeControlTemplate } from "@wrnexus/core";
|
||||
|
||||
test("nested encoded conditional and empty-list templates prepare shared controls", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrn-nested-controls-"));
|
||||
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64");
|
||||
try {
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "components"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default () => '';");
|
||||
writeFileSync(
|
||||
join(app, "components/Button.wrn"),
|
||||
"component Button { view { <button>Review</button> } }",
|
||||
);
|
||||
const mount = '<div data-component="Button"></div>';
|
||||
const inner = encode([{ cond: "review.active", body: mount }]);
|
||||
const list = encode({ list: "items", item: "item", body: "<p>Item</p>", empty: mount });
|
||||
const outer = encode([
|
||||
{
|
||||
cond: "review",
|
||||
body: `<template data-wrn-if="${inner}"></template><template data-wrn-control-end></template><template data-wrn-each="${list}"></template><template data-wrn-control-end></template>`,
|
||||
},
|
||||
]);
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Button.wrn"
|
||||
? { render: () => "<button>Review</button>" }
|
||||
: {
|
||||
default: () =>
|
||||
`<template data-wrn-if="${encodeControlTemplate(JSON.parse(Buffer.from(outer, "base64").toString()))}"></template><template data-wrn-control-end></template>`,
|
||||
},
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
});
|
||||
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
}))!.text();
|
||||
const registry = /data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html);
|
||||
expect(registry).not.toBeNull();
|
||||
const definitions = JSON.parse(registry![1]!);
|
||||
const decode = (markup: string, kind: string) => {
|
||||
const reference = new RegExp(`data-wrn-${kind}="(@[A-Za-z0-9-]+:[0-9]+)"`).exec(markup)![1]!;
|
||||
return definitions[Number(reference.split(":")[1])];
|
||||
};
|
||||
const branch = definitions[0][0].body;
|
||||
expect(decode(branch, "if")[0].body).toBe("<button>Review</button>");
|
||||
expect(decode(branch, "each").empty).toBe("<button>Review</button>");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("shared branch references are prepared once across a branching control graph", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrn-shared-controls-"));
|
||||
try {
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default () => ''; ");
|
||||
let reads = 0;
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async () => ({
|
||||
default: () => {
|
||||
let body = "<p>Leaf</p>";
|
||||
for (let depth = 0; depth < 12; depth++) {
|
||||
let storedBody = body;
|
||||
const entry = {
|
||||
cond: "open",
|
||||
get body() {
|
||||
reads++;
|
||||
return storedBody;
|
||||
},
|
||||
set body(value: string) {
|
||||
storedBody = value;
|
||||
},
|
||||
};
|
||||
const reference = encodeControlTemplate([entry]);
|
||||
body =
|
||||
`<template data-wrn-if="${reference}"></template><template data-wrn-control-end></template>`.repeat(
|
||||
2,
|
||||
);
|
||||
}
|
||||
return body;
|
||||
},
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
});
|
||||
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
}))!.text();
|
||||
expect(reads).toBeLessThan(200);
|
||||
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
expect(definitions).toHaveLength(12);
|
||||
expect(definitions[0][0].body).toBe("<p>Leaf</p>");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("deep inline loop definitions stay bounded after shared component preparation", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrn-loop-size-"));
|
||||
try {
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "components"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default () => ''; ");
|
||||
writeFileSync(join(app, "components/Button.wrn"), "component Button { view { <button /> } }");
|
||||
let body = '<div data-component="Button"></div>';
|
||||
for (let depth = 0; depth < 12; depth++) {
|
||||
const encoded = Buffer.from(
|
||||
JSON.stringify({ list: "items", item: "item", body, empty: "" }),
|
||||
).toString("base64");
|
||||
body = `<template data-wrn-each="${encoded}"></template><template data-wrn-control-end></template>`;
|
||||
}
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Button.wrn"
|
||||
? { render: () => `<button>${"Review".repeat(200)}</button>` }
|
||||
: { default: () => body },
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
});
|
||||
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
}))!.text();
|
||||
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
expect(definitions).toHaveLength(12);
|
||||
expect(html.length).toBeLessThan(12000);
|
||||
expect(
|
||||
definitions.filter((definition: { body: string }) => definition.body.includes("<button>")),
|
||||
).toHaveLength(1);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("inert control nesting does not consume the component composition budget", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrn-control-budget-"));
|
||||
try {
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "components"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default ()=>'';");
|
||||
writeFileSync(
|
||||
join(app, "components/Button.wrn"),
|
||||
"component Button { view { <button>Nested field</button> } }",
|
||||
);
|
||||
let body = '<div data-component="Button"></div>';
|
||||
for (let depth = 0; depth < 20; depth++) {
|
||||
const encoded = Buffer.from(JSON.stringify([{ cond: "open", body }])).toString("base64");
|
||||
body = `<template data-wrn-if="${encoded}"></template><template data-wrn-control-end></template>`;
|
||||
}
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Button.wrn"
|
||||
? { render: () => "<button>Nested field</button>" }
|
||||
: { default: () => body },
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
});
|
||||
const html = await (await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
}))!.text();
|
||||
const definitions = JSON.parse(/data-wrn-controls[^>]*>(.*?)<\/script>/s.exec(html)![1]!);
|
||||
expect(JSON.stringify(definitions)).toContain("<button>Nested field</button>");
|
||||
expect(JSON.stringify(definitions)).not.toContain("data-component");
|
||||
expect(html.length).toBeLessThan(20000);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -37,19 +37,19 @@ test("serves package assets with a safe content type", async () => {
|
||||
});
|
||||
|
||||
test("published JavaScript runtimes bundle relative chunks by default", async () => {
|
||||
const entry=fileURLToPath(new URL("./fixtures/plugin-runtime/entry.js",import.meta.url));
|
||||
const assets=mergePluginAssets([{id:"published",entry}],[]);
|
||||
const response=await servePluginAsset(assets,"/__wrnexus/assets/published.js","development");
|
||||
const entry = fileURLToPath(new URL("./fixtures/plugin-runtime/entry.js", import.meta.url));
|
||||
const assets = mergePluginAssets([{ id: "published", entry }], []);
|
||||
const response = await servePluginAsset(assets, "/__wrnexus/assets/published.js", "development");
|
||||
expect(response?.status).toBe(200);
|
||||
const source=await response!.text();
|
||||
const source = await response!.text();
|
||||
expect(source).toContain("published-runtime-chunk-loaded");
|
||||
expect(source).not.toMatch(/from\s+["']\.\/chunk\.js["']/);
|
||||
});
|
||||
|
||||
test("explicit bundle false preserves prebuilt runtime modules", async () => {
|
||||
const entry=fileURLToPath(new URL("./fixtures/plugin-runtime/entry.js",import.meta.url));
|
||||
const assets=mergePluginAssets([{id:"raw",entry,bundle:false}],[]);
|
||||
const response=await servePluginAsset(assets,"/__wrnexus/assets/raw.js","development");
|
||||
const entry = fileURLToPath(new URL("./fixtures/plugin-runtime/entry.js", import.meta.url));
|
||||
const assets = mergePluginAssets([{ id: "raw", entry, bundle: false }], []);
|
||||
const response = await servePluginAsset(assets, "/__wrnexus/assets/raw.js", "development");
|
||||
expect(await response!.text()).toContain('from "./chunk.js"');
|
||||
});
|
||||
|
||||
@@ -98,13 +98,13 @@ test("collectScripts ships zero framework JavaScript for static markup by defaul
|
||||
|
||||
test("collectScripts adds navigation in explicit client mode", () => {
|
||||
expect(collectScripts("<main>Server rendered</main>", [], { mode: "client" })).toEqual([
|
||||
"/__wrnexus/nav.js",
|
||||
expect.stringMatching(/^\/__wrnexus\/nav\.js\?v=[a-f0-9]{16}$/),
|
||||
]);
|
||||
});
|
||||
|
||||
test("collectScripts auto-adds navigation when a page already needs a browser runtime", () => {
|
||||
expect(collectScripts('<button data-scope="{}">Count</button>')).toEqual([
|
||||
"/__wrnexus/nav.js",
|
||||
expect.stringMatching(/^\/__wrnexus\/nav\.js\?v=[a-f0-9]{16}$/),
|
||||
"/__wrnexus/reactive.js",
|
||||
]);
|
||||
});
|
||||
@@ -39,6 +39,24 @@ const manifest: ProdManifest = {
|
||||
|
||||
const server = { upgrade: () => false } as never;
|
||||
|
||||
test("navigation runtime uses a content version and revalidates its stable URL", async () => {
|
||||
const { collectScripts } = await import("../src/script-selection.ts");
|
||||
const href = collectScripts("", [], { mode: "client" })[0] as string;
|
||||
expect(href).toMatch(/^\/__wrnexus\/nav\.js\?v=[a-f0-9]{16}$/);
|
||||
const handlers = createProductionHandlers(manifest, {});
|
||||
const versioned = (await handlers.fetch(
|
||||
new Request("http://localhost" + href),
|
||||
server,
|
||||
)) as Response;
|
||||
expect(versioned.headers.get("cache-control")).toBe("no-cache");
|
||||
expect(await versioned.text()).toContain("loadedNavigationScripts");
|
||||
const stable = (await handlers.fetch(
|
||||
new Request("http://localhost/__wrnexus/nav.js"),
|
||||
server,
|
||||
)) as Response;
|
||||
expect(stable.headers.get("cache-control")).toBe("no-cache");
|
||||
});
|
||||
|
||||
test("supervised production runtime injects reconnecting DOM-morph support", async () => {
|
||||
const handlers = createProductionHandlers(manifest, { developmentRuntime: true });
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), server);
|
||||
|
||||
@@ -2,52 +2,122 @@ import { expect, test } from "bun:test";
|
||||
import { defineRoom, type Middleware } from "@wrnexus/core";
|
||||
import { createProductionHandlers } from "../src/prod.ts";
|
||||
|
||||
const room = defineRoom({ security: { requireUser: true }, authorize: info => info.user === "signed-in-user" });
|
||||
const room = defineRoom({
|
||||
security: { requireUser: true },
|
||||
authorize: (info) => info.user === "signed-in-user",
|
||||
});
|
||||
const auth: Middleware = async (ctx, next) => {
|
||||
if (ctx.req.headers.get("cookie") === "test-session=valid") ctx.user = { id: "signed-in-user" };
|
||||
return next();
|
||||
};
|
||||
function handlers(middleware: Middleware[] = [auth]) {
|
||||
return createProductionHandlers({ pages: [], api: [], realtime: [{raw:"/realtime/updates",mod:{default:room}}], middleware, components:[], layouts:[] }, {});
|
||||
return createProductionHandlers(
|
||||
{
|
||||
pages: [],
|
||||
api: [],
|
||||
realtime: [{ raw: "/realtime/updates", mod: { default: room } }],
|
||||
middleware,
|
||||
components: [],
|
||||
layouts: [],
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
function request(query = "", cookie = "test-session=valid", origin = "https://account.example.test") {
|
||||
return new Request("https://account.example.test/realtime/updates"+query, { headers: {host:"account.example.test", origin, cookie, upgrade:"websocket"} });
|
||||
function request(
|
||||
query = "",
|
||||
cookie = "test-session=valid",
|
||||
origin = "https://account.example.test",
|
||||
) {
|
||||
return new Request("https://account.example.test/realtime/updates" + query, {
|
||||
headers: { host: "account.example.test", origin, cookie, upgrade: "websocket" },
|
||||
});
|
||||
}
|
||||
test("room upgrade receives middleware identity after post-processing completes", async () => {
|
||||
let finished = false;
|
||||
const post: Middleware = async (_, next) => { const res = await next(); finished = true; return new Response(res.body, { status:res.status, headers:res.headers }); };
|
||||
const post: Middleware = async (_, next) => {
|
||||
const res = await next();
|
||||
finished = true;
|
||||
return new Response(res.body, { status: res.status, headers: res.headers });
|
||||
};
|
||||
let upgraded = false;
|
||||
const response = await handlers([auth,post]).fetch(request(), { upgrade: (_, options) => {
|
||||
expect(finished).toBe(true);
|
||||
expect((options.data as any).meta.user).toBe("signed-in-user");
|
||||
expect(options.headers?.has("x-wrnexus-upgrade-permit")).toBe(false);
|
||||
upgraded = true; return true;
|
||||
} });
|
||||
expect(response).toBeUndefined(); expect(upgraded).toBe(true);
|
||||
const response = await handlers([auth, post]).fetch(request(), {
|
||||
upgrade: (_, options) => {
|
||||
expect(finished).toBe(true);
|
||||
expect((options.data as any).meta.user).toBe("signed-in-user");
|
||||
expect(options.headers?.has("x-wrnexus-upgrade-permit")).toBe(false);
|
||||
upgraded = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
expect(response).toBeUndefined();
|
||||
expect(upgraded).toBe(true);
|
||||
});
|
||||
test("query user cannot impersonate an authenticated room member", async () => {
|
||||
const response = await handlers().fetch(request("?user=signed-in-user", ""), {upgrade:()=>{throw Error("must not upgrade")}});
|
||||
const response = await handlers().fetch(request("?user=signed-in-user", ""), {
|
||||
upgrade: () => {
|
||||
throw Error("must not upgrade");
|
||||
},
|
||||
});
|
||||
expect(response?.status).toBe(403);
|
||||
});
|
||||
test("middleware rejection and cross-site origins never upgrade", async () => {
|
||||
const server = {upgrade:()=>{throw Error("must not upgrade")}};
|
||||
expect((await handlers([()=>new Response("denied",{status:401})]).fetch(request(),server))?.status).toBe(401);
|
||||
expect((await handlers().fetch(request("", "test-session=valid", "https://evil.example"),server))?.status).toBe(403);
|
||||
const server = {
|
||||
upgrade: () => {
|
||||
throw Error("must not upgrade");
|
||||
},
|
||||
};
|
||||
expect(
|
||||
(await handlers([() => new Response("denied", { status: 401 })]).fetch(request(), server))
|
||||
?.status,
|
||||
).toBe(401);
|
||||
expect(
|
||||
(await handlers().fetch(request("", "test-session=valid", "https://evil.example"), server))
|
||||
?.status,
|
||||
).toBe(403);
|
||||
});
|
||||
test("post-processing rejection overrides room authorization", async () => {
|
||||
const deny: Middleware = async (_,next)=>{await next();return new Response("denied",{status:403})};
|
||||
expect((await handlers([auth,deny]).fetch(request(),{upgrade:()=>{throw Error("must not upgrade")}}))?.status).toBe(403);
|
||||
const deny: Middleware = async (_, next) => {
|
||||
await next();
|
||||
return new Response("denied", { status: 403 });
|
||||
};
|
||||
expect(
|
||||
(
|
||||
await handlers([auth, deny]).fetch(request(), {
|
||||
upgrade: () => {
|
||||
throw Error("must not upgrade");
|
||||
},
|
||||
})
|
||||
)?.status,
|
||||
).toBe(403);
|
||||
});
|
||||
|
||||
test("Bun accepts a real socket with the middleware-authenticated identity", async () => {
|
||||
const app = handlers();
|
||||
const server = Bun.serve({ port:0, fetch:app.fetch, websocket:app.websocket });
|
||||
const server = Bun.serve({ port: 0, fetch: app.fetch, websocket: app.websocket });
|
||||
try {
|
||||
await new Promise<void>((resolve,reject)=>{
|
||||
const socket = new WebSocket(`ws://localhost:${server.port}/realtime/updates`, {headers:{cookie:"test-session=valid",origin:`http://localhost:${server.port}`}});
|
||||
const timer=setTimeout(()=>{socket.close();reject(Error("upgrade timed out"))},3000);
|
||||
socket.onopen=()=>{clearTimeout(timer);socket.close();resolve()};
|
||||
socket.onerror=()=>{clearTimeout(timer);reject(Error("upgrade failed"))};
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const Socket = WebSocket as unknown as {
|
||||
new (url: string, options?: import("bun").WebSocketOptions): WebSocket;
|
||||
};
|
||||
const socket = new Socket(`ws://localhost:${server.port}/realtime/updates`, {
|
||||
headers: { cookie: "test-session=valid", origin: `http://localhost:${server.port}` },
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
socket.close();
|
||||
reject(Error("upgrade timed out"));
|
||||
}, 3000);
|
||||
socket.onopen = () => {
|
||||
clearTimeout(timer);
|
||||
socket.close();
|
||||
resolve();
|
||||
};
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timer);
|
||||
reject(Error("upgrade failed"));
|
||||
};
|
||||
});
|
||||
} finally { server.stop(true); await app.shutdown(); }
|
||||
} finally {
|
||||
server.stop(true);
|
||||
await app.shutdown();
|
||||
}
|
||||
});
|
||||
@@ -8,6 +8,31 @@ function request(headers: Record<string, string> = {}): Request {
|
||||
});
|
||||
}
|
||||
|
||||
test("RPC CSRF rejects cross-site fetch metadata even with matching tokens", () => {
|
||||
expect(
|
||||
validateRpcCsrf(
|
||||
request({
|
||||
cookie: "wrn-csrf=token",
|
||||
"x-csrf-token": "token",
|
||||
"sec-fetch-site": "cross-site",
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("RPC CSRF rejects malformed cookie encoding without throwing", () => {
|
||||
for (const cookieName of ["wrn-csrf", "wrnexus_csrf"]) {
|
||||
expect(
|
||||
validateRpcCsrf(
|
||||
request({
|
||||
cookie: `${cookieName}=%E0%A4%A`,
|
||||
"x-csrf-token": "%E0%A4%A",
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("RPC CSRF requires a matching cookie and header from the same origin", () => {
|
||||
expect(validateRpcCsrf(request())).toBe(false);
|
||||
expect(validateRpcCsrf(request({ cookie: "wrn-csrf=token" }))).toBe(false);
|
||||
|
||||
@@ -10,7 +10,7 @@ test("component resolution preserves JSON user/permission props and drops unreso
|
||||
raw: "/",
|
||||
mod: {
|
||||
default: () =>
|
||||
'<div data-component="account-shell" identity="{"displayName":"Alice"}" permissions="{"read":true,"update":false}" preferences="{"theme":"dark"}" pending="{account.pending}"><p>Account content</p></div>',
|
||||
'<div data-component="account-shell" identity="{"displayName":"Alice"}" permissions="{"read":true,"update":false}" preferences="{"theme":"dark"}" pending="{account.pending}" value="{{order}}"><p>Account content</p></div>',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -44,4 +44,156 @@ test("component resolution preserves JSON user/permission props and drops unreso
|
||||
expect(JSON.parse(received.permissions!)).toEqual({ read: true, update: false });
|
||||
expect(JSON.parse(received.preferences!)).toEqual({ theme: "dark" });
|
||||
expect(received.pending).toBeUndefined();
|
||||
expect(received.value).toBe("{{order}}");
|
||||
});
|
||||
|
||||
test("recursive inert components reuse request-local control prototypes", async () => {
|
||||
const { encodeControlTemplate } = await import("@wrnexus/core");
|
||||
let renders = 0;
|
||||
const handlers = createProductionHandlers(
|
||||
{
|
||||
pages: [{ raw: "/", mod: { default: () => '<div data-component="Tree"></div>' } }],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middleware: [],
|
||||
layouts: [],
|
||||
components: [
|
||||
{
|
||||
name: "Tree",
|
||||
mod: {
|
||||
render: () => {
|
||||
renders++;
|
||||
const control = encodeControlTemplate([
|
||||
{ cond: "open", body: '<div data-component="Tree"></div>' },
|
||||
]);
|
||||
return `<div data-scope="open: false"><template data-wrn-if="${control}"></template><template data-wrn-control-end></template></div>`;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
for (let index = 0; index < 2; index++) {
|
||||
const before = renders;
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
expect(response?.status).toBe(200);
|
||||
const html = await response!.text();
|
||||
expect(renders - before).toBeLessThanOrEqual(3);
|
||||
expect(html.length).toBeLessThan(10000);
|
||||
expect(html).toContain("wrn-controls-");
|
||||
}
|
||||
});
|
||||
|
||||
test("recursive mixed prop bindings do not grow inert prototypes", async () => {
|
||||
const { encodeControlTemplate } = await import("@wrnexus/core");
|
||||
let renders = 0;
|
||||
const handlers = createProductionHandlers(
|
||||
{
|
||||
pages: [
|
||||
{ raw: "/", mod: { default: () => '<div data-component="Branch" prefix="live"></div>' } },
|
||||
],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middleware: [],
|
||||
layouts: [],
|
||||
components: [
|
||||
{
|
||||
name: "Branch",
|
||||
mod: {
|
||||
render: (props: Record<string, string>) => {
|
||||
renders++;
|
||||
const prefix = props.prefix || "";
|
||||
const binding = "["prefix","{prefix}-child"]";
|
||||
const body = `<div data-component="Branch" prefix="${prefix}-child" data-wrn-prop-bind-0="${binding}"></div>`;
|
||||
const control = encodeControlTemplate([{ cond: "open", body }]);
|
||||
return `<div data-scope="open: false, prefix: '${prefix}'"><p>${prefix}</p><template data-wrn-if="${control}"></template><template data-wrn-control-end></template></div>`;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), { upgrade: () => false });
|
||||
const html = await response!.text();
|
||||
expect(renders).toBeLessThanOrEqual(3);
|
||||
expect(html.length).toBeLessThan(10000);
|
||||
expect(html).toContain("<p>live</p>");
|
||||
});
|
||||
|
||||
test("loop-local bridge metadata does not multiply recursive prototypes", async () => {
|
||||
const { encodeControlTemplate } = await import("@wrnexus/core");
|
||||
let renders = 0;
|
||||
const handlers = createProductionHandlers(
|
||||
{
|
||||
pages: [{ raw: "/", mod: { default: () => '<div data-component="LoopTree"></div>' } }],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middleware: [],
|
||||
layouts: [],
|
||||
components: [
|
||||
{
|
||||
name: "LoopTree",
|
||||
mod: {
|
||||
render: () => {
|
||||
renders++;
|
||||
const locals = Buffer.from(JSON.stringify({ row: renders })).toString("base64");
|
||||
const control = encodeControlTemplate([
|
||||
{
|
||||
cond: "open",
|
||||
body: `<div data-component="LoopTree" data-wrn-loop-locals="${locals}"></div>`,
|
||||
},
|
||||
]);
|
||||
return `<div data-scope="open: false"><template data-wrn-if="${control}"></template><template data-wrn-control-end></template></div>`;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), { upgrade: () => false });
|
||||
expect(response?.status).toBe(200);
|
||||
expect(renders).toBeLessThanOrEqual(3);
|
||||
expect((await response!.text()).length).toBeLessThan(10000);
|
||||
});
|
||||
|
||||
test("inline compiler controls in recursive prototypes are hoisted once", async () => {
|
||||
let renders = 0;
|
||||
const encoded = Buffer.from(
|
||||
JSON.stringify([
|
||||
{
|
||||
cond: "open",
|
||||
body: '<div data-component="InlineTree"></div><div data-component="InlineTree"></div>',
|
||||
},
|
||||
]),
|
||||
).toString("base64");
|
||||
const handlers = createProductionHandlers(
|
||||
{
|
||||
pages: [{ raw: "/", mod: { default: () => '<div data-component="InlineTree"></div>' } }],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middleware: [],
|
||||
layouts: [],
|
||||
components: [
|
||||
{
|
||||
name: "InlineTree",
|
||||
mod: {
|
||||
render: () => {
|
||||
renders++;
|
||||
return `<div data-scope="open: false"><template data-wrn-if="${encoded}"></template><template data-wrn-control-end></template></div>`;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{},
|
||||
);
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), { upgrade: () => false });
|
||||
expect(response?.status).toBe(200);
|
||||
expect(renders).toBeLessThanOrEqual(3);
|
||||
expect((await response!.text()).length).toBeLessThan(10000);
|
||||
});
|
||||
Reference in new issue
Block a user