Download extensions/bluebubbles/src/send.test.ts from Darochin/openskynet: direct link, hf CLI and curl.
- Browser
- Download file 24.2 kB
-
https://huggingface.co/Darochin/openskynet/resolve/main/extensions/bluebubbles/src/send.test.ts
- Command line
-
hf download hf://Darochin/openskynet/extensions/bluebubbles/src/send.test.ts
-
curl -L -o send.test.ts https://huggingface.co/Darochin/openskynet/resolve/main/extensions/bluebubbles/src/send.test.ts
24.2 kB
| import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; | |
| import { beforeEach, describe, expect, it, vi } from "vitest"; | |
| import "./test-mocks.js"; | |
| import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; | |
| import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js"; | |
| import { sendMessageBlueBubbles, resolveChatGuidForTarget } from "./send.js"; | |
| import { | |
| BLUE_BUBBLES_PRIVATE_API_STATUS, | |
| installBlueBubblesFetchTestHooks, | |
| mockBlueBubblesPrivateApiStatusOnce, | |
| } from "./test-harness.js"; | |
| import type { BlueBubblesSendTarget } from "./types.js"; | |
| const mockFetch = vi.fn(); | |
| const privateApiStatusMock = vi.mocked(getCachedBlueBubblesPrivateApiStatus); | |
| installBlueBubblesFetchTestHooks({ | |
| mockFetch, | |
| privateApiStatusMock, | |
| }); | |
| function mockResolvedHandleTarget( | |
| guid: string = "iMessage;-;+15551234567", | |
| address: string = "+15551234567", | |
| ) { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { | |
| guid, | |
| participants: [{ address }], | |
| }, | |
| ], | |
| }), | |
| }); | |
| } | |
| function mockSendResponse(body: unknown) { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| text: () => Promise.resolve(JSON.stringify(body)), | |
| }); | |
| } | |
| function mockNewChatSendResponse(guid: string) { | |
| mockFetch | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => Promise.resolve({ data: [] }), | |
| }) | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| text: () => | |
| Promise.resolve( | |
| JSON.stringify({ | |
| data: { guid }, | |
| }), | |
| ), | |
| }); | |
| } | |
| describe("send", () => { | |
| describe("resolveChatGuidForTarget", () => { | |
| const resolveHandleTargetGuid = async (data: Array<Record<string, unknown>>) => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => Promise.resolve({ data }), | |
| }); | |
| const target: BlueBubblesSendTarget = { | |
| kind: "handle", | |
| address: "+15551234567", | |
| service: "imessage", | |
| }; | |
| return await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| }; | |
| it("returns chatGuid directly for chat_guid target", async () => { | |
| const target: BlueBubblesSendTarget = { | |
| kind: "chat_guid", | |
| chatGuid: "iMessage;-;+15551234567", | |
| }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("iMessage;-;+15551234567"); | |
| expect(mockFetch).not.toHaveBeenCalled(); | |
| }); | |
| it("queries chats to resolve chat_id target", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { id: 123, guid: "iMessage;-;chat123", participants: [] }, | |
| { id: 456, guid: "iMessage;-;chat456", participants: [] }, | |
| ], | |
| }), | |
| }); | |
| const target: BlueBubblesSendTarget = { kind: "chat_id", chatId: 456 }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("iMessage;-;chat456"); | |
| expect(mockFetch).toHaveBeenCalledWith( | |
| expect.stringContaining("/api/v1/chat/query"), | |
| expect.objectContaining({ method: "POST" }), | |
| ); | |
| }); | |
| it("queries chats to resolve chat_identifier target", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { | |
| identifier: "chat123@group.imessage", | |
| guid: "iMessage;-;chat123", | |
| participants: [], | |
| }, | |
| ], | |
| }), | |
| }); | |
| const target: BlueBubblesSendTarget = { | |
| kind: "chat_identifier", | |
| chatIdentifier: "chat123@group.imessage", | |
| }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("iMessage;-;chat123"); | |
| }); | |
| it("matches chat_identifier against the 3rd component of chat GUID", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { | |
| guid: "iMessage;+;chat660250192681427962", | |
| participants: [], | |
| }, | |
| ], | |
| }), | |
| }); | |
| const target: BlueBubblesSendTarget = { | |
| kind: "chat_identifier", | |
| chatIdentifier: "chat660250192681427962", | |
| }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("iMessage;+;chat660250192681427962"); | |
| }); | |
| it("resolves handle target by matching participant", async () => { | |
| const result = await resolveHandleTargetGuid([ | |
| { | |
| guid: "iMessage;-;+15559999999", | |
| participants: [{ address: "+15559999999" }], | |
| }, | |
| { | |
| guid: "iMessage;-;+15551234567", | |
| participants: [{ address: "+15551234567" }], | |
| }, | |
| ]); | |
| expect(result).toBe("iMessage;-;+15551234567"); | |
| }); | |
| it("prefers direct chat guid when handle also appears in a group chat", async () => { | |
| const result = await resolveHandleTargetGuid([ | |
| { | |
| guid: "iMessage;+;group-123", | |
| participants: [{ address: "+15551234567" }, { address: "+15550001111" }], | |
| }, | |
| { | |
| guid: "iMessage;-;+15551234567", | |
| participants: [{ address: "+15551234567" }], | |
| }, | |
| ]); | |
| expect(result).toBe("iMessage;-;+15551234567"); | |
| }); | |
| it("returns null when handle only exists in group chat (not DM)", async () => { | |
| // This is the critical fix: if a phone number only exists as a participant in a group chat | |
| // (no direct DM chat), we should NOT send to that group. Return null instead. | |
| mockFetch | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { | |
| guid: "iMessage;+;group-the-council", | |
| participants: [ | |
| { address: "+12622102921" }, | |
| { address: "+15550001111" }, | |
| { address: "+15550002222" }, | |
| ], | |
| }, | |
| ], | |
| }), | |
| }) | |
| // Empty second page to stop pagination | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => Promise.resolve({ data: [] }), | |
| }); | |
| const target: BlueBubblesSendTarget = { | |
| kind: "handle", | |
| address: "+12622102921", | |
| service: "imessage", | |
| }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| // Should return null, NOT the group chat GUID | |
| expect(result).toBeNull(); | |
| }); | |
| it("returns null when chat not found", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => Promise.resolve({ data: [] }), | |
| }); | |
| const target: BlueBubblesSendTarget = { kind: "chat_id", chatId: 999 }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBeNull(); | |
| }); | |
| it("handles API error gracefully", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: false, | |
| status: 500, | |
| }); | |
| const target: BlueBubblesSendTarget = { kind: "chat_id", chatId: 123 }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBeNull(); | |
| }); | |
| it("paginates through chats to find match", async () => { | |
| mockFetch | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: Array(500) | |
| .fill(null) | |
| .map((_, i) => ({ | |
| id: i, | |
| guid: `chat-${i}`, | |
| participants: [], | |
| })), | |
| }), | |
| }) | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [{ id: 555, guid: "found-chat", participants: [] }], | |
| }), | |
| }); | |
| const target: BlueBubblesSendTarget = { kind: "chat_id", chatId: 555 }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("found-chat"); | |
| expect(mockFetch).toHaveBeenCalledTimes(2); | |
| }); | |
| it("normalizes handle addresses for matching", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { | |
| guid: "iMessage;-;test@example.com", | |
| participants: [{ address: "Test@Example.COM" }], | |
| }, | |
| ], | |
| }), | |
| }); | |
| const target: BlueBubblesSendTarget = { | |
| kind: "handle", | |
| address: "test@example.com", | |
| service: "auto", | |
| }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("iMessage;-;test@example.com"); | |
| }); | |
| it("extracts guid from various response formats", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => | |
| Promise.resolve({ | |
| data: [ | |
| { | |
| chatGuid: "format1-guid", | |
| id: 100, | |
| participants: [], | |
| }, | |
| ], | |
| }), | |
| }); | |
| const target: BlueBubblesSendTarget = { kind: "chat_id", chatId: 100 }; | |
| const result = await resolveChatGuidForTarget({ | |
| baseUrl: "http://localhost:1234", | |
| password: "test", | |
| target, | |
| }); | |
| expect(result).toBe("format1-guid"); | |
| }); | |
| }); | |
| describe("sendMessageBlueBubbles", () => { | |
| beforeEach(() => { | |
| mockFetch.mockReset(); | |
| }); | |
| it("throws when text is empty", async () => { | |
| await expect( | |
| sendMessageBlueBubbles("+15551234567", "", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }), | |
| ).rejects.toThrow("requires text"); | |
| }); | |
| it("throws when text is whitespace only", async () => { | |
| await expect( | |
| sendMessageBlueBubbles("+15551234567", " ", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }), | |
| ).rejects.toThrow("requires text"); | |
| }); | |
| it("throws when text becomes empty after markdown stripping", async () => { | |
| // Edge case: input like "***" or "---" passes initial check but becomes empty after stripMarkdown | |
| await expect( | |
| sendMessageBlueBubbles("+15551234567", "***", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }), | |
| ).rejects.toThrow("empty after markdown removal"); | |
| }); | |
| it("throws when serverUrl is missing", async () => { | |
| await expect(sendMessageBlueBubbles("+15551234567", "Hello", {})).rejects.toThrow( | |
| "serverUrl is required", | |
| ); | |
| }); | |
| it("throws when password is missing", async () => { | |
| await expect( | |
| sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| }), | |
| ).rejects.toThrow("password is required"); | |
| }); | |
| it("throws when chatGuid cannot be resolved for non-handle targets", async () => { | |
| mockFetch.mockResolvedValue({ | |
| ok: true, | |
| json: () => Promise.resolve({ data: [] }), | |
| }); | |
| await expect( | |
| sendMessageBlueBubbles("chat_id:999", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }), | |
| ).rejects.toThrow("chatGuid not found"); | |
| }); | |
| it("sends message successfully", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-uuid-123" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello world!", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("msg-uuid-123"); | |
| expect(mockFetch).toHaveBeenCalledTimes(2); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| expect(sendCall[0]).toContain("/api/v1/message/text"); | |
| const body = JSON.parse(sendCall[1].body); | |
| expect(body.chatGuid).toBe("iMessage;-;+15551234567"); | |
| expect(body.message).toBe("Hello world!"); | |
| expect(body.method).toBeUndefined(); | |
| }); | |
| it("strips markdown formatting from outbound messages", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-uuid-stripped" } }); | |
| const result = await sendMessageBlueBubbles( | |
| "+15551234567", | |
| "**Bold** and *italic* with `code`\n## Header", | |
| { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }, | |
| ); | |
| expect(result.messageId).toBe("msg-uuid-stripped"); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| const body = JSON.parse(sendCall[1].body); | |
| // Markdown should be stripped: no asterisks, backticks, or hashes | |
| expect(body.message).toBe("Bold and italic with code\nHeader"); | |
| }); | |
| it("strips markdown when creating a new chat", async () => { | |
| mockNewChatSendResponse("new-msg-stripped"); | |
| const result = await sendMessageBlueBubbles("+15550009999", "**Welcome** to the _chat_!", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("new-msg-stripped"); | |
| const createCall = mockFetch.mock.calls[1]; | |
| expect(createCall[0]).toContain("/api/v1/chat/new"); | |
| const body = JSON.parse(createCall[1].body); | |
| // Markdown should be stripped | |
| expect(body.message).toBe("Welcome to the chat!"); | |
| }); | |
| it("creates a new chat when handle target is missing", async () => { | |
| mockNewChatSendResponse("new-msg-guid"); | |
| const result = await sendMessageBlueBubbles("+15550009999", "Hello new chat", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("new-msg-guid"); | |
| expect(mockFetch).toHaveBeenCalledTimes(2); | |
| const createCall = mockFetch.mock.calls[1]; | |
| expect(createCall[0]).toContain("/api/v1/chat/new"); | |
| const body = JSON.parse(createCall[1].body); | |
| expect(body.addresses).toEqual(["+15550009999"]); | |
| expect(body.message).toBe("Hello new chat"); | |
| }); | |
| it("throws when creating a new chat requires Private API", async () => { | |
| mockFetch | |
| .mockResolvedValueOnce({ | |
| ok: true, | |
| json: () => Promise.resolve({ data: [] }), | |
| }) | |
| .mockResolvedValueOnce({ | |
| ok: false, | |
| status: 403, | |
| text: () => Promise.resolve("Private API not enabled"), | |
| }); | |
| await expect( | |
| sendMessageBlueBubbles("+15550008888", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }), | |
| ).rejects.toThrow("Private API must be enabled"); | |
| }); | |
| it("uses private-api when reply metadata is present", async () => { | |
| mockBlueBubblesPrivateApiStatusOnce( | |
| privateApiStatusMock, | |
| BLUE_BUBBLES_PRIVATE_API_STATUS.enabled, | |
| ); | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-uuid-124" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Replying", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| replyToMessageGuid: "reply-guid-123", | |
| replyToPartIndex: 1, | |
| }); | |
| expect(result.messageId).toBe("msg-uuid-124"); | |
| expect(mockFetch).toHaveBeenCalledTimes(2); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| const body = JSON.parse(sendCall[1].body); | |
| expect(body.method).toBe("private-api"); | |
| expect(body.selectedMessageGuid).toBe("reply-guid-123"); | |
| expect(body.partIndex).toBe(1); | |
| }); | |
| it("downgrades threaded reply to plain send when private API is disabled", async () => { | |
| mockBlueBubblesPrivateApiStatusOnce( | |
| privateApiStatusMock, | |
| BLUE_BUBBLES_PRIVATE_API_STATUS.disabled, | |
| ); | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-uuid-plain" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Reply fallback", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| replyToMessageGuid: "reply-guid-123", | |
| replyToPartIndex: 1, | |
| }); | |
| expect(result.messageId).toBe("msg-uuid-plain"); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| const body = JSON.parse(sendCall[1].body); | |
| expect(body.method).toBeUndefined(); | |
| expect(body.selectedMessageGuid).toBeUndefined(); | |
| expect(body.partIndex).toBeUndefined(); | |
| }); | |
| it("normalizes effect names and uses private-api for effects", async () => { | |
| mockBlueBubblesPrivateApiStatusOnce( | |
| privateApiStatusMock, | |
| BLUE_BUBBLES_PRIVATE_API_STATUS.enabled, | |
| ); | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-uuid-125" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| effectId: "invisible ink", | |
| }); | |
| expect(result.messageId).toBe("msg-uuid-125"); | |
| expect(mockFetch).toHaveBeenCalledTimes(2); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| const body = JSON.parse(sendCall[1].body); | |
| expect(body.method).toBe("private-api"); | |
| expect(body.effectId).toBe("com.apple.MobileSMS.expressivesend.invisibleink"); | |
| }); | |
| it("warns and downgrades private-api features when status is unknown", async () => { | |
| const runtimeLog = vi.fn(); | |
| setBlueBubblesRuntime({ log: runtimeLog } as unknown as PluginRuntime); | |
| const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-uuid-unknown" } }); | |
| try { | |
| const result = await sendMessageBlueBubbles("+15551234567", "Reply fallback", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| replyToMessageGuid: "reply-guid-123", | |
| effectId: "invisible ink", | |
| }); | |
| expect(result.messageId).toBe("msg-uuid-unknown"); | |
| expect(runtimeLog).toHaveBeenCalledTimes(1); | |
| expect(runtimeLog.mock.calls[0]?.[0]).toContain("Private API status unknown"); | |
| expect(warnSpy).not.toHaveBeenCalled(); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| const body = JSON.parse(sendCall[1].body); | |
| expect(body.method).toBeUndefined(); | |
| expect(body.selectedMessageGuid).toBeUndefined(); | |
| expect(body.partIndex).toBeUndefined(); | |
| expect(body.effectId).toBeUndefined(); | |
| } finally { | |
| clearBlueBubblesRuntime(); | |
| warnSpy.mockRestore(); | |
| } | |
| }); | |
| it("sends message with chat_guid target directly", async () => { | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| text: () => | |
| Promise.resolve( | |
| JSON.stringify({ | |
| data: { messageId: "direct-msg-123" }, | |
| }), | |
| ), | |
| }); | |
| const result = await sendMessageBlueBubbles( | |
| "chat_guid:iMessage;-;direct-chat", | |
| "Direct message", | |
| { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }, | |
| ); | |
| expect(result.messageId).toBe("direct-msg-123"); | |
| expect(mockFetch).toHaveBeenCalledTimes(1); | |
| }); | |
| it("handles send failure", async () => { | |
| mockResolvedHandleTarget(); | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: false, | |
| status: 500, | |
| text: () => Promise.resolve("Internal server error"), | |
| }); | |
| await expect( | |
| sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }), | |
| ).rejects.toThrow("send failed (500)"); | |
| }); | |
| it("handles empty response body", async () => { | |
| mockResolvedHandleTarget(); | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| text: () => Promise.resolve(""), | |
| }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("ok"); | |
| }); | |
| it("handles invalid JSON response body", async () => { | |
| mockResolvedHandleTarget(); | |
| mockFetch.mockResolvedValueOnce({ | |
| ok: true, | |
| text: () => Promise.resolve("not valid json"), | |
| }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("ok"); | |
| }); | |
| it("extracts messageId from various response formats", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ id: "numeric-id-456" }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("numeric-id-456"); | |
| }); | |
| it("extracts messageGuid from response payload", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { messageGuid: "msg-guid-789" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("msg-guid-789"); | |
| }); | |
| it("extracts top-level message_id from response payload", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ message_id: "bb-msg-321" }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("bb-msg-321"); | |
| }); | |
| it("extracts nested result.message_id from response payload", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ result: { message_id: "bb-msg-654" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| expect(result.messageId).toBe("bb-msg-654"); | |
| }); | |
| it("resolves credentials from config", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg-123" } }); | |
| const result = await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| cfg: { | |
| channels: { | |
| bluebubbles: { | |
| serverUrl: "http://config-server:5678", | |
| password: "config-pass", | |
| }, | |
| }, | |
| }, | |
| }); | |
| expect(result.messageId).toBe("msg-123"); | |
| const calledUrl = mockFetch.mock.calls[0][0] as string; | |
| expect(calledUrl).toContain("config-server:5678"); | |
| }); | |
| it("includes tempGuid in request payload", async () => { | |
| mockResolvedHandleTarget(); | |
| mockSendResponse({ data: { guid: "msg" } }); | |
| await sendMessageBlueBubbles("+15551234567", "Hello", { | |
| serverUrl: "http://localhost:1234", | |
| password: "test", | |
| }); | |
| const sendCall = mockFetch.mock.calls[1]; | |
| const body = JSON.parse(sendCall[1].body); | |
| expect(body.tempGuid).toBeDefined(); | |
| expect(typeof body.tempGuid).toBe("string"); | |
| expect(body.tempGuid.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| }); | |