refactor(extensions): reuse shared helper primitives

This commit is contained in:
Peter Steinberger
2026-03-07 10:40:57 +00:00
parent 3c71e2bd48
commit 1aa77e4603
58 changed files with 1567 additions and 2195 deletions

View File

@@ -240,6 +240,15 @@ function getFirstDispatchCall(): DispatchReplyParams {
}
describe("BlueBubbles webhook monitor", () => {
const WEBHOOK_PATH = "/bluebubbles-webhook";
const BASE_WEBHOOK_MESSAGE_DATA = {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
} as const;
let unregister: () => void;
beforeEach(() => {
@@ -261,122 +270,144 @@ describe("BlueBubbles webhook monitor", () => {
unregister?.();
});
function createWebhookPayload(
dataOverrides: Record<string, unknown> = {},
): Record<string, unknown> {
return {
type: "new-message",
data: {
...BASE_WEBHOOK_MESSAGE_DATA,
...dataOverrides,
},
};
}
function createWebhookTargetDeps(core?: PluginRuntime): {
config: OpenClawConfig;
core: PluginRuntime;
runtime: {
log: ReturnType<typeof vi.fn<(message: string) => void>>;
error: ReturnType<typeof vi.fn<(message: string) => void>>;
};
} {
const resolvedCore = core ?? createMockRuntime();
setBlueBubblesRuntime(resolvedCore);
return {
config: {},
core: resolvedCore,
runtime: {
log: vi.fn<(message: string) => void>(),
error: vi.fn<(message: string) => void>(),
},
};
}
function registerWebhookTarget(
params: {
account?: ResolvedBlueBubblesAccount;
config?: OpenClawConfig;
core?: PluginRuntime;
runtime?: {
log: ReturnType<typeof vi.fn<(message: string) => void>>;
error: ReturnType<typeof vi.fn<(message: string) => void>>;
};
path?: string;
statusSink?: Parameters<typeof registerBlueBubblesWebhookTarget>[0]["statusSink"];
trackForCleanup?: boolean;
} = {},
): {
config: OpenClawConfig;
core: PluginRuntime;
runtime: {
log: ReturnType<typeof vi.fn<(message: string) => void>>;
error: ReturnType<typeof vi.fn<(message: string) => void>>;
};
stop: () => void;
} {
const deps =
params.config && params.core && params.runtime
? { config: params.config, core: params.core, runtime: params.runtime }
: createWebhookTargetDeps(params.core);
const stop = registerBlueBubblesWebhookTarget({
account: params.account ?? createMockAccount(),
...deps,
path: params.path ?? WEBHOOK_PATH,
statusSink: params.statusSink,
});
if (params.trackForCleanup !== false) {
unregister = stop;
}
return { ...deps, stop };
}
async function sendWebhookRequest(params: {
method?: string;
url?: string;
body?: unknown;
headers?: Record<string, string>;
remoteAddress?: string;
}): Promise<{
req: IncomingMessage;
res: ServerResponse & { body: string; statusCode: number };
handled: boolean;
}> {
const req = createMockRequest(
params.method ?? "POST",
params.url ?? WEBHOOK_PATH,
params.body ?? createWebhookPayload(),
params.headers,
);
if (params.remoteAddress) {
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: params.remoteAddress,
};
}
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
return { req, res, handled };
}
describe("webhook parsing + auth handling", () => {
it("rejects non-POST requests", async () => {
const account = createMockAccount();
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget();
const { handled, res } = await sendWebhookRequest({
method: "GET",
body: {},
});
const req = createMockRequest("GET", "/bluebubbles-webhook", {});
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(405);
});
it("accepts POST requests with valid JSON payload", async () => {
const account = createMockAccount();
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget();
const { handled, res } = await sendWebhookRequest({
body: createWebhookPayload({ date: Date.now() }),
});
const payload = {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
date: Date.now(),
},
};
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
expect(res.body).toBe("ok");
});
it("rejects requests with invalid JSON", async () => {
const account = createMockAccount();
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget();
const { handled, res } = await sendWebhookRequest({
body: "invalid json {{",
});
const req = createMockRequest("POST", "/bluebubbles-webhook", "invalid json {{");
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(400);
});
it("accepts URL-encoded payload wrappers", async () => {
const account = createMockAccount();
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const payload = {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
date: Date.now(),
},
};
registerWebhookTarget();
const payload = createWebhookPayload({ date: Date.now() });
const encodedBody = new URLSearchParams({
payload: JSON.stringify(payload),
}).toString();
const req = createMockRequest("POST", "/bluebubbles-webhook", encodedBody);
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
const { handled, res } = await sendWebhookRequest({ body: encodedBody });
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
@@ -386,23 +417,12 @@ describe("BlueBubbles webhook monitor", () => {
it("returns 408 when request body times out (Slow-Loris protection)", async () => {
vi.useFakeTimers();
try {
const account = createMockAccount();
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
registerWebhookTarget();
// Create a request that never sends data or ends (simulates slow-loris)
const req = new EventEmitter() as IncomingMessage;
req.method = "POST";
req.url = "/bluebubbles-webhook?password=test-password";
req.url = `${WEBHOOK_PATH}?password=test-password`;
req.headers = {};
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "127.0.0.1",
@@ -426,22 +446,13 @@ describe("BlueBubbles webhook monitor", () => {
});
it("rejects unauthorized requests before reading the body", async () => {
const account = createMockAccount({ password: "secret-token" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget({
account: createMockAccount({ password: "secret-token" }),
});
const req = new EventEmitter() as IncomingMessage;
req.method = "POST";
req.url = "/bluebubbles-webhook?password=wrong-token";
req.url = `${WEBHOOK_PATH}?password=wrong-token`;
req.headers = {};
const onSpy = vi.spyOn(req, "on");
(req as unknown as { socket: { remoteAddress: string } }).socket = {
@@ -457,112 +468,43 @@ describe("BlueBubbles webhook monitor", () => {
});
it("authenticates via password query parameter", async () => {
const account = createMockAccount({ password: "secret-token" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
// Mock non-localhost request
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
registerWebhookTarget({
account: createMockAccount({ password: "secret-token" }),
});
(req as unknown as { socket: { remoteAddress: string } }).socket = {
const { handled, res } = await sendWebhookRequest({
url: `${WEBHOOK_PATH}?password=secret-token`,
body: createWebhookPayload(),
remoteAddress: "192.168.1.100",
};
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
});
it("authenticates via x-password header", async () => {
const account = createMockAccount({ password: "secret-token" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
const req = createMockRequest(
"POST",
"/bluebubbles-webhook",
{
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
},
{ "x-password": "secret-token" },
);
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "192.168.1.100",
};
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget({
account: createMockAccount({ password: "secret-token" }),
});
const { handled, res } = await sendWebhookRequest({
body: createWebhookPayload(),
headers: { "x-password": "secret-token" },
remoteAddress: "192.168.1.100",
});
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
});
it("rejects unauthorized requests with wrong password", async () => {
const account = createMockAccount({ password: "secret-token" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
const req = createMockRequest("POST", "/bluebubbles-webhook?password=wrong-token", {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
registerWebhookTarget({
account: createMockAccount({ password: "secret-token" }),
});
(req as unknown as { socket: { remoteAddress: string } }).socket = {
const { handled, res } = await sendWebhookRequest({
url: `${WEBHOOK_PATH}?password=wrong-token`,
body: createWebhookPayload(),
remoteAddress: "192.168.1.100",
};
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
});
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res.statusCode).toBe(401);
});
@@ -570,50 +512,37 @@ describe("BlueBubbles webhook monitor", () => {
it("rejects ambiguous routing when multiple targets match the same password", async () => {
const accountA = createMockAccount({ password: "secret-token" });
const accountB = createMockAccount({ password: "secret-token" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
const { config, core, runtime } = createWebhookTargetDeps();
const sinkA = vi.fn();
const sinkB = vi.fn();
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
});
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "192.168.1.100",
};
const unregisterA = registerBlueBubblesWebhookTarget({
const unregisterA = registerWebhookTarget({
account: accountA,
config,
runtime: { log: vi.fn(), error: vi.fn() },
runtime,
core,
path: "/bluebubbles-webhook",
trackForCleanup: false,
statusSink: sinkA,
});
const unregisterB = registerBlueBubblesWebhookTarget({
}).stop;
const unregisterB = registerWebhookTarget({
account: accountB,
config,
runtime: { log: vi.fn(), error: vi.fn() },
runtime,
core,
path: "/bluebubbles-webhook",
trackForCleanup: false,
statusSink: sinkB,
});
}).stop;
unregister = () => {
unregisterA();
unregisterB();
};
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
const { handled, res } = await sendWebhookRequest({
url: `${WEBHOOK_PATH}?password=secret-token`,
body: createWebhookPayload(),
remoteAddress: "192.168.1.100",
});
expect(handled).toBe(true);
expect(res.statusCode).toBe(401);
@@ -624,50 +553,37 @@ describe("BlueBubbles webhook monitor", () => {
it("ignores targets without passwords when a password-authenticated target matches", async () => {
const accountStrict = createMockAccount({ password: "secret-token" });
const accountWithoutPassword = createMockAccount({ password: undefined });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
const { config, core, runtime } = createWebhookTargetDeps();
const sinkStrict = vi.fn();
const sinkWithoutPassword = vi.fn();
const req = createMockRequest("POST", "/bluebubbles-webhook?password=secret-token", {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
});
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "192.168.1.100",
};
const unregisterStrict = registerBlueBubblesWebhookTarget({
const unregisterStrict = registerWebhookTarget({
account: accountStrict,
config,
runtime: { log: vi.fn(), error: vi.fn() },
runtime,
core,
path: "/bluebubbles-webhook",
trackForCleanup: false,
statusSink: sinkStrict,
});
const unregisterNoPassword = registerBlueBubblesWebhookTarget({
}).stop;
const unregisterNoPassword = registerWebhookTarget({
account: accountWithoutPassword,
config,
runtime: { log: vi.fn(), error: vi.fn() },
runtime,
core,
path: "/bluebubbles-webhook",
trackForCleanup: false,
statusSink: sinkWithoutPassword,
});
}).stop;
unregister = () => {
unregisterStrict();
unregisterNoPassword();
};
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
const { handled, res } = await sendWebhookRequest({
url: `${WEBHOOK_PATH}?password=secret-token`,
body: createWebhookPayload(),
remoteAddress: "192.168.1.100",
});
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
@@ -677,34 +593,20 @@ describe("BlueBubbles webhook monitor", () => {
it("requires authentication for loopback requests when password is configured", async () => {
const account = createMockAccount({ password: "secret-token" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
const { config, core, runtime } = createWebhookTargetDeps();
for (const remoteAddress of ["127.0.0.1", "::1", "::ffff:127.0.0.1"]) {
const req = createMockRequest("POST", "/bluebubbles-webhook", {
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
});
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress,
};
const loopbackUnregister = registerBlueBubblesWebhookTarget({
const loopbackUnregister = registerWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
runtime,
core,
path: "/bluebubbles-webhook",
});
trackForCleanup: false,
}).stop;
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
const { handled, res } = await sendWebhookRequest({
body: createWebhookPayload(),
remoteAddress,
});
expect(handled).toBe(true);
expect(res.statusCode).toBe(401);
@@ -713,17 +615,8 @@ describe("BlueBubbles webhook monitor", () => {
});
it("rejects targets without passwords for loopback and proxied-looking requests", async () => {
const account = createMockAccount({ password: undefined });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget({
account: createMockAccount({ password: undefined }),
});
const headerVariants: Record<string, string>[] = [
@@ -732,26 +625,11 @@ describe("BlueBubbles webhook monitor", () => {
{ host: "localhost", forwarded: "for=203.0.113.10;proto=https;host=example.com" },
];
for (const headers of headerVariants) {
const req = createMockRequest(
"POST",
"/bluebubbles-webhook",
{
type: "new-message",
data: {
text: "hello",
handle: { address: "+15551234567" },
isGroup: false,
isFromMe: false,
guid: "msg-1",
},
},
const { handled, res } = await sendWebhookRequest({
body: createWebhookPayload(),
headers,
);
(req as unknown as { socket: { remoteAddress: string } }).socket = {
remoteAddress: "127.0.0.1",
};
const res = createMockResponse();
const handled = await handleBlueBubblesWebhookRequest(req, res);
});
expect(handled).toBe(true);
expect(res.statusCode).toBe(401);
}
@@ -770,36 +648,18 @@ describe("BlueBubbles webhook monitor", () => {
const { resolveChatGuidForTarget } = await import("./send.js");
vi.mocked(resolveChatGuidForTarget).mockClear();
const account = createMockAccount({ groupPolicy: "open" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget({
account: createMockAccount({ groupPolicy: "open" }),
});
const payload = {
type: "new-message",
data: {
await sendWebhookRequest({
body: createWebhookPayload({
text: "hello from group",
handle: { address: "+15551234567" },
isGroup: true,
isFromMe: false,
guid: "msg-1",
chatId: "123",
date: Date.now(),
},
};
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
}),
});
await flushAsync();
expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
@@ -819,36 +679,18 @@ describe("BlueBubbles webhook monitor", () => {
return EMPTY_DISPATCH_RESULT;
});
const account = createMockAccount({ groupPolicy: "open" });
const config: OpenClawConfig = {};
const core = createMockRuntime();
setBlueBubblesRuntime(core);
unregister = registerBlueBubblesWebhookTarget({
account,
config,
runtime: { log: vi.fn(), error: vi.fn() },
core,
path: "/bluebubbles-webhook",
registerWebhookTarget({
account: createMockAccount({ groupPolicy: "open" }),
});
const payload = {
type: "new-message",
data: {
await sendWebhookRequest({
body: createWebhookPayload({
text: "hello from group",
handle: { address: "+15551234567" },
isGroup: true,
isFromMe: false,
guid: "msg-1",
chat: { chatGuid: "iMessage;+;chat123456" },
date: Date.now(),
},
};
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
const res = createMockResponse();
await handleBlueBubblesWebhookRequest(req, res);
}),
});
await flushAsync();
expect(resolveChatGuidForTarget).not.toHaveBeenCalled();