refactor(test): share discord send rest harness

This commit is contained in:
Peter Steinberger
2026-02-14 21:16:57 +00:00
parent 0b59c48087
commit b97191b81a
3 changed files with 97 additions and 118 deletions

View File

@@ -14,43 +14,12 @@ import {
sendMessageDiscord,
unpinMessageDiscord,
} from "./send.js";
import { makeDiscordRest } from "./send.test-harness.js";
vi.mock("../web/media.js", () => ({
loadWebMedia: vi.fn().mockResolvedValue({
buffer: Buffer.from("img"),
fileName: "photo.jpg",
contentType: "image/jpeg",
kind: "image",
}),
loadWebMediaRaw: vi.fn().mockResolvedValue({
buffer: Buffer.from("img"),
fileName: "asset.png",
contentType: "image/png",
kind: "image",
}),
}));
const makeRest = () => {
const postMock = vi.fn();
const putMock = vi.fn();
const getMock = vi.fn();
const patchMock = vi.fn();
const deleteMock = vi.fn();
return {
rest: {
post: postMock,
put: putMock,
get: getMock,
patch: patchMock,
delete: deleteMock,
} as unknown as import("@buape/carbon").RequestClient,
postMock,
putMock,
getMock,
patchMock,
deleteMock,
};
};
vi.mock("../web/media.js", async () => {
const { discordWebMediaMockFactory } = await import("./send.test-harness.js");
return discordWebMediaMockFactory();
});
describe("sendMessageDiscord", () => {
beforeEach(() => {
@@ -58,7 +27,7 @@ describe("sendMessageDiscord", () => {
});
it("sends basic channel messages", async () => {
const { rest, postMock, getMock } = makeRest();
const { rest, postMock, getMock } = makeDiscordRest();
// Channel type lookup returns a normal text channel (not a forum).
getMock.mockResolvedValueOnce({ type: ChannelType.GuildText });
postMock.mockResolvedValue({
@@ -77,7 +46,7 @@ describe("sendMessageDiscord", () => {
});
it("auto-creates a forum thread when target is a Forum channel", async () => {
const { rest, postMock, getMock } = makeRest();
const { rest, postMock, getMock } = makeDiscordRest();
// Channel type lookup returns a Forum channel.
getMock.mockResolvedValueOnce({ type: ChannelType.GuildForum });
postMock.mockResolvedValue({
@@ -102,7 +71,7 @@ describe("sendMessageDiscord", () => {
});
it("posts media as a follow-up message in forum channels", async () => {
const { rest, postMock, getMock } = makeRest();
const { rest, postMock, getMock } = makeDiscordRest();
getMock.mockResolvedValueOnce({ type: ChannelType.GuildForum });
postMock
.mockResolvedValueOnce({
@@ -138,7 +107,7 @@ describe("sendMessageDiscord", () => {
});
it("chunks long forum posts into follow-up messages", async () => {
const { rest, postMock, getMock } = makeRest();
const { rest, postMock, getMock } = makeDiscordRest();
getMock.mockResolvedValueOnce({ type: ChannelType.GuildForum });
postMock
.mockResolvedValueOnce({
@@ -160,7 +129,7 @@ describe("sendMessageDiscord", () => {
});
it("starts DM when recipient is a user", async () => {
const { rest, postMock } = makeRest();
const { rest, postMock } = makeDiscordRest();
postMock
.mockResolvedValueOnce({ id: "chan1" })
.mockResolvedValueOnce({ id: "msg1", channel_id: "chan1" });
@@ -182,7 +151,7 @@ describe("sendMessageDiscord", () => {
});
it("rejects bare numeric IDs as ambiguous", async () => {
const { rest } = makeRest();
const { rest } = makeDiscordRest();
await expect(
sendMessageDiscord("273512430271856640", "hello", { rest, token: "t" }),
).rejects.toThrow(/Ambiguous Discord recipient/);
@@ -195,7 +164,7 @@ describe("sendMessageDiscord", () => {
});
it("adds missing permission hints on 50013", async () => {
const { rest, postMock, getMock } = makeRest();
const { rest, postMock, getMock } = makeDiscordRest();
const perms = PermissionFlagsBits.ViewChannel;
const apiError = Object.assign(new Error("Missing Permissions"), {
code: 50013,
@@ -228,7 +197,7 @@ describe("sendMessageDiscord", () => {
});
it("uploads media attachments", async () => {
const { rest, postMock } = makeRest();
const { rest, postMock } = makeDiscordRest();
postMock.mockResolvedValue({ id: "msg", channel_id: "789" });
const res = await sendMessageDiscord("channel:789", "photo", {
rest,
@@ -247,7 +216,7 @@ describe("sendMessageDiscord", () => {
});
it("sends media with empty text without content field", async () => {
const { rest, postMock } = makeRest();
const { rest, postMock } = makeDiscordRest();
postMock.mockResolvedValue({ id: "msg", channel_id: "789" });
const res = await sendMessageDiscord("channel:789", "", {
rest,
@@ -261,7 +230,7 @@ describe("sendMessageDiscord", () => {
});
it("preserves whitespace in media captions", async () => {
const { rest, postMock } = makeRest();
const { rest, postMock } = makeDiscordRest();
postMock.mockResolvedValue({ id: "msg", channel_id: "789" });
await sendMessageDiscord("channel:789", " spaced ", {
rest,
@@ -273,7 +242,7 @@ describe("sendMessageDiscord", () => {
});
it("includes message_reference when replying", async () => {
const { rest, postMock } = makeRest();
const { rest, postMock } = makeDiscordRest();
postMock.mockResolvedValue({ id: "msg1", channel_id: "789" });
await sendMessageDiscord("channel:789", "hello", {
rest,
@@ -288,7 +257,7 @@ describe("sendMessageDiscord", () => {
});
it("replies only on the first chunk", async () => {
const { rest, postMock } = makeRest();
const { rest, postMock } = makeDiscordRest();
postMock.mockResolvedValue({ id: "msg1", channel_id: "789" });
await sendMessageDiscord("channel:789", "a".repeat(2001), {
rest,
@@ -312,7 +281,7 @@ describe("reactMessageDiscord", () => {
});
it("reacts with unicode emoji", async () => {
const { rest, putMock } = makeRest();
const { rest, putMock } = makeDiscordRest();
await reactMessageDiscord("chan1", "msg1", "✅", { rest, token: "t" });
expect(putMock).toHaveBeenCalledWith(
Routes.channelMessageOwnReaction("chan1", "msg1", "%E2%9C%85"),
@@ -320,7 +289,7 @@ describe("reactMessageDiscord", () => {
});
it("normalizes variation selectors in unicode emoji", async () => {
const { rest, putMock } = makeRest();
const { rest, putMock } = makeDiscordRest();
await reactMessageDiscord("chan1", "msg1", "⭐️", { rest, token: "t" });
expect(putMock).toHaveBeenCalledWith(
Routes.channelMessageOwnReaction("chan1", "msg1", "%E2%AD%90"),
@@ -328,7 +297,7 @@ describe("reactMessageDiscord", () => {
});
it("reacts with custom emoji syntax", async () => {
const { rest, putMock } = makeRest();
const { rest, putMock } = makeDiscordRest();
await reactMessageDiscord("chan1", "msg1", "<:party_blob:123>", {
rest,
token: "t",
@@ -345,7 +314,7 @@ describe("removeReactionDiscord", () => {
});
it("removes a unicode emoji reaction", async () => {
const { rest, deleteMock } = makeRest();
const { rest, deleteMock } = makeDiscordRest();
await removeReactionDiscord("chan1", "msg1", "✅", { rest, token: "t" });
expect(deleteMock).toHaveBeenCalledWith(
Routes.channelMessageOwnReaction("chan1", "msg1", "%E2%9C%85"),
@@ -359,7 +328,7 @@ describe("removeOwnReactionsDiscord", () => {
});
it("removes all own reactions on a message", async () => {
const { rest, getMock, deleteMock } = makeRest();
const { rest, getMock, deleteMock } = makeDiscordRest();
getMock.mockResolvedValue({
reactions: [
{ emoji: { name: "✅", id: null } },
@@ -386,7 +355,7 @@ describe("fetchReactionsDiscord", () => {
});
it("returns reactions with users", async () => {
const { rest, getMock } = makeRest();
const { rest, getMock } = makeDiscordRest();
getMock
.mockResolvedValueOnce({
reactions: [
@@ -421,7 +390,7 @@ describe("fetchChannelPermissionsDiscord", () => {
});
it("calculates permissions from guild roles", async () => {
const { rest, getMock } = makeRest();
const { rest, getMock } = makeDiscordRest();
const perms = PermissionFlagsBits.ViewChannel | PermissionFlagsBits.SendMessages;
getMock
.mockResolvedValueOnce({
@@ -449,7 +418,7 @@ describe("fetchChannelPermissionsDiscord", () => {
});
it("treats Administrator as all permissions despite overwrites", async () => {
const { rest, getMock } = makeRest();
const { rest, getMock } = makeDiscordRest();
getMock
.mockResolvedValueOnce({
id: "chan1",
@@ -483,7 +452,7 @@ describe("readMessagesDiscord", () => {
});
it("passes query params as an object", async () => {
const { rest, getMock } = makeRest();
const { rest, getMock } = makeDiscordRest();
getMock.mockResolvedValue([]);
await readMessagesDiscord("chan1", { limit: 5, before: "10" }, { rest, token: "t" });
const call = getMock.mock.calls[0];
@@ -498,7 +467,7 @@ describe("edit/delete message helpers", () => {
});
it("edits message content", async () => {
const { rest, patchMock } = makeRest();
const { rest, patchMock } = makeDiscordRest();
patchMock.mockResolvedValue({ id: "m1" });
await editMessageDiscord("chan1", "m1", { content: "hello" }, { rest, token: "t" });
expect(patchMock).toHaveBeenCalledWith(
@@ -508,7 +477,7 @@ describe("edit/delete message helpers", () => {
});
it("deletes message", async () => {
const { rest, deleteMock } = makeRest();
const { rest, deleteMock } = makeDiscordRest();
deleteMock.mockResolvedValue({});
await deleteMessageDiscord("chan1", "m1", { rest, token: "t" });
expect(deleteMock).toHaveBeenCalledWith(Routes.channelMessage("chan1", "m1"));
@@ -521,7 +490,7 @@ describe("pin helpers", () => {
});
it("pins and unpins messages", async () => {
const { rest, putMock, deleteMock } = makeRest();
const { rest, putMock, deleteMock } = makeDiscordRest();
putMock.mockResolvedValue({});
deleteMock.mockResolvedValue({});
await pinMessageDiscord("chan1", "m1", { rest, token: "t" });
@@ -537,7 +506,7 @@ describe("searchMessagesDiscord", () => {
});
it("uses URLSearchParams for search", async () => {
const { rest, getMock } = makeRest();
const { rest, getMock } = makeDiscordRest();
getMock.mockResolvedValue({ total_results: 0, messages: [] });
await searchMessagesDiscord(
{ guildId: "g1", content: "hello", limit: 5 },
@@ -548,7 +517,7 @@ describe("searchMessagesDiscord", () => {
});
it("supports channel/author arrays and clamps limit", async () => {
const { rest, getMock } = makeRest();
const { rest, getMock } = makeDiscordRest();
getMock.mockResolvedValue({ total_results: 0, messages: [] });
await searchMessagesDiscord(
{