bluebubbles: gracefully handle disabled private API with action/tool filtering and fallbacks (#16002)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 243cc0cc9a
Co-authored-by: tyler6204 <243?+tyler6204@users.noreply.github.com>
Co-authored-by: tyler6204 <64381258+tyler6204@users.noreply.github.com>
Reviewed-by: @tyler6204
This commit is contained in:
Tyler Yust
2026-02-13 21:15:56 -08:00
committed by GitHub
parent d8beddc8b7
commit 45e12d2388
13 changed files with 305 additions and 23 deletions

View File

@@ -1,5 +1,6 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { markBlueBubblesChatRead, sendBlueBubblesTyping, setGroupIconBlueBubbles } from "./chat.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
vi.mock("./accounts.js", () => ({
resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
@@ -13,12 +14,18 @@ vi.mock("./accounts.js", () => ({
}),
}));
vi.mock("./probe.js", () => ({
getCachedBlueBubblesPrivateApiStatus: vi.fn().mockReturnValue(null),
}));
const mockFetch = vi.fn();
describe("chat", () => {
beforeEach(() => {
vi.stubGlobal("fetch", mockFetch);
mockFetch.mockReset();
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReset();
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null);
});
afterEach(() => {
@@ -73,6 +80,17 @@ describe("chat", () => {
);
});
it("does not send read receipt when private API is disabled", async () => {
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
await markBlueBubblesChatRead("iMessage;-;+15551234567", {
serverUrl: "http://localhost:1234",
password: "test-password",
});
expect(mockFetch).not.toHaveBeenCalled();
});
it("includes password in URL query", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -190,6 +208,17 @@ describe("chat", () => {
);
});
it("does not send typing when private API is disabled", async () => {
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
await sendBlueBubblesTyping("iMessage;-;+15551234567", true, {
serverUrl: "http://localhost:1234",
password: "test",
});
expect(mockFetch).not.toHaveBeenCalled();
});
it("sends typing stop with DELETE method", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -348,6 +377,17 @@ describe("chat", () => {
).rejects.toThrow("password is required");
});
it("throws when private API is disabled", async () => {
vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
await expect(
setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {
serverUrl: "http://localhost:1234",
password: "test",
}),
).rejects.toThrow("requires Private API");
expect(mockFetch).not.toHaveBeenCalled();
});
it("sets group icon successfully", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,