mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 05:20:48 +00:00
test: simplify control ui http coverage
This commit is contained in:
@@ -40,6 +40,25 @@ describe("handleControlUiHttpRequest", () => {
|
||||
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: {
|
||||
url: string;
|
||||
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({
|
||||
fn: async (tmp) => {
|
||||
const { res, end } = makeMockHttpResponse();
|
||||
const handled = handleControlUiHttpRequest(
|
||||
{ url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH, method: "GET" } as IncomingMessage,
|
||||
{ url: testCase.url, method: "GET" } as IncomingMessage,
|
||||
res,
|
||||
{
|
||||
...(testCase.basePath ? { basePath: testCase.basePath } : {}),
|
||||
root: { kind: "resolved", path: tmp },
|
||||
config: {
|
||||
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);
|
||||
const parsed = parseBootstrapPayload(end);
|
||||
expect(parsed.basePath).toBe("");
|
||||
expect(parsed.assistantName).toBe("</script><script>alert(1)//");
|
||||
expect(parsed.assistantAvatar).toBe("/avatar/main");
|
||||
expect(parsed.basePath).toBe(testCase.expectedBasePath);
|
||||
expect(parsed.assistantName).toBe(testCase.assistantName);
|
||||
expect(parsed.assistantAvatar).toBe(testCase.expectedAvatarUrl);
|
||||
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({
|
||||
fn: async (tmp) => {
|
||||
const { res, end } = makeMockHttpResponse();
|
||||
const handled = handleControlUiHttpRequest(
|
||||
{ url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`, method: "GET" } as IncomingMessage,
|
||||
{ url: testCase.url, method: "HEAD" } as IncomingMessage,
|
||||
res,
|
||||
{
|
||||
basePath: "/openclaw",
|
||||
...(testCase.basePath ? { basePath: testCase.basePath } : {}),
|
||||
root: { kind: "resolved", path: tmp },
|
||||
config: {
|
||||
agents: { defaults: { workspace: tmp } },
|
||||
ui: { assistant: { name: "Ops", avatar: "ops.png" } },
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(handled).toBe(true);
|
||||
const parsed = parseBootstrapPayload(end);
|
||||
expect(parsed.basePath).toBe("/openclaw");
|
||||
expect(parsed.assistantName).toBe("Ops");
|
||||
expect(parsed.assistantAvatar).toBe("/openclaw/avatar/main");
|
||||
expect(parsed.assistantAgentId).toBe("main");
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(end.mock.calls[0]?.length ?? -1).toBe(0);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
fn: async (tmp) => {
|
||||
const assetsDir = path.join(tmp, "assets");
|
||||
@@ -362,33 +421,12 @@ describe("handleControlUiHttpRequest", () => {
|
||||
url: "/assets/app.hl.js",
|
||||
method: "GET",
|
||||
rootPath: tmp,
|
||||
rootKind: testCase.rootKind,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(404);
|
||||
expect(end).toHaveBeenCalledWith("Not Found");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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');");
|
||||
expect(res.statusCode).toBe(testCase.expectedStatus);
|
||||
expect(String(end.mock.calls[0]?.[0] ?? "")).toBe(testCase.expectedBody);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -396,17 +434,12 @@ describe("handleControlUiHttpRequest", () => {
|
||||
it("does not handle POST to root-mounted paths (plugin webhook passthrough)", async () => {
|
||||
await withControlUiRoot({
|
||||
fn: async (tmp) => {
|
||||
for (const webhookPath of ["/bluebubbles-webhook", "/custom-webhook", "/callback"]) {
|
||||
const { res } = makeMockHttpResponse();
|
||||
const handled = handleControlUiHttpRequest(
|
||||
{ url: webhookPath, method: "POST" } as IncomingMessage,
|
||||
res,
|
||||
{ root: { kind: "resolved", path: tmp } },
|
||||
);
|
||||
expect(handled, `POST to ${webhookPath} should pass through to plugin handlers`).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
expectUnhandledRoutes({
|
||||
urls: ["/bluebubbles-webhook", "/custom-webhook", "/callback"],
|
||||
method: "POST",
|
||||
rootPath: tmp,
|
||||
expectationLabel: "POST should pass through to plugin handlers",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -414,43 +447,35 @@ describe("handleControlUiHttpRequest", () => {
|
||||
it("does not handle POST to paths outside basePath", async () => {
|
||||
await withControlUiRoot({
|
||||
fn: async (tmp) => {
|
||||
const { res } = makeMockHttpResponse();
|
||||
const handled = handleControlUiHttpRequest(
|
||||
{ url: "/bluebubbles-webhook", method: "POST" } as IncomingMessage,
|
||||
res,
|
||||
{ basePath: "/openclaw", root: { kind: "resolved", path: tmp } },
|
||||
);
|
||||
expect(handled).toBe(false);
|
||||
expectUnhandledRoutes({
|
||||
urls: ["/bluebubbles-webhook"],
|
||||
method: "POST",
|
||||
rootPath: tmp,
|
||||
basePath: "/openclaw",
|
||||
expectationLabel: "POST outside basePath should pass through",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
fn: async (tmp) => {
|
||||
for (const apiPath of ["/api", "/api/sessions", "/api/channels/nostr"]) {
|
||||
const { handled } = runControlUiRequest({
|
||||
url: apiPath,
|
||||
method: "GET",
|
||||
rootPath: tmp,
|
||||
});
|
||||
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);
|
||||
}
|
||||
expectUnhandledRoutes({
|
||||
urls: testCase.urls,
|
||||
method: "GET",
|
||||
rootPath: tmp,
|
||||
expectationLabel: "expected route to not be handled",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -458,13 +483,12 @@ describe("handleControlUiHttpRequest", () => {
|
||||
it("falls through POST requests when basePath is empty", async () => {
|
||||
await withControlUiRoot({
|
||||
fn: async (tmp) => {
|
||||
const { handled, end } = runControlUiRequest({
|
||||
url: "/webhook/bluebubbles",
|
||||
expectUnhandledRoutes({
|
||||
urls: ["/webhook/bluebubbles"],
|
||||
method: "POST",
|
||||
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 () => {
|
||||
await withControlUiRoot({
|
||||
fn: async (tmp) => {
|
||||
for (const route of ["/openclaw", "/openclaw/", "/openclaw/some-page"]) {
|
||||
const { handled, end } = runControlUiRequest({
|
||||
url: route,
|
||||
method: "POST",
|
||||
rootPath: tmp,
|
||||
basePath: "/openclaw",
|
||||
});
|
||||
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();
|
||||
}
|
||||
expectUnhandledRoutes({
|
||||
urls: ["/openclaw", "/openclaw/", "/openclaw/some-page"],
|
||||
method: "POST",
|
||||
rootPath: tmp,
|
||||
basePath: "/openclaw",
|
||||
expectationLabel: "POST under basePath should pass through to plugin handlers",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user