Skip to content

Commit 2513a8d

Browse files
committed
fix(bluebubbles): refactor sendMessageBlueBubbles to use resolveBlueBubblesServerAccount and enhance private network handling in tests
1 parent 81c4597 commit 2513a8d

File tree

2 files changed

+71
-18
lines changed

2 files changed

+71
-18
lines changed

extensions/bluebubbles/src/send.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
installBlueBubblesFetchTestHooks,
1010
mockBlueBubblesPrivateApiStatusOnce,
1111
} from "./test-harness.js";
12-
import type { BlueBubblesSendTarget } from "./types.js";
12+
import { _setFetchGuardForTesting, type BlueBubblesSendTarget } from "./types.js";
1313

1414
const mockFetch = vi.fn();
1515
const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus);
@@ -61,6 +61,33 @@ function mockNewChatSendResponse(guid: string) {
6161
});
6262
}
6363

64+
function installSsrFPolicyCapture(policies: unknown[]) {
65+
_setFetchGuardForTesting(async (params) => {
66+
policies.push(params.policy);
67+
const raw = await globalThis.fetch(params.url, params.init);
68+
let body: ArrayBuffer;
69+
if (typeof raw.arrayBuffer === "function") {
70+
body = await raw.arrayBuffer();
71+
} else {
72+
const text =
73+
typeof (raw as { text?: () => Promise<string> }).text === "function"
74+
? await (raw as { text: () => Promise<string> }).text()
75+
: typeof (raw as { json?: () => Promise<unknown> }).json === "function"
76+
? JSON.stringify(await (raw as { json: () => Promise<unknown> }).json())
77+
: "";
78+
body = new TextEncoder().encode(text).buffer;
79+
}
80+
return {
81+
response: new Response(body, {
82+
status: (raw as { status?: number }).status ?? 200,
83+
headers: (raw as { headers?: HeadersInit }).headers,
84+
}),
85+
release: async () => {},
86+
finalUrl: params.url,
87+
};
88+
});
89+
}
90+
6491
describe("send", () => {
6592
describe("resolveChatGuidForTarget", () => {
6693
const resolveHandleTargetGuid = async (data: Array<Record<string, unknown>>) => {
@@ -448,6 +475,44 @@ describe("send", () => {
448475
expect(body.method).toBeUndefined();
449476
});
450477

478+
it("auto-enables private-network fetches for loopback serverUrl when allowPrivateNetwork is not set", async () => {
479+
const policies: unknown[] = [];
480+
installSsrFPolicyCapture(policies);
481+
mockResolvedHandleTarget();
482+
mockSendResponse({ data: { guid: "msg-loopback" } });
483+
484+
try {
485+
const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", {
486+
serverUrl: "http://localhost:1234",
487+
password: "test",
488+
});
489+
490+
expect(result.messageId).toBe("msg-loopback");
491+
expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]);
492+
} finally {
493+
_setFetchGuardForTesting(null);
494+
}
495+
});
496+
497+
it("auto-enables private-network fetches for private IP serverUrl when allowPrivateNetwork is not set", async () => {
498+
const policies: unknown[] = [];
499+
installSsrFPolicyCapture(policies);
500+
mockResolvedHandleTarget();
501+
mockSendResponse({ data: { guid: "msg-private-ip" } });
502+
503+
try {
504+
const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", {
505+
serverUrl: "http://192.168.1.5:1234",
506+
password: "test",
507+
});
508+
509+
expect(result.messageId).toBe("msg-private-ip");
510+
expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]);
511+
} finally {
512+
_setFetchGuardForTesting(null);
513+
}
514+
});
515+
451516
it("strips markdown formatting from outbound messages", async () => {
452517
mockResolvedHandleTarget();
453518
mockSendResponse({ data: { guid: "msg-uuid-stripped" } });

extensions/bluebubbles/src/send.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import crypto from "node:crypto";
2-
import { resolveBlueBubblesAccount } from "./accounts.js";
2+
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
33
import {
44
getCachedBlueBubblesPrivateApiStatus,
55
isBlueBubblesPrivateApiStatusEnabled,
66
} from "./probe.js";
77
import type { OpenClawConfig } from "./runtime-api.js";
88
import { stripMarkdown } from "./runtime-api.js";
99
import { warnBlueBubbles } from "./runtime.js";
10-
import { normalizeSecretInputString } from "./secret-input.js";
1110
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
1211
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
1312
import {
@@ -446,24 +445,13 @@ export async function sendMessageBlueBubbles(
446445
throw new Error("BlueBubbles send requires text (message was empty after markdown removal)");
447446
}
448447

449-
const account = resolveBlueBubblesAccount({
448+
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveBlueBubblesServerAccount({
450449
cfg: opts.cfg ?? {},
451450
accountId: opts.accountId,
451+
serverUrl: opts.serverUrl,
452+
password: opts.password,
452453
});
453-
const baseUrl =
454-
normalizeSecretInputString(opts.serverUrl) ||
455-
normalizeSecretInputString(account.config.serverUrl);
456-
const password =
457-
normalizeSecretInputString(opts.password) ||
458-
normalizeSecretInputString(account.config.password);
459-
if (!baseUrl) {
460-
throw new Error("BlueBubbles serverUrl is required");
461-
}
462-
if (!password) {
463-
throw new Error("BlueBubbles password is required");
464-
}
465-
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);
466-
const allowPrivateNetwork = account.config.allowPrivateNetwork === true;
454+
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
467455

468456
const target = resolveBlueBubblesSendTarget(to);
469457
const chatGuid = await resolveChatGuidForTarget({

0 commit comments

Comments
 (0)