fix(bluebubbles): refactor sendMessageBlueBubbles to use resolveBlueBubblesServerAccount and enhance private network handling in tests

This commit is contained in:
Tyler Yust
2026-03-26 07:18:21 -07:00
parent 81c45976db
commit 2513a8d852
2 changed files with 71 additions and 18 deletions

View File

@@ -9,7 +9,7 @@ import {
installBlueBubblesFetchTestHooks,
mockBlueBubblesPrivateApiStatusOnce,
} from "./test-harness.js";
import type { BlueBubblesSendTarget } from "./types.js";
import { _setFetchGuardForTesting, type BlueBubblesSendTarget } from "./types.js";
const mockFetch = vi.fn();
const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus);
@@ -61,6 +61,33 @@ function mockNewChatSendResponse(guid: string) {
});
}
function installSsrFPolicyCapture(policies: unknown[]) {
_setFetchGuardForTesting(async (params) => {
policies.push(params.policy);
const raw = await globalThis.fetch(params.url, params.init);
let body: ArrayBuffer;
if (typeof raw.arrayBuffer === "function") {
body = await raw.arrayBuffer();
} else {
const text =
typeof (raw as { text?: () => Promise<string> }).text === "function"
? await (raw as { text: () => Promise<string> }).text()
: typeof (raw as { json?: () => Promise<unknown> }).json === "function"
? JSON.stringify(await (raw as { json: () => Promise<unknown> }).json())
: "";
body = new TextEncoder().encode(text).buffer;
}
return {
response: new Response(body, {
status: (raw as { status?: number }).status ?? 200,
headers: (raw as { headers?: HeadersInit }).headers,
}),
release: async () => {},
finalUrl: params.url,
};
});
}
describe("send", () => {
describe("resolveChatGuidForTarget", () => {
const resolveHandleTargetGuid = async (data: Array<Record<string, unknown>>) => {
@@ -448,6 +475,44 @@ describe("send", () => {
expect(body.method).toBeUndefined();
});
it("auto-enables private-network fetches for loopback serverUrl when allowPrivateNetwork is not set", async () => {
const policies: unknown[] = [];
installSsrFPolicyCapture(policies);
mockResolvedHandleTarget();
mockSendResponse({ data: { guid: "msg-loopback" } });
try {
const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", {
serverUrl: "http://localhost:1234",
password: "test",
});
expect(result.messageId).toBe("msg-loopback");
expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]);
} finally {
_setFetchGuardForTesting(null);
}
});
it("auto-enables private-network fetches for private IP serverUrl when allowPrivateNetwork is not set", async () => {
const policies: unknown[] = [];
installSsrFPolicyCapture(policies);
mockResolvedHandleTarget();
mockSendResponse({ data: { guid: "msg-private-ip" } });
try {
const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", {
serverUrl: "http://192.168.1.5:1234",
password: "test",
});
expect(result.messageId).toBe("msg-private-ip");
expect(policies).toEqual([{ allowPrivateNetwork: true }, { allowPrivateNetwork: true }]);
} finally {
_setFetchGuardForTesting(null);
}
});
it("strips markdown formatting from outbound messages", async () => {
mockResolvedHandleTarget();
mockSendResponse({ data: { guid: "msg-uuid-stripped" } });

View File

@@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
import {
getCachedBlueBubblesPrivateApiStatus,
isBlueBubblesPrivateApiStatusEnabled,
@@ -7,7 +7,6 @@ import {
import type { OpenClawConfig } from "./runtime-api.js";
import { stripMarkdown } from "./runtime-api.js";
import { warnBlueBubbles } from "./runtime.js";
import { normalizeSecretInputString } from "./secret-input.js";
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
import {
@@ -446,24 +445,13 @@ export async function sendMessageBlueBubbles(
throw new Error("BlueBubbles send requires text (message was empty after markdown removal)");
}
const account = resolveBlueBubblesAccount({
const { baseUrl, password, accountId, allowPrivateNetwork } = resolveBlueBubblesServerAccount({
cfg: opts.cfg ?? {},
accountId: opts.accountId,
serverUrl: opts.serverUrl,
password: opts.password,
});
const baseUrl =
normalizeSecretInputString(opts.serverUrl) ||
normalizeSecretInputString(account.config.serverUrl);
const password =
normalizeSecretInputString(opts.password) ||
normalizeSecretInputString(account.config.password);
if (!baseUrl) {
throw new Error("BlueBubbles serverUrl is required");
}
if (!password) {
throw new Error("BlueBubbles password is required");
}
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(account.accountId);
const allowPrivateNetwork = account.config.allowPrivateNetwork === true;
const privateApiStatus = getCachedBlueBubblesPrivateApiStatus(accountId);
const target = resolveBlueBubblesSendTarget(to);
const chatGuid = await resolveChatGuidForTarget({