Skip to content

Commit 91d4f5c

Browse files
committed
test: simplify control ui http coverage
1 parent 987c254 commit 91d4f5c

File tree

1 file changed

+119
-98
lines changed

1 file changed

+119
-98
lines changed

src/gateway/control-ui.http.test.ts

Lines changed: 119 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ describe("handleControlUiHttpRequest", () => {
4040
expect(params.end).toHaveBeenCalledWith("Not Found");
4141
}
4242

43+
function expectUnhandledRoutes(params: {
44+
urls: string[];
45+
method: "GET" | "POST";
46+
rootPath: string;
47+
basePath?: string;
48+
expectationLabel: string;
49+
}) {
50+
for (const url of params.urls) {
51+
const { handled, end } = runControlUiRequest({
52+
url,
53+
method: params.method,
54+
rootPath: params.rootPath,
55+
...(params.basePath ? { basePath: params.basePath } : {}),
56+
});
57+
expect(handled, `${params.expectationLabel}: ${url}`).toBe(false);
58+
expect(end, `${params.expectationLabel}: ${url}`).not.toHaveBeenCalled();
59+
}
60+
}
61+
4362
function runControlUiRequest(params: {
4463
url: string;
4564
method: "GET" | "HEAD" | "POST";
@@ -147,53 +166,80 @@ describe("handleControlUiHttpRequest", () => {
147166
});
148167
});
149168

150-
it("serves bootstrap config JSON", async () => {
169+
it.each([
170+
{
171+
name: "at root",
172+
url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH,
173+
expectedBasePath: "",
174+
assistantName: "</script><script>alert(1)//",
175+
assistantAvatar: "</script>.png",
176+
expectedAvatarUrl: "/avatar/main",
177+
},
178+
{
179+
name: "under basePath",
180+
url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`,
181+
basePath: "/openclaw",
182+
expectedBasePath: "/openclaw",
183+
assistantName: "Ops",
184+
assistantAvatar: "ops.png",
185+
expectedAvatarUrl: "/openclaw/avatar/main",
186+
},
187+
])("serves bootstrap config JSON $name", async (testCase) => {
151188
await withControlUiRoot({
152189
fn: async (tmp) => {
153190
const { res, end } = makeMockHttpResponse();
154191
const handled = handleControlUiHttpRequest(
155-
{ url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH, method: "GET" } as IncomingMessage,
192+
{ url: testCase.url, method: "GET" } as IncomingMessage,
156193
res,
157194
{
195+
...(testCase.basePath ? { basePath: testCase.basePath } : {}),
158196
root: { kind: "resolved", path: tmp },
159197
config: {
160198
agents: { defaults: { workspace: tmp } },
161-
ui: { assistant: { name: "</script><script>alert(1)//", avatar: "</script>.png" } },
199+
ui: {
200+
assistant: {
201+
name: testCase.assistantName,
202+
avatar: testCase.assistantAvatar,
203+
},
204+
},
162205
},
163206
},
164207
);
165208
expect(handled).toBe(true);
166209
const parsed = parseBootstrapPayload(end);
167-
expect(parsed.basePath).toBe("");
168-
expect(parsed.assistantName).toBe("</script><script>alert(1)//");
169-
expect(parsed.assistantAvatar).toBe("/avatar/main");
210+
expect(parsed.basePath).toBe(testCase.expectedBasePath);
211+
expect(parsed.assistantName).toBe(testCase.assistantName);
212+
expect(parsed.assistantAvatar).toBe(testCase.expectedAvatarUrl);
170213
expect(parsed.assistantAgentId).toBe("main");
171214
},
172215
});
173216
});
174217

175-
it("serves bootstrap config JSON under basePath", async () => {
218+
it.each([
219+
{
220+
name: "at root",
221+
url: CONTROL_UI_BOOTSTRAP_CONFIG_PATH,
222+
},
223+
{
224+
name: "under basePath",
225+
url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`,
226+
basePath: "/openclaw",
227+
},
228+
])("serves bootstrap config HEAD $name without writing a body", async (testCase) => {
176229
await withControlUiRoot({
177230
fn: async (tmp) => {
178231
const { res, end } = makeMockHttpResponse();
179232
const handled = handleControlUiHttpRequest(
180-
{ url: `/openclaw${CONTROL_UI_BOOTSTRAP_CONFIG_PATH}`, method: "GET" } as IncomingMessage,
233+
{ url: testCase.url, method: "HEAD" } as IncomingMessage,
181234
res,
182235
{
183-
basePath: "/openclaw",
236+
...(testCase.basePath ? { basePath: testCase.basePath } : {}),
184237
root: { kind: "resolved", path: tmp },
185-
config: {
186-
agents: { defaults: { workspace: tmp } },
187-
ui: { assistant: { name: "Ops", avatar: "ops.png" } },
188-
},
189238
},
190239
);
191240
expect(handled).toBe(true);
192-
const parsed = parseBootstrapPayload(end);
193-
expect(parsed.basePath).toBe("/openclaw");
194-
expect(parsed.assistantName).toBe("Ops");
195-
expect(parsed.assistantAvatar).toBe("/openclaw/avatar/main");
196-
expect(parsed.assistantAgentId).toBe("main");
241+
expect(res.statusCode).toBe(200);
242+
expect(end.mock.calls[0]?.length ?? -1).toBe(0);
197243
},
198244
});
199245
});
@@ -350,7 +396,20 @@ describe("handleControlUiHttpRequest", () => {
350396
});
351397
});
352398

353-
it("rejects hardlinked asset files for custom/resolved roots (security boundary)", async () => {
399+
it.each([
400+
{
401+
name: "rejects hardlinked asset files for custom/resolved roots",
402+
rootKind: "resolved" as const,
403+
expectedStatus: 404,
404+
expectedBody: "Not Found",
405+
},
406+
{
407+
name: "serves hardlinked asset files for bundled roots",
408+
rootKind: "bundled" as const,
409+
expectedStatus: 200,
410+
expectedBody: "console.log('hi');",
411+
},
412+
])("$name", async (testCase) => {
354413
await withControlUiRoot({
355414
fn: async (tmp) => {
356415
const assetsDir = path.join(tmp, "assets");
@@ -362,126 +421,88 @@ describe("handleControlUiHttpRequest", () => {
362421
url: "/assets/app.hl.js",
363422
method: "GET",
364423
rootPath: tmp,
424+
rootKind: testCase.rootKind,
365425
});
366426

367427
expect(handled).toBe(true);
368-
expect(res.statusCode).toBe(404);
369-
expect(end).toHaveBeenCalledWith("Not Found");
428+
expect(res.statusCode).toBe(testCase.expectedStatus);
429+
expect(String(end.mock.calls[0]?.[0] ?? "")).toBe(testCase.expectedBody);
370430
},
371431
});
372432
});
373433

374-
it("serves hardlinked asset files for bundled roots (pnpm global install)", async () => {
434+
it("does not handle POST to root-mounted paths (plugin webhook passthrough)", async () => {
375435
await withControlUiRoot({
376436
fn: async (tmp) => {
377-
const assetsDir = path.join(tmp, "assets");
378-
await fs.mkdir(assetsDir, { recursive: true });
379-
await fs.writeFile(path.join(assetsDir, "app.js"), "console.log('hi');");
380-
await fs.link(path.join(assetsDir, "app.js"), path.join(assetsDir, "app.hl.js"));
381-
382-
const { res, end, handled } = runControlUiRequest({
383-
url: "/assets/app.hl.js",
384-
method: "GET",
437+
expectUnhandledRoutes({
438+
urls: ["/bluebubbles-webhook", "/custom-webhook", "/callback"],
439+
method: "POST",
385440
rootPath: tmp,
386-
rootKind: "bundled",
441+
expectationLabel: "POST should pass through to plugin handlers",
387442
});
388-
389-
expect(handled).toBe(true);
390-
expect(res.statusCode).toBe(200);
391-
expect(String(end.mock.calls[0]?.[0] ?? "")).toBe("console.log('hi');");
392-
},
393-
});
394-
});
395-
396-
it("does not handle POST to root-mounted paths (plugin webhook passthrough)", async () => {
397-
await withControlUiRoot({
398-
fn: async (tmp) => {
399-
for (const webhookPath of ["/bluebubbles-webhook", "/custom-webhook", "/callback"]) {
400-
const { res } = makeMockHttpResponse();
401-
const handled = handleControlUiHttpRequest(
402-
{ url: webhookPath, method: "POST" } as IncomingMessage,
403-
res,
404-
{ root: { kind: "resolved", path: tmp } },
405-
);
406-
expect(handled, `POST to ${webhookPath} should pass through to plugin handlers`).toBe(
407-
false,
408-
);
409-
}
410443
},
411444
});
412445
});
413446

414447
it("does not handle POST to paths outside basePath", async () => {
415448
await withControlUiRoot({
416449
fn: async (tmp) => {
417-
const { res } = makeMockHttpResponse();
418-
const handled = handleControlUiHttpRequest(
419-
{ url: "/bluebubbles-webhook", method: "POST" } as IncomingMessage,
420-
res,
421-
{ basePath: "/openclaw", root: { kind: "resolved", path: tmp } },
422-
);
423-
expect(handled).toBe(false);
424-
},
425-
});
426-
});
427-
428-
it("does not handle /api paths when basePath is empty", async () => {
429-
await withControlUiRoot({
430-
fn: async (tmp) => {
431-
for (const apiPath of ["/api", "/api/sessions", "/api/channels/nostr"]) {
432-
const { handled } = runControlUiRequest({
433-
url: apiPath,
434-
method: "GET",
435-
rootPath: tmp,
436-
});
437-
expect(handled, `expected ${apiPath} to not be handled`).toBe(false);
438-
}
450+
expectUnhandledRoutes({
451+
urls: ["/bluebubbles-webhook"],
452+
method: "POST",
453+
rootPath: tmp,
454+
basePath: "/openclaw",
455+
expectationLabel: "POST outside basePath should pass through",
456+
});
439457
},
440458
});
441459
});
442460

443-
it("does not handle /plugins paths when basePath is empty", async () => {
461+
it.each([
462+
{
463+
name: "does not handle /api paths when basePath is empty",
464+
urls: ["/api", "/api/sessions", "/api/channels/nostr"],
465+
},
466+
{
467+
name: "does not handle /plugins paths when basePath is empty",
468+
urls: ["/plugins", "/plugins/diffs/view/abc/def"],
469+
},
470+
])("$name", async (testCase) => {
444471
await withControlUiRoot({
445472
fn: async (tmp) => {
446-
for (const pluginPath of ["/plugins", "/plugins/diffs/view/abc/def"]) {
447-
const { handled } = runControlUiRequest({
448-
url: pluginPath,
449-
method: "GET",
450-
rootPath: tmp,
451-
});
452-
expect(handled, `expected ${pluginPath} to not be handled`).toBe(false);
453-
}
473+
expectUnhandledRoutes({
474+
urls: testCase.urls,
475+
method: "GET",
476+
rootPath: tmp,
477+
expectationLabel: "expected route to not be handled",
478+
});
454479
},
455480
});
456481
});
457482

458483
it("falls through POST requests when basePath is empty", async () => {
459484
await withControlUiRoot({
460485
fn: async (tmp) => {
461-
const { handled, end } = runControlUiRequest({
462-
url: "/webhook/bluebubbles",
486+
expectUnhandledRoutes({
487+
urls: ["/webhook/bluebubbles"],
463488
method: "POST",
464489
rootPath: tmp,
490+
expectationLabel: "POST webhook should fall through",
465491
});
466-
expect(handled).toBe(false);
467-
expect(end).not.toHaveBeenCalled();
468492
},
469493
});
470494
});
471495

472496
it("falls through POST requests under configured basePath (plugin webhook passthrough)", async () => {
473497
await withControlUiRoot({
474498
fn: async (tmp) => {
475-
for (const route of ["/openclaw", "/openclaw/", "/openclaw/some-page"]) {
476-
const { handled, end } = runControlUiRequest({
477-
url: route,
478-
method: "POST",
479-
rootPath: tmp,
480-
basePath: "/openclaw",
481-
});
482-
expect(handled, `POST to ${route} should pass through to plugin handlers`).toBe(false);
483-
expect(end, `POST to ${route} should not write a response`).not.toHaveBeenCalled();
484-
}
499+
expectUnhandledRoutes({
500+
urls: ["/openclaw", "/openclaw/", "/openclaw/some-page"],
501+
method: "POST",
502+
rootPath: tmp,
503+
basePath: "/openclaw",
504+
expectationLabel: "POST under basePath should pass through to plugin handlers",
505+
});
485506
},
486507
});
487508
});

0 commit comments

Comments
 (0)