mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 11:20:21 +00:00
refactor(test): dedupe bluebubbles webhook helpers
This commit is contained in:
@@ -12,14 +12,16 @@ import { resetBlueBubblesSelfChatCache } from "./monitor-self-chat-cache.js";
|
||||
import { handleBlueBubblesWebhookRequest, resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
import {
|
||||
createMockAccount,
|
||||
createMessageReactionPayloadForTest,
|
||||
createNewMessagePayloadForTest,
|
||||
createMockRequest,
|
||||
createMockResponse,
|
||||
createTimestampedNewMessagePayloadForTest,
|
||||
createTimestampedMessageReactionPayloadForTest,
|
||||
dispatchWebhookRequestForTest,
|
||||
dispatchWebhookPayloadForTest,
|
||||
flushAsync,
|
||||
setupWebhookTargetForTest,
|
||||
setupWebhookTargetsForTest,
|
||||
trackWebhookRegistrationForTest,
|
||||
} from "./monitor.webhook.test-helpers.js";
|
||||
import type { OpenClawConfig, PluginRuntime } from "./runtime-api.js";
|
||||
|
||||
@@ -148,13 +150,17 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
config?: OpenClawConfig;
|
||||
core?: PluginRuntime;
|
||||
}) {
|
||||
const registration = setupWebhookTargetForTest({
|
||||
createCore: createMockRuntime,
|
||||
core: params?.core,
|
||||
account: params?.account,
|
||||
config: params?.config,
|
||||
});
|
||||
unregister = registration.unregister;
|
||||
const registration = trackWebhookRegistrationForTest(
|
||||
setupWebhookTargetForTest({
|
||||
createCore: createMockRuntime,
|
||||
core: params?.core,
|
||||
account: params?.account,
|
||||
config: params?.config,
|
||||
}),
|
||||
(nextUnregister) => {
|
||||
unregister = nextUnregister;
|
||||
},
|
||||
);
|
||||
return { core: registration.core };
|
||||
}
|
||||
|
||||
@@ -163,10 +169,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}
|
||||
|
||||
async function dispatchWebhookPayloadDirect(payload: unknown, url = "/bluebubbles-webhook") {
|
||||
return handleBlueBubblesWebhookRequest(
|
||||
const { handled } = await dispatchWebhookRequestForTest(
|
||||
createMockRequest("POST", url, payload),
|
||||
createMockResponse(),
|
||||
);
|
||||
return handled;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -197,9 +203,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from allowed sender",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
const res = await dispatchWebhookPayload(payload);
|
||||
@@ -216,9 +221,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from blocked sender",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
const res = await dispatchWebhookPayload(payload);
|
||||
@@ -235,9 +239,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from blocked sender",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
const res = await dispatchWebhookPayload(payload);
|
||||
@@ -255,7 +258,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({ date: Date.now() });
|
||||
const payload = createTimestampedNewMessagePayloadForTest();
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
@@ -271,7 +274,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({ date: Date.now() });
|
||||
const payload = createTimestampedNewMessagePayloadForTest();
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
@@ -289,10 +292,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello again",
|
||||
guid: "msg-2",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -311,10 +313,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from anyone",
|
||||
handle: { address: "+15559999999" },
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -329,7 +330,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({ date: Date.now() });
|
||||
const payload = createTimestampedNewMessagePayloadForTest();
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
@@ -345,11 +346,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -364,11 +364,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -384,10 +383,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from group",
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -403,11 +401,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from allowed group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -421,15 +418,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
mockResolveRequireMention.mockReturnValue(true);
|
||||
mockMatchesMentionPatterns.mockReturnValue(true);
|
||||
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "bert, can you help me?",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -443,15 +437,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
mockResolveRequireMention.mockReturnValue(true);
|
||||
mockMatchesMentionPatterns.mockReturnValue(false);
|
||||
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello everyone",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -462,15 +453,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
it("processes group message without mention when requireMention=false", async () => {
|
||||
mockResolveRequireMention.mockReturnValue(false);
|
||||
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello everyone",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -481,11 +469,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
describe("group metadata", () => {
|
||||
it("includes group subject + members in ctx", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
@@ -494,7 +480,6 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
{ address: "+15551234567", displayName: "Alice" },
|
||||
{ address: "+15557654321", displayName: "Bob" },
|
||||
],
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -508,17 +493,14 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
describe("group sender identity in envelope", () => {
|
||||
it("includes sender in envelope body and group label as from for group messages", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello everyone",
|
||||
senderName: "Alice",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
chatName: "Family Chat",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -540,14 +522,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("falls back to group:peerId when chatName is missing", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -564,9 +543,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
it("uses sender as from label for DM messages", async () => {
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
senderName: "Alice",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -645,23 +623,25 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
};
|
||||
}) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"];
|
||||
|
||||
const registration = setupWebhookTargetForTest({
|
||||
createCore: createMockRuntime,
|
||||
core,
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
unregister = registration.unregister;
|
||||
const registration = trackWebhookRegistrationForTest(
|
||||
setupWebhookTargetForTest({
|
||||
createCore: createMockRuntime,
|
||||
core,
|
||||
}),
|
||||
(nextUnregister) => {
|
||||
unregister = nextUnregister;
|
||||
},
|
||||
);
|
||||
|
||||
const messageId = "race-msg-1";
|
||||
const chatGuid = "iMessage;-;+15551234567";
|
||||
|
||||
const payloadA = createNewMessagePayloadForTest({
|
||||
const payloadA = createTimestampedNewMessagePayloadForTest({
|
||||
guid: messageId,
|
||||
chatGuid,
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
const payloadB = createNewMessagePayloadForTest({
|
||||
const payloadB = createTimestampedNewMessagePayloadForTest({
|
||||
guid: messageId,
|
||||
chatGuid,
|
||||
attachments: [
|
||||
@@ -671,7 +651,6 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
totalBytes: 1024,
|
||||
},
|
||||
],
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayloadDirect(payloadA);
|
||||
@@ -699,11 +678,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
describe("reply metadata", () => {
|
||||
it("surfaces reply fields in ctx when provided", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "replying now",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
replyTo: {
|
||||
@@ -711,7 +688,6 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
text: "original message",
|
||||
handle: { address: "+15550000000", displayName: "Alice" },
|
||||
},
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -727,11 +703,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("preserves part index prefixes in reply tags when short IDs are unavailable", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "replying now",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
replyTo: {
|
||||
@@ -739,7 +713,6 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
text: "original message",
|
||||
handle: { address: "+15550000000", displayName: "Alice" },
|
||||
},
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -752,19 +725,16 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("hydrates missing reply sender/body from the recent-message cache", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open", groupPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const chatGuid = "iMessage;+;chat-reply-cache";
|
||||
|
||||
const originalPayload = createNewMessagePayloadForTest({
|
||||
const originalPayload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "original message (cached)",
|
||||
handle: { address: "+15550000000" },
|
||||
isGroup: true,
|
||||
guid: "cache-msg-0",
|
||||
chatGuid,
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(originalPayload);
|
||||
@@ -772,14 +742,13 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
// Only assert the reply message behavior below.
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockClear();
|
||||
|
||||
const replyPayload = createNewMessagePayloadForTest({
|
||||
const replyPayload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "replying now",
|
||||
isGroup: true,
|
||||
guid: "cache-msg-1",
|
||||
chatGuid,
|
||||
// Only the GUID is provided; sender/body must be hydrated.
|
||||
replyToMessageGuid: "cache-msg-0",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(replyPayload);
|
||||
@@ -796,15 +765,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("falls back to threadOriginatorGuid when reply metadata is absent", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "replying now",
|
||||
threadOriginatorGuid: "msg-0",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -817,14 +783,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
describe("tapback text parsing", () => {
|
||||
it("does not rewrite tapback-like text without metadata", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "Loved this idea",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -837,15 +800,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("parses tapback text with custom emoji when metadata is present", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: 'Reacted 😅 to "nice one"',
|
||||
guid: "msg-2",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -864,7 +824,6 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
vi.mocked(sendBlueBubblesReaction).mockClear();
|
||||
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
config: {
|
||||
messages: {
|
||||
ackReaction: "❤️",
|
||||
@@ -873,9 +832,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -905,11 +863,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "/status",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -929,12 +886,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "/status",
|
||||
handle: { address: "+15559999999" },
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -952,11 +908,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "/status",
|
||||
handle: { address: "+15559999999" },
|
||||
guid: "msg-dm-open-unauthorized",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -981,9 +936,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1001,9 +955,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1017,9 +970,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
||||
@@ -1043,9 +995,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
||||
@@ -1070,9 +1021,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(
|
||||
@@ -1100,9 +1050,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1129,9 +1078,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const inboundPayload = createNewMessagePayloadForTest({
|
||||
const inboundPayload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(inboundPayload);
|
||||
@@ -1139,13 +1087,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
// Send response did not include a message id, so nothing should be enqueued yet.
|
||||
expect(mockEnqueueSystemEvent).not.toHaveBeenCalled();
|
||||
|
||||
const fromMePayload = createNewMessagePayloadForTest({
|
||||
const fromMePayload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "replying now",
|
||||
handle: { address: "+15557654321" },
|
||||
isFromMe: true,
|
||||
guid: "msg-out-456",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(fromMePayload);
|
||||
@@ -1171,22 +1118,20 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const inboundPayload = createNewMessagePayloadForTest({
|
||||
const inboundPayload = createTimestampedNewMessagePayloadForTest({
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(inboundPayload);
|
||||
|
||||
expect(mockEnqueueSystemEvent).not.toHaveBeenCalled();
|
||||
|
||||
const fromMePayload = createNewMessagePayloadForTest({
|
||||
const fromMePayload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "replying now",
|
||||
handle: { address: "+15557654321" },
|
||||
isFromMe: true,
|
||||
guid: "msg-out-789",
|
||||
chatIdentifier: "+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(fromMePayload);
|
||||
@@ -1208,9 +1153,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
account: createMockAccount({ dmPolicy: "pairing", allowFrom: [] }),
|
||||
});
|
||||
|
||||
const payload = createMessageReactionPayloadForTest({
|
||||
date: Date.now(),
|
||||
});
|
||||
const payload = createTimestampedMessageReactionPayloadForTest();
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
@@ -1222,9 +1165,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createMessageReactionPayloadForTest({
|
||||
const payload = createTimestampedMessageReactionPayloadForTest({
|
||||
associatedMessageType: 2000, // Heart reaction added
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1240,9 +1182,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createMessageReactionPayloadForTest({
|
||||
const payload = createTimestampedMessageReactionPayloadForTest({
|
||||
associatedMessageType: 3000, // Heart reaction removed
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1258,9 +1199,8 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createMessageReactionPayloadForTest({
|
||||
const payload = createTimestampedMessageReactionPayloadForTest({
|
||||
isFromMe: true, // From self
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1274,10 +1214,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
setupWebhookTarget();
|
||||
|
||||
// Test thumbs up reaction (2001)
|
||||
const payload = createMessageReactionPayloadForTest({
|
||||
const payload = createTimestampedMessageReactionPayloadForTest({
|
||||
associatedMessageGuid: "msg-123",
|
||||
associatedMessageType: 2001, // Thumbs up
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1291,14 +1230,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
|
||||
describe("short message ID mapping", () => {
|
||||
it("assigns sequential short IDs to messages", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
guid: "p:1/msg-uuid-12345",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1311,14 +1247,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("resolves short ID back to UUID", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
guid: "p:1/msg-uuid-12345",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1373,29 +1306,31 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
accountId: "acc-b",
|
||||
};
|
||||
const core = createMockRuntime();
|
||||
const registration = setupWebhookTargetsForTest({
|
||||
createCore: createMockRuntime,
|
||||
core,
|
||||
accounts: [{ account: accountA }, { account: accountB }],
|
||||
});
|
||||
unregister = registration.unregister;
|
||||
trackWebhookRegistrationForTest(
|
||||
setupWebhookTargetsForTest({
|
||||
createCore: createMockRuntime,
|
||||
core,
|
||||
accounts: [{ account: accountA }, { account: accountB }],
|
||||
}),
|
||||
(nextUnregister) => {
|
||||
unregister = nextUnregister;
|
||||
},
|
||||
);
|
||||
|
||||
await dispatchWebhookPayload(
|
||||
createNewMessagePayloadForTest({
|
||||
createTimestampedNewMessagePayloadForTest({
|
||||
text: "message for account a",
|
||||
guid: "a-msg-1",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
}),
|
||||
"/bluebubbles-webhook?password=password-a",
|
||||
);
|
||||
|
||||
await dispatchWebhookPayload(
|
||||
createNewMessagePayloadForTest({
|
||||
createTimestampedNewMessagePayloadForTest({
|
||||
text: "message for account b",
|
||||
guid: "b-msg-1",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
}),
|
||||
"/bluebubbles-webhook?password=password-b",
|
||||
);
|
||||
@@ -1424,10 +1359,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(
|
||||
createNewMessagePayloadForTest({
|
||||
createTimestampedNewMessagePayloadForTest({
|
||||
text: "current text",
|
||||
chatGuid: "iMessage;-;+15550002002",
|
||||
date: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1504,11 +1438,10 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(
|
||||
createNewMessagePayloadForTest({
|
||||
createTimestampedNewMessagePayloadForTest({
|
||||
text: "latest text",
|
||||
guid: "msg-bomb-1",
|
||||
chatGuid: "iMessage;-;+15550004004",
|
||||
date: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1525,10 +1458,9 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
it("ignores messages from self (fromMe=true)", async () => {
|
||||
setupWebhookTarget();
|
||||
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "my own message",
|
||||
isFromMe: true,
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
@@ -1537,9 +1469,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("drops reflected self-chat duplicates after a confirmed assistant outbound", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const { sendMessageBlueBubbles } = await import("./send.js");
|
||||
vi.mocked(sendMessageBlueBubbles).mockResolvedValueOnce({ messageId: "msg-self-1" });
|
||||
@@ -1584,15 +1514,12 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("does not drop inbound messages when no fromMe self-chat copy was seen", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const inboundPayload = createNewMessagePayloadForTest({
|
||||
const inboundPayload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "genuinely new message",
|
||||
guid: "msg-inbound-1",
|
||||
chatGuid: "iMessage;-;+15551234567",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(inboundPayload);
|
||||
@@ -1604,9 +1531,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-07T00:00:00Z"));
|
||||
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const fromMePayload = createNewMessagePayloadForTest({
|
||||
@@ -1637,9 +1562,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("does not cache regular fromMe DMs as self-chat reflections", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const fromMePayload = createNewMessagePayloadForTest({
|
||||
@@ -1668,9 +1591,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("does not drop user-authored self-chat prompts without a confirmed assistant outbound", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const fromMePayload = createNewMessagePayloadForTest({
|
||||
@@ -1698,9 +1619,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("does not treat a pending text-only match as confirmed assistant outbound", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const { sendMessageBlueBubbles } = await import("./send.js");
|
||||
vi.mocked(sendMessageBlueBubbles).mockResolvedValueOnce({ messageId: "ok" });
|
||||
@@ -1745,9 +1664,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("does not treat chatGuid-inferred sender ids as self-chat evidence", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({ dmPolicy: "open" }),
|
||||
});
|
||||
setupWebhookTarget();
|
||||
|
||||
const timestamp = Date.now();
|
||||
const fromMePayload = createNewMessagePayloadForTest({
|
||||
|
||||
@@ -10,19 +10,21 @@ import { fetchBlueBubblesHistory } from "./history.js";
|
||||
import { handleBlueBubblesWebhookRequest, resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
import {
|
||||
LOOPBACK_REMOTE_ADDRESSES_FOR_TEST,
|
||||
createWebhookDispatchForTest,
|
||||
createMockAccount,
|
||||
createHangingWebhookRequestForTest,
|
||||
createMockResponse,
|
||||
createLoopbackWebhookRequestParamsForTest,
|
||||
createNewMessagePayloadForTest,
|
||||
createPasswordQueryRequestParamsForTest,
|
||||
createProtectedWebhookAccountForTest,
|
||||
createRemoteWebhookRequestParamsForTest,
|
||||
createTimestampedNewMessagePayloadForTest,
|
||||
dispatchWebhookPayloadForTest,
|
||||
expectWebhookRequestStatusForTest,
|
||||
expectWebhookStatusForTest,
|
||||
setupWebhookTargetForTest,
|
||||
setupWebhookTargetsForTest,
|
||||
trackWebhookRegistrationForTest,
|
||||
type WebhookRequestParams,
|
||||
} from "./monitor.webhook.test-helpers.js";
|
||||
import type { OpenClawConfig, PluginRuntime } from "./runtime-api.js";
|
||||
|
||||
@@ -162,14 +164,18 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
core?: PluginRuntime;
|
||||
statusSink?: (event: unknown) => void;
|
||||
}) {
|
||||
const registration = setupWebhookTargetForTest({
|
||||
createCore: createMockRuntime,
|
||||
core: params?.core,
|
||||
account: params?.account,
|
||||
config: params?.config,
|
||||
statusSink: params?.statusSink,
|
||||
});
|
||||
unregister = registration.unregister;
|
||||
const registration = trackWebhookRegistrationForTest(
|
||||
setupWebhookTargetForTest({
|
||||
createCore: createMockRuntime,
|
||||
core: params?.core,
|
||||
account: params?.account,
|
||||
config: params?.config,
|
||||
statusSink: params?.statusSink,
|
||||
}),
|
||||
(nextUnregister) => {
|
||||
unregister = nextUnregister;
|
||||
},
|
||||
);
|
||||
return {
|
||||
account: registration.account,
|
||||
config: registration.config,
|
||||
@@ -183,43 +189,72 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
return account;
|
||||
}
|
||||
|
||||
function createWebhookTarget(
|
||||
account: ResolvedBlueBubblesAccount,
|
||||
statusSink: (event: unknown) => void = vi.fn(),
|
||||
) {
|
||||
return { account, statusSink };
|
||||
}
|
||||
|
||||
function createProtectedWebhookTarget(password = TEST_WEBHOOK_PASSWORD) {
|
||||
return createWebhookTarget(createProtectedWebhookAccountForTest(password));
|
||||
}
|
||||
|
||||
async function expectProtectedWebhookRequestStatus(
|
||||
params: WebhookRequestParams,
|
||||
expectedStatus: number,
|
||||
expectedBody?: string,
|
||||
) {
|
||||
setupProtectedWebhookTarget();
|
||||
return expectWebhookRequestStatusForTest(params, expectedStatus, expectedBody);
|
||||
}
|
||||
|
||||
async function expectRegisteredWebhookRequestStatus(
|
||||
params: WebhookRequestParams,
|
||||
expectedStatus: number,
|
||||
expectedBody?: string,
|
||||
) {
|
||||
setupWebhookTarget();
|
||||
return expectWebhookRequestStatusForTest(params, expectedStatus, expectedBody);
|
||||
}
|
||||
|
||||
function registerWebhookTargets(
|
||||
params: Array<{
|
||||
account: ResolvedBlueBubblesAccount;
|
||||
statusSink?: (event: unknown) => void;
|
||||
}>,
|
||||
) {
|
||||
const registration = setupWebhookTargetsForTest({
|
||||
createCore: createMockRuntime,
|
||||
accounts: params,
|
||||
});
|
||||
unregister = registration.unregister;
|
||||
trackWebhookRegistrationForTest(
|
||||
setupWebhookTargetsForTest({
|
||||
createCore: createMockRuntime,
|
||||
accounts: params,
|
||||
}),
|
||||
(nextUnregister) => {
|
||||
unregister = nextUnregister;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("webhook parsing + auth handling", () => {
|
||||
it("rejects non-POST requests", async () => {
|
||||
setupWebhookTarget();
|
||||
await expectWebhookRequestStatusForTest({ method: "GET" }, 405);
|
||||
await expectRegisteredWebhookRequestStatus({ method: "GET" }, 405);
|
||||
});
|
||||
|
||||
it("accepts POST requests with valid JSON payload", async () => {
|
||||
setupWebhookTarget();
|
||||
const payload = createNewMessagePayloadForTest({ date: Date.now() });
|
||||
await expectWebhookRequestStatusForTest({ body: payload }, 200, "ok");
|
||||
const payload = createTimestampedNewMessagePayloadForTest();
|
||||
await expectRegisteredWebhookRequestStatus({ body: payload }, 200, "ok");
|
||||
});
|
||||
|
||||
it("rejects requests with invalid JSON", async () => {
|
||||
setupWebhookTarget();
|
||||
await expectWebhookRequestStatusForTest({ body: "invalid json {{" }, 400);
|
||||
await expectRegisteredWebhookRequestStatus({ body: "invalid json {{" }, 400);
|
||||
});
|
||||
|
||||
it("accepts URL-encoded payload wrappers", async () => {
|
||||
setupWebhookTarget();
|
||||
const payload = createNewMessagePayloadForTest({ date: Date.now() });
|
||||
const payload = createTimestampedNewMessagePayloadForTest();
|
||||
const encodedBody = new URLSearchParams({
|
||||
payload: JSON.stringify(payload),
|
||||
}).toString();
|
||||
await expectWebhookRequestStatusForTest({ body: encodedBody }, 200, "ok");
|
||||
await expectRegisteredWebhookRequestStatus({ body: encodedBody }, 200, "ok");
|
||||
});
|
||||
|
||||
it("returns 408 when request body times out (Slow-Loris protection)", async () => {
|
||||
@@ -230,9 +265,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
// Create a request that never sends data or ends (simulates slow-loris)
|
||||
const { req, destroyMock } = createHangingWebhookRequestForTest();
|
||||
|
||||
const res = createMockResponse();
|
||||
|
||||
const handledPromise = handleBlueBubblesWebhookRequest(req, res);
|
||||
const { res, handledPromise } = createWebhookDispatchForTest(req);
|
||||
|
||||
// Advance past the 30s timeout
|
||||
await vi.advanceTimersByTimeAsync(31_000);
|
||||
@@ -257,21 +290,15 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("authenticates via password query parameter", async () => {
|
||||
setupProtectedWebhookTarget();
|
||||
await expectWebhookRequestStatusForTest(
|
||||
createPasswordQueryRequestParamsForTest({
|
||||
body: createNewMessagePayloadForTest(),
|
||||
password: TEST_WEBHOOK_PASSWORD,
|
||||
}),
|
||||
await expectProtectedWebhookRequestStatus(
|
||||
createPasswordQueryRequestParamsForTest({ password: TEST_WEBHOOK_PASSWORD }),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
it("authenticates via x-password header", async () => {
|
||||
setupProtectedWebhookTarget();
|
||||
await expectWebhookRequestStatusForTest(
|
||||
await expectProtectedWebhookRequestStatus(
|
||||
createRemoteWebhookRequestParamsForTest({
|
||||
body: createNewMessagePayloadForTest(),
|
||||
overrides: {
|
||||
headers: { "x-password": TEST_WEBHOOK_PASSWORD }, // pragma: allowlist secret
|
||||
},
|
||||
@@ -281,65 +308,43 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
});
|
||||
|
||||
it("rejects unauthorized requests with wrong password", async () => {
|
||||
setupProtectedWebhookTarget();
|
||||
await expectWebhookRequestStatusForTest(
|
||||
createPasswordQueryRequestParamsForTest({
|
||||
body: createNewMessagePayloadForTest(),
|
||||
password: "wrong-token",
|
||||
}),
|
||||
await expectProtectedWebhookRequestStatus(
|
||||
createPasswordQueryRequestParamsForTest({ password: "wrong-token" }),
|
||||
401,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects ambiguous routing when multiple targets match the same password", async () => {
|
||||
const accountA = createProtectedWebhookAccountForTest(TEST_WEBHOOK_PASSWORD);
|
||||
const accountB = createProtectedWebhookAccountForTest(TEST_WEBHOOK_PASSWORD);
|
||||
const sinkA = vi.fn();
|
||||
const sinkB = vi.fn();
|
||||
registerWebhookTargets([
|
||||
{ account: accountA, statusSink: sinkA },
|
||||
{ account: accountB, statusSink: sinkB },
|
||||
]);
|
||||
const targetA = createProtectedWebhookTarget();
|
||||
const targetB = createProtectedWebhookTarget();
|
||||
registerWebhookTargets([targetA, targetB]);
|
||||
|
||||
await expectWebhookRequestStatusForTest(
|
||||
createPasswordQueryRequestParamsForTest({
|
||||
body: createNewMessagePayloadForTest(),
|
||||
password: TEST_WEBHOOK_PASSWORD,
|
||||
}),
|
||||
createPasswordQueryRequestParamsForTest({ password: TEST_WEBHOOK_PASSWORD }),
|
||||
401,
|
||||
);
|
||||
expect(sinkA).not.toHaveBeenCalled();
|
||||
expect(sinkB).not.toHaveBeenCalled();
|
||||
expect(targetA.statusSink).not.toHaveBeenCalled();
|
||||
expect(targetB.statusSink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores targets without passwords when a password-authenticated target matches", async () => {
|
||||
const accountStrict = createProtectedWebhookAccountForTest(TEST_WEBHOOK_PASSWORD);
|
||||
const accountWithoutPassword = createMockAccount({ password: undefined });
|
||||
const sinkStrict = vi.fn();
|
||||
const sinkWithoutPassword = vi.fn();
|
||||
registerWebhookTargets([
|
||||
{ account: accountStrict, statusSink: sinkStrict },
|
||||
{ account: accountWithoutPassword, statusSink: sinkWithoutPassword },
|
||||
]);
|
||||
const strictTarget = createProtectedWebhookTarget();
|
||||
const passwordlessTarget = createWebhookTarget(createMockAccount({ password: undefined }));
|
||||
registerWebhookTargets([strictTarget, passwordlessTarget]);
|
||||
|
||||
await expectWebhookRequestStatusForTest(
|
||||
createPasswordQueryRequestParamsForTest({
|
||||
body: createNewMessagePayloadForTest(),
|
||||
password: TEST_WEBHOOK_PASSWORD,
|
||||
}),
|
||||
createPasswordQueryRequestParamsForTest({ password: TEST_WEBHOOK_PASSWORD }),
|
||||
200,
|
||||
);
|
||||
expect(sinkStrict).toHaveBeenCalledTimes(1);
|
||||
expect(sinkWithoutPassword).not.toHaveBeenCalled();
|
||||
expect(strictTarget.statusSink).toHaveBeenCalledTimes(1);
|
||||
expect(passwordlessTarget.statusSink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for loopback requests when password is configured", async () => {
|
||||
setupProtectedWebhookTarget();
|
||||
for (const remoteAddress of LOOPBACK_REMOTE_ADDRESSES_FOR_TEST) {
|
||||
await expectWebhookRequestStatusForTest(
|
||||
createLoopbackWebhookRequestParamsForTest(remoteAddress, {
|
||||
body: createNewMessagePayloadForTest(),
|
||||
}),
|
||||
createLoopbackWebhookRequestParamsForTest(remoteAddress),
|
||||
401,
|
||||
);
|
||||
}
|
||||
@@ -357,7 +362,6 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
for (const headers of headerVariants) {
|
||||
await expectWebhookRequestStatusForTest(
|
||||
createLoopbackWebhookRequestParamsForTest("127.0.0.1", {
|
||||
body: createNewMessagePayloadForTest(),
|
||||
overrides: { headers },
|
||||
}),
|
||||
401,
|
||||
@@ -377,12 +381,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const { resolveChatGuidForTarget } = await import("./send.js");
|
||||
vi.mocked(resolveChatGuidForTarget).mockClear();
|
||||
|
||||
setupWebhookTarget({ account: createMockAccount({ groupPolicy: "open" }) });
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
setupWebhookTarget();
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from group",
|
||||
isGroup: true,
|
||||
chatId: "123",
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayloadForTest({ body: payload });
|
||||
@@ -404,12 +407,11 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
return EMPTY_DISPATCH_RESULT;
|
||||
});
|
||||
|
||||
setupWebhookTarget({ account: createMockAccount({ groupPolicy: "open" }) });
|
||||
const payload = createNewMessagePayloadForTest({
|
||||
setupWebhookTarget();
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello from group",
|
||||
isGroup: true,
|
||||
chat: { chatGuid: "iMessage;+;chat123456" },
|
||||
date: Date.now(),
|
||||
});
|
||||
|
||||
await dispatchWebhookPayloadForTest({ body: payload });
|
||||
|
||||
@@ -54,6 +54,15 @@ export function createNewMessagePayloadForTest(dataOverrides: Record<string, unk
|
||||
};
|
||||
}
|
||||
|
||||
export function createTimestampedNewMessagePayloadForTest(
|
||||
dataOverrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return createNewMessagePayloadForTest({
|
||||
...dataOverrides,
|
||||
date: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createMessageReactionPayloadForTest(dataOverrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
type: "message-reaction",
|
||||
@@ -68,6 +77,15 @@ export function createMessageReactionPayloadForTest(dataOverrides: Record<string
|
||||
};
|
||||
}
|
||||
|
||||
export function createTimestampedMessageReactionPayloadForTest(
|
||||
dataOverrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return createMessageReactionPayloadForTest({
|
||||
...dataOverrides,
|
||||
date: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createMockRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
@@ -123,7 +141,7 @@ export function createRemoteWebhookRequestParamsForTest(
|
||||
} = {},
|
||||
): WebhookRequestParams {
|
||||
return {
|
||||
body: params.body ?? {},
|
||||
body: params.body ?? createNewMessagePayloadForTest(),
|
||||
remoteAddress: params.remoteAddress ?? "192.168.1.100",
|
||||
...params.overrides,
|
||||
};
|
||||
@@ -155,7 +173,7 @@ export function createLoopbackWebhookRequestParamsForTest(
|
||||
} = {},
|
||||
): WebhookRequestParams {
|
||||
return {
|
||||
body: params.body ?? {},
|
||||
body: params.body ?? createNewMessagePayloadForTest(),
|
||||
remoteAddress,
|
||||
...params.overrides,
|
||||
};
|
||||
@@ -193,12 +211,27 @@ export async function flushAsync() {
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebhookDispatchForTest(req: IncomingMessage) {
|
||||
const res = createMockResponse();
|
||||
const handledPromise = handleBlueBubblesWebhookRequest(req, res);
|
||||
return { res, handledPromise };
|
||||
}
|
||||
|
||||
export async function dispatchWebhookRequestForTest(
|
||||
req: IncomingMessage,
|
||||
options: { flushAsyncAfter?: boolean } = {},
|
||||
) {
|
||||
const { res, handledPromise } = createWebhookDispatchForTest(req);
|
||||
const handled = await handledPromise;
|
||||
if (options.flushAsyncAfter) {
|
||||
await flushAsync();
|
||||
}
|
||||
return { handled, res };
|
||||
}
|
||||
|
||||
export async function dispatchWebhookPayloadForTest(params: WebhookRequestParams = {}) {
|
||||
const req = createMockRequestForTest(params);
|
||||
const res = createMockResponse();
|
||||
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
return { handled, res };
|
||||
return dispatchWebhookRequestForTest(req, { flushAsyncAfter: true });
|
||||
}
|
||||
|
||||
export async function expectWebhookStatusForTest(
|
||||
@@ -206,8 +239,7 @@ export async function expectWebhookStatusForTest(
|
||||
expectedStatus: number,
|
||||
expectedBody?: string,
|
||||
) {
|
||||
const res = createMockResponse();
|
||||
const handled = await handleBlueBubblesWebhookRequest(req, res);
|
||||
const { res, handled } = await dispatchWebhookRequestForTest(req);
|
||||
expect(handled).toBe(true);
|
||||
expect(res.statusCode).toBe(expectedStatus);
|
||||
if (expectedBody !== undefined) {
|
||||
@@ -224,6 +256,14 @@ export async function expectWebhookRequestStatusForTest(
|
||||
return expectWebhookStatusForTest(createMockRequestForTest(params), expectedStatus, expectedBody);
|
||||
}
|
||||
|
||||
export function trackWebhookRegistrationForTest<T extends { unregister: () => void }>(
|
||||
registration: T,
|
||||
setUnregister: (unregister: () => void) => void,
|
||||
) {
|
||||
setUnregister(registration.unregister);
|
||||
return registration;
|
||||
}
|
||||
|
||||
export function registerWebhookTargetForTest(params: {
|
||||
core: PluginRuntime;
|
||||
account?: ResolvedBlueBubblesAccount;
|
||||
|
||||
Reference in New Issue
Block a user