test: dedupe fixtures and test harness setup

This commit is contained in:
Peter Steinberger
2026-02-23 05:43:30 +00:00
parent 8af19ddc5b
commit 1c753ea786
75 changed files with 1886 additions and 2136 deletions

View File

@@ -47,6 +47,22 @@ describe("bluebubblesMessageActions", () => {
const handleAction = bluebubblesMessageActions.handleAction!;
const callHandleAction = (ctx: Omit<Parameters<typeof handleAction>[0], "channel">) =>
handleAction({ channel: "bluebubbles", ...ctx });
const blueBubblesConfig = (): OpenClawConfig => ({
channels: {
bluebubbles: {
serverUrl: "http://localhost:1234",
password: "test-password",
},
},
});
const runReactAction = async (params: Record<string, unknown>) => {
return await callHandleAction({
action: "react",
params,
cfg: blueBubblesConfig(),
accountId: null,
});
};
beforeEach(() => {
vi.clearAllMocks();
@@ -285,23 +301,10 @@ describe("bluebubblesMessageActions", () => {
it("sends reaction successfully with chatGuid", async () => {
const { sendBlueBubblesReaction } = await import("./reactions.js");
const cfg: OpenClawConfig = {
channels: {
bluebubbles: {
serverUrl: "http://localhost:1234",
password: "test-password",
},
},
};
const result = await callHandleAction({
action: "react",
params: {
emoji: "❤️",
messageId: "msg-123",
chatGuid: "iMessage;-;+15551234567",
},
cfg,
accountId: null,
const result = await runReactAction({
emoji: "❤️",
messageId: "msg-123",
chatGuid: "iMessage;-;+15551234567",
});
expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
@@ -320,24 +323,11 @@ describe("bluebubblesMessageActions", () => {
it("sends reaction removal successfully", async () => {
const { sendBlueBubblesReaction } = await import("./reactions.js");
const cfg: OpenClawConfig = {
channels: {
bluebubbles: {
serverUrl: "http://localhost:1234",
password: "test-password",
},
},
};
const result = await callHandleAction({
action: "react",
params: {
emoji: "❤️",
messageId: "msg-123",
chatGuid: "iMessage;-;+15551234567",
remove: true,
},
cfg,
accountId: null,
const result = await runReactAction({
emoji: "❤️",
messageId: "msg-123",
chatGuid: "iMessage;-;+15551234567",
remove: true,
});
expect(sendBlueBubblesReaction).toHaveBeenCalledWith(

View File

@@ -64,6 +64,24 @@ describe("downloadBlueBubblesAttachment", () => {
setBlueBubblesRuntime(runtimeStub);
});
async function expectAttachmentTooLarge(params: { bufferBytes: number; maxBytes?: number }) {
const largeBuffer = new Uint8Array(params.bufferBytes);
mockFetch.mockResolvedValueOnce({
ok: true,
headers: new Headers(),
arrayBuffer: () => Promise.resolve(largeBuffer.buffer),
});
const attachment: BlueBubblesAttachment = { guid: "att-large" };
await expect(
downloadBlueBubblesAttachment(attachment, {
serverUrl: "http://localhost:1234",
password: "test",
...(params.maxBytes === undefined ? {} : { maxBytes: params.maxBytes }),
}),
).rejects.toThrow("too large");
}
it("throws when guid is missing", async () => {
const attachment: BlueBubblesAttachment = {};
await expect(
@@ -175,38 +193,14 @@ describe("downloadBlueBubblesAttachment", () => {
});
it("throws when attachment exceeds max bytes", async () => {
const largeBuffer = new Uint8Array(10 * 1024 * 1024);
mockFetch.mockResolvedValueOnce({
ok: true,
headers: new Headers(),
arrayBuffer: () => Promise.resolve(largeBuffer.buffer),
await expectAttachmentTooLarge({
bufferBytes: 10 * 1024 * 1024,
maxBytes: 5 * 1024 * 1024,
});
const attachment: BlueBubblesAttachment = { guid: "att-large" };
await expect(
downloadBlueBubblesAttachment(attachment, {
serverUrl: "http://localhost:1234",
password: "test",
maxBytes: 5 * 1024 * 1024,
}),
).rejects.toThrow("too large");
});
it("uses default max bytes when not specified", async () => {
const largeBuffer = new Uint8Array(9 * 1024 * 1024);
mockFetch.mockResolvedValueOnce({
ok: true,
headers: new Headers(),
arrayBuffer: () => Promise.resolve(largeBuffer.buffer),
});
const attachment: BlueBubblesAttachment = { guid: "att-large" };
await expect(
downloadBlueBubblesAttachment(attachment, {
serverUrl: "http://localhost:1234",
password: "test",
}),
).rejects.toThrow("too large");
await expectAttachmentTooLarge({ bufferBytes: 9 * 1024 * 1024 });
});
it("uses attachment mimeType as fallback when response has no content-type", async () => {

View File

@@ -22,6 +22,44 @@ installBlueBubblesFetchTestHooks({
});
describe("chat", () => {
function mockOkTextResponse() {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
});
}
async function expectCalledUrlIncludesPassword(params: {
password: string;
invoke: () => Promise<void>;
}) {
mockOkTextResponse();
await params.invoke();
const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toContain(`password=${params.password}`);
}
async function expectCalledUrlUsesConfigCredentials(params: {
serverHost: string;
password: string;
invoke: (cfg: {
channels: { bluebubbles: { serverUrl: string; password: string } };
}) => Promise<void>;
}) {
mockOkTextResponse();
await params.invoke({
channels: {
bluebubbles: {
serverUrl: `http://${params.serverHost}`,
password: params.password,
},
},
});
const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toContain(params.serverHost);
expect(calledUrl).toContain(`password=${params.password}`);
}
describe("markBlueBubblesChatRead", () => {
it("does nothing when chatGuid is empty or whitespace", async () => {
for (const chatGuid of ["", " "]) {
@@ -73,18 +111,14 @@ describe("chat", () => {
});
it("includes password in URL query", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
});
await markBlueBubblesChatRead("chat-123", {
serverUrl: "http://localhost:1234",
await expectCalledUrlIncludesPassword({
password: "my-secret",
invoke: () =>
markBlueBubblesChatRead("chat-123", {
serverUrl: "http://localhost:1234",
password: "my-secret",
}),
});
const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toContain("password=my-secret");
});
it("throws on non-ok response", async () => {
@@ -119,25 +153,14 @@ describe("chat", () => {
});
it("resolves credentials from config", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
await expectCalledUrlUsesConfigCredentials({
serverHost: "config-server:9999",
password: "config-pass",
invoke: (cfg) =>
markBlueBubblesChatRead("chat-123", {
cfg,
}),
});
await markBlueBubblesChatRead("chat-123", {
cfg: {
channels: {
bluebubbles: {
serverUrl: "http://config-server:9999",
password: "config-pass",
},
},
},
});
const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toContain("config-server:9999");
expect(calledUrl).toContain("password=config-pass");
});
});
@@ -536,18 +559,14 @@ describe("chat", () => {
});
it("includes password in URL query", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
});
await setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
serverUrl: "http://localhost:1234",
await expectCalledUrlIncludesPassword({
password: "my-secret",
invoke: () =>
setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
serverUrl: "http://localhost:1234",
password: "my-secret",
}),
});
const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toContain("password=my-secret");
});
it("throws on non-ok response", async () => {
@@ -582,25 +601,14 @@ describe("chat", () => {
});
it("resolves credentials from config", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
await expectCalledUrlUsesConfigCredentials({
serverHost: "config-server:9999",
password: "config-pass",
invoke: (cfg) =>
setGroupIconBlueBubbles("chat-123", new Uint8Array([1]), "icon.png", {
cfg,
}),
});
await setGroupIconBlueBubbles("chat-123", new Uint8Array([1]), "icon.png", {
cfg: {
channels: {
bluebubbles: {
serverUrl: "http://config-server:9999",
password: "config-pass",
},
},
},
});
const calledUrl = mockFetch.mock.calls[0][0] as string;
expect(calledUrl).toContain("config-server:9999");
expect(calledUrl).toContain("password=config-pass");
});
it("includes filename in multipart body", async () => {

View File

@@ -19,6 +19,27 @@ describe("reactions", () => {
});
describe("sendBlueBubblesReaction", () => {
async function expectRemovedReaction(emoji: string) {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
});
await sendBlueBubblesReaction({
chatGuid: "chat-123",
messageGuid: "msg-123",
emoji,
remove: true,
opts: {
serverUrl: "http://localhost:1234",
password: "test",
},
});
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.reaction).toBe("-love");
}
it("throws when chatGuid is empty", async () => {
await expect(
sendBlueBubblesReaction({
@@ -208,45 +229,11 @@ describe("reactions", () => {
});
it("sends reaction removal with dash prefix", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
});
await sendBlueBubblesReaction({
chatGuid: "chat-123",
messageGuid: "msg-123",
emoji: "love",
remove: true,
opts: {
serverUrl: "http://localhost:1234",
password: "test",
},
});
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.reaction).toBe("-love");
await expectRemovedReaction("love");
});
it("strips leading dash from emoji when remove flag is set", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(""),
});
await sendBlueBubblesReaction({
chatGuid: "chat-123",
messageGuid: "msg-123",
emoji: "-love",
remove: true,
opts: {
serverUrl: "http://localhost:1234",
password: "test",
},
});
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.reaction).toBe("-love");
await expectRemovedReaction("-love");
});
it("uses custom partIndex when provided", async () => {

View File

@@ -44,6 +44,23 @@ function mockSendResponse(body: unknown) {
});
}
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>>) => {
@@ -453,20 +470,7 @@ describe("send", () => {
});
it("strips markdown when creating a new chat", async () => {
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: [] }),
})
.mockResolvedValueOnce({
ok: true,
text: () =>
Promise.resolve(
JSON.stringify({
data: { guid: "new-msg-stripped" },
}),
),
});
mockNewChatSendResponse("new-msg-stripped");
const result = await sendMessageBlueBubbles("+15550009999", "**Welcome** to the _chat_!", {
serverUrl: "http://localhost:1234",
@@ -483,20 +487,7 @@ describe("send", () => {
});
it("creates a new chat when handle target is missing", async () => {
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: [] }),
})
.mockResolvedValueOnce({
ok: true,
text: () =>
Promise.resolve(
JSON.stringify({
data: { guid: "new-msg-guid" },
}),
),
});
mockNewChatSendResponse("new-msg-guid");
const result = await sendMessageBlueBubbles("+15550009999", "Hello new chat", {
serverUrl: "http://localhost:1234",