mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 08:38:38 +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");
|
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();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user