test: simplify control ui http coverage

This commit is contained in:
Peter Steinberger
2026-03-13 18:03:18 +00:00
parent 987c254eea
commit 91d4f5cd2f

View File

@@ -40,6 +40,25 @@ describe("handleControlUiHttpRequest", () => {
expect(params.end).toHaveBeenCalledWith("Not Found"); expect(params.end).toHaveBeenCalledWith("Not Found");
} }
function expectUnhandledRoutes(params: {
urls: string[];
method: "GET" | "POST";
rootPath: string;
basePath?: string;
expectationLabel: string;
}) {
for (const url of params.urls) {
const { handled, end } = runControlUiRequest({
url,
method: params.method,
rootPath: params.rootPath,
...(params.basePath ? { basePath: params.basePath } : {}),
});
expect(handled, `${params.expectationLabel}: ${url}`).toBe(false);
expect(end, `${params.expectationLabel}: ${url}`).not.toHaveBeenCalled();
}
}
function runControlUiRequest(params: { function runControlUiRequest(params: {
url: string; url: string;
method: "GET" | "HEAD" | "POST"; method: "GET" | "HEAD" | "POST";
@@ -147,53 +166,80 @@ describe("handleControlUiHttpRequest", () => {
}); });
}); });
it("serves bootstrap config JSON", async () => { it.each([
{
name: "at root",
url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH,
expectedBasePath: "",
assistantName: "</script><script>alert(1)//",
assistantAvatar: "</script>.png",
expectedAvatarUrl: "/avatar/main",
},
{
name: "under basePath",
url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`,
basePath: "/openclaw",
expectedBasePath: "/openclaw",
assistantName: "Ops",
assistantAvatar: "ops.png",
expectedAvatarUrl: "/openclaw/avatar/main",
},
])("serves bootstrap config JSON $name", async (testCase) => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
const { res, end } = makeMockHttpResponse(); const { res, end } = makeMockHttpResponse();
const handled = handleControlUiHttpRequest( const handled = handleControlUiHttpRequest(
{ url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH, method: "GET" } as IncomingMessage, { url: testCase.url, method: "GET" } as IncomingMessage,
res, res,
{ {
...(testCase.basePath ? { basePath: testCase.basePath } : {}),
root: { kind: "resolved", path: tmp }, root: { kind: "resolved", path: tmp },
config: { config: {
agents: { defaults: { workspace: tmp } }, agents: { defaults: { workspace: tmp } },
ui: { assistant: { name: "</script><script>alert(1)//", avatar: "</script>.png" } }, ui: {
assistant: {
name: testCase.assistantName,
avatar: testCase.assistantAvatar,
},
},
}, },
}, },
); );
expect(handled).toBe(true); expect(handled).toBe(true);
const parsed = parseBootstrapPayload(end); const parsed = parseBootstrapPayload(end);
expect(parsed.basePath).toBe(""); expect(parsed.basePath).toBe(testCase.expectedBasePath);
expect(parsed.assistantName).toBe("</script><script>alert(1)//"); expect(parsed.assistantName).toBe(testCase.assistantName);
expect(parsed.assistantAvatar).toBe("/avatar/main"); expect(parsed.assistantAvatar).toBe(testCase.expectedAvatarUrl);
expect(parsed.assistantAgentId).toBe("main"); expect(parsed.assistantAgentId).toBe("main");
}, },
}); });
}); });
it("serves bootstrap config JSON under basePath", async () => { it.each([
{
name: "at root",
url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH,
},
{
name: "under basePath",
url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`,
basePath: "/openclaw",
},
])("serves bootstrap config HEAD $name without writing a body", async (testCase) => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
const { res, end } = makeMockHttpResponse(); const { res, end } = makeMockHttpResponse();
const handled = handleControlUiHttpRequest( const handled = handleControlUiHttpRequest(
{ url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`, method: "GET" } as IncomingMessage, { url: testCase.url, method: "HEAD" } as IncomingMessage,
res, res,
{ {
basePath: "/openclaw", ...(testCase.basePath ? { basePath: testCase.basePath } : {}),
root: { kind: "resolved", path: tmp }, root: { kind: "resolved", path: tmp },
config: {
agents: { defaults: { workspace: tmp } },
ui: { assistant: { name: "Ops", avatar: "ops.png" } },
},
}, },
); );
expect(handled).toBe(true); expect(handled).toBe(true);
const parsed = parseBootstrapPayload(end); expect(res.statusCode).toBe(200);
expect(parsed.basePath).toBe("/openclaw"); expect(end.mock.calls[0]?.length ?? -1).toBe(0);
expect(parsed.assistantName).toBe("Ops");
expect(parsed.assistantAvatar).toBe("/openclaw/avatar/main");
expect(parsed.assistantAgentId).toBe("main");
}, },
}); });
}); });
@@ -350,7 +396,20 @@ describe("handleControlUiHttpRequest", () => {
}); });
}); });
it("rejects hardlinked asset files for custom/resolved roots (security boundary)", async () => { it.each([
{
name: "rejects hardlinked asset files for custom/resolved roots",
rootKind: "resolved" as const,
expectedStatus: 404,
expectedBody: "Not Found",
},
{
name: "serves hardlinked asset files for bundled roots",
rootKind: "bundled" as const,
expectedStatus: 200,
expectedBody: "console.log('hi');",
},
])("$name", async (testCase) => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
const assetsDir = path.join(tmp, "assets"); const assetsDir = path.join(tmp, "assets");
@@ -362,33 +421,12 @@ describe("handleControlUiHttpRequest", () => {
url: "/assets/app.hl.js", url: "/assets/app.hl.js",
method: "GET", method: "GET",
rootPath: tmp, rootPath: tmp,
rootKind: testCase.rootKind,
}); });
expect(handled).toBe(true); expect(handled).toBe(true);
expect(res.statusCode).toBe(404); expect(res.statusCode).toBe(testCase.expectedStatus);
expect(end).toHaveBeenCalledWith("Not Found"); expect(String(end.mock.calls[0]?.[0] ?? "")).toBe(testCase.expectedBody);
},
});
});
it("serves hardlinked asset files for bundled roots (pnpm global install)", async () => {
await withControlUiRoot({
fn: async (tmp) => {
const assetsDir = path.join(tmp, "assets");
await fs.mkdir(assetsDir, { recursive: true });
await fs.writeFile(path.join(assetsDir, "app.js"), "console.log('hi');");
await fs.link(path.join(assetsDir, "app.js"), path.join(assetsDir, "app.hl.js"));
const { res, end, handled } = runControlUiRequest({
url: "/assets/app.hl.js",
method: "GET",
rootPath: tmp,
rootKind: "bundled",
});
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(String(end.mock.calls[0]?.[0] ?? "")).toBe("console.log('hi');");
}, },
}); });
}); });
@@ -396,17 +434,12 @@ describe("handleControlUiHttpRequest", () => {
it("does not handle POST to root-mounted paths (plugin webhook passthrough)", async () => { it("does not handle POST to root-mounted paths (plugin webhook passthrough)", async () => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
for (const webhookPath of ["/bluebubbles-webhook", "/custom-webhook", "/callback"]) { expectUnhandledRoutes({
const { res } = makeMockHttpResponse(); urls: ["/bluebubbles-webhook", "/custom-webhook", "/callback"],
const handled = handleControlUiHttpRequest( method: "POST",
{ url: webhookPath, method: "POST" } as IncomingMessage, rootPath: tmp,
res, expectationLabel: "POST should pass through to plugin handlers",
{ root: { kind: "resolved", path: tmp } }, });
);
expect(handled, `POST to ${webhookPath} should pass through to plugin handlers`).toBe(
false,
);
}
}, },
}); });
}); });
@@ -414,43 +447,35 @@ describe("handleControlUiHttpRequest", () => {
it("does not handle POST to paths outside basePath", async () => { it("does not handle POST to paths outside basePath", async () => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
const { res } = makeMockHttpResponse(); expectUnhandledRoutes({
const handled = handleControlUiHttpRequest( urls: ["/bluebubbles-webhook"],
{ url: "/bluebubbles-webhook", method: "POST" } as IncomingMessage, method: "POST",
res, rootPath: tmp,
{ basePath: "/openclaw", root: { kind: "resolved", path: tmp } }, basePath: "/openclaw",
); expectationLabel: "POST outside basePath should pass through",
expect(handled).toBe(false); });
}, },
}); });
}); });
it("does not handle /api paths when basePath is empty", async () => { it.each([
{
name: "does not handle /api paths when basePath is empty",
urls: ["/api", "/api/sessions", "/api/channels/nostr"],
},
{
name: "does not handle /plugins paths when basePath is empty",
urls: ["/plugins", "/plugins/diffs/view/abc/def"],
},
])("$name", async (testCase) => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
for (const apiPath of ["/api", "/api/sessions", "/api/channels/nostr"]) { expectUnhandledRoutes({
const { handled } = runControlUiRequest({ urls: testCase.urls,
url: apiPath, method: "GET",
method: "GET", rootPath: tmp,
rootPath: tmp, expectationLabel: "expected route to not be handled",
}); });
expect(handled, `expected ${apiPath} to not be handled`).toBe(false);
}
},
});
});
it("does not handle /plugins paths when basePath is empty", async () => {
await withControlUiRoot({
fn: async (tmp) => {
for (const pluginPath of ["/plugins", "/plugins/diffs/view/abc/def"]) {
const { handled } = runControlUiRequest({
url: pluginPath,
method: "GET",
rootPath: tmp,
});
expect(handled, `expected ${pluginPath} to not be handled`).toBe(false);
}
}, },
}); });
}); });
@@ -458,13 +483,12 @@ describe("handleControlUiHttpRequest", () => {
it("falls through POST requests when basePath is empty", async () => { it("falls through POST requests when basePath is empty", async () => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
const { handled, end } = runControlUiRequest({ expectUnhandledRoutes({
url: "/webhook/bluebubbles", urls: ["/webhook/bluebubbles"],
method: "POST", method: "POST",
rootPath: tmp, rootPath: tmp,
expectationLabel: "POST webhook should fall through",
}); });
expect(handled).toBe(false);
expect(end).not.toHaveBeenCalled();
}, },
}); });
}); });
@@ -472,16 +496,13 @@ describe("handleControlUiHttpRequest", () => {
it("falls through POST requests under configured basePath (plugin webhook passthrough)", async () => { it("falls through POST requests under configured basePath (plugin webhook passthrough)", async () => {
await withControlUiRoot({ await withControlUiRoot({
fn: async (tmp) => { fn: async (tmp) => {
for (const route of ["/openclaw", "/openclaw/", "/openclaw/some-page"]) { expectUnhandledRoutes({
const { handled, end } = runControlUiRequest({ urls: ["/openclaw", "/openclaw/", "/openclaw/some-page"],
url: route, method: "POST",
method: "POST", rootPath: tmp,
rootPath: tmp, basePath: "/openclaw",
basePath: "/openclaw", expectationLabel: "POST under basePath should pass through to plugin handlers",
}); });
expect(handled, `POST to ${route} should pass through to plugin handlers`).toBe(false);
expect(end, `POST to ${route} should not write a response`).not.toHaveBeenCalled();
}
}, },
}); });
}); });