test(telegram): dedupe send fallback/media fixtures and trim reset overhead

This commit is contained in:
Peter Steinberger
2026-02-21 23:55:58 +00:00
parent dfe0483d80
commit 5af39b051d

View File

@@ -1,5 +1,5 @@
import type { Bot } from "grammy"; import type { Bot } from "grammy";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { import {
getTelegramSendTestMocks, getTelegramSendTestMocks,
importTelegramSendModule, importTelegramSendModule,
@@ -40,6 +40,22 @@ async function expectChatNotFoundWithChatId(
} }
} }
function mockLoadedMedia({
buffer = Buffer.from("media"),
contentType,
fileName,
}: {
buffer?: Buffer;
contentType?: string;
fileName?: string;
}): void {
loadWebMedia.mockResolvedValueOnce({
buffer,
...(contentType ? { contentType } : {}),
...(fileName ? { fileName } : {}),
});
}
describe("sent-message-cache", () => { describe("sent-message-cache", () => {
afterEach(() => { afterEach(() => {
clearSentMessageCache(); clearSentMessageCache();
@@ -189,34 +205,81 @@ describe("sendMessageTelegram", () => {
} }
}); });
it("falls back to plain text when Telegram rejects HTML", async () => { it("falls back to plain text when Telegram rejects HTML and preserves send params", async () => {
const chatId = "123";
const parseErr = new Error( const parseErr = new Error(
"400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9", "400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9",
); );
const sendMessage = vi const cases = [
.fn() {
.mockRejectedValueOnce(parseErr) name: "plain text send",
.mockResolvedValueOnce({ chatId: "123",
message_id: 42, text: "_oops_",
chat: { id: chatId }, htmlText: "<i>oops</i>",
messageId: 42,
options: { verbose: true } as const,
firstCall: { parse_mode: "HTML" },
secondCall: undefined,
},
{
name: "threaded reply send",
chatId: "-1001234567890",
text: "_bad markdown_",
htmlText: "<i>bad markdown</i>",
messageId: 60,
options: { messageThreadId: 271, replyToMessageId: 100 } as const,
firstCall: {
parse_mode: "HTML",
message_thread_id: 271,
reply_to_message_id: 100,
},
secondCall: {
message_thread_id: 271,
reply_to_message_id: 100,
},
},
] as const;
for (const testCase of cases) {
const sendMessage = vi
.fn()
.mockRejectedValueOnce(parseErr)
.mockResolvedValueOnce({
message_id: testCase.messageId,
chat: { id: testCase.chatId },
});
const api = { sendMessage } as unknown as {
sendMessage: typeof sendMessage;
};
const res = await sendMessageTelegram(testCase.chatId, testCase.text, {
token: "tok",
api,
...testCase.options,
}); });
const api = { sendMessage } as unknown as {
sendMessage: typeof sendMessage;
};
const res = await sendMessageTelegram(chatId, "_oops_", { expect(sendMessage, testCase.name).toHaveBeenNthCalledWith(
token: "tok", 1,
api, testCase.chatId,
verbose: true, testCase.htmlText,
}); testCase.firstCall,
);
expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "<i>oops</i>", { if (testCase.secondCall) {
parse_mode: "HTML", expect(sendMessage, testCase.name).toHaveBeenNthCalledWith(
}); 2,
expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "_oops_"); testCase.chatId,
expect(res.chatId).toBe(chatId); testCase.text,
expect(res.messageId).toBe("42"); testCase.secondCall,
);
} else {
expect(sendMessage, testCase.name).toHaveBeenNthCalledWith(
2,
testCase.chatId,
testCase.text,
);
}
expect(res.chatId, testCase.name).toBe(testCase.chatId);
expect(res.messageId, testCase.name).toBe(String(testCase.messageId));
}
}); });
it("keeps link_preview_options disabled for both html and plain-text fallback", async () => { it("keeps link_preview_options disabled for both html and plain-text fallback", async () => {
@@ -306,41 +369,6 @@ describe("sendMessageTelegram", () => {
}); });
}); });
it("preserves thread params in plain text fallback", async () => {
const chatId = "-1001234567890";
const parseErr = new Error(
"400: Bad Request: can't parse entities: Can't find end of the entity",
);
const sendMessage = vi
.fn()
.mockRejectedValueOnce(parseErr)
.mockResolvedValueOnce({
message_id: 60,
chat: { id: chatId },
});
const api = { sendMessage } as unknown as {
sendMessage: typeof sendMessage;
};
const res = await sendMessageTelegram(chatId, "_bad markdown_", {
token: "tok",
api,
messageThreadId: 271,
replyToMessageId: 100,
});
expect(sendMessage).toHaveBeenNthCalledWith(1, chatId, "<i>bad markdown</i>", {
parse_mode: "HTML",
message_thread_id: 271,
reply_to_message_id: 100,
});
expect(sendMessage).toHaveBeenNthCalledWith(2, chatId, "_bad markdown_", {
message_thread_id: 271,
reply_to_message_id: 100,
});
expect(res.messageId).toBe("60");
});
it("includes thread params in media messages", async () => { it("includes thread params in media messages", async () => {
const chatId = "-1001234567890"; const chatId = "-1001234567890";
const sendPhoto = vi.fn().mockResolvedValue({ const sendPhoto = vi.fn().mockResolvedValue({
@@ -351,7 +379,7 @@ describe("sendMessageTelegram", () => {
sendPhoto: typeof sendPhoto; sendPhoto: typeof sendPhoto;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-image"), buffer: Buffer.from("fake-image"),
contentType: "image/jpeg", contentType: "image/jpeg",
fileName: "photo.jpg", fileName: "photo.jpg",
@@ -388,7 +416,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage; sendMessage: typeof sendMessage;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-image"), buffer: Buffer.from("fake-image"),
contentType: "image/jpeg", contentType: "image/jpeg",
fileName: "photo.jpg", fileName: "photo.jpg",
@@ -423,7 +451,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage; sendMessage: typeof sendMessage;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-image"), buffer: Buffer.from("fake-image"),
contentType: "image/jpeg", contentType: "image/jpeg",
fileName: "photo.jpg", fileName: "photo.jpg",
@@ -455,7 +483,7 @@ describe("sendMessageTelegram", () => {
sendPhoto: typeof sendPhoto; sendPhoto: typeof sendPhoto;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-image"), buffer: Buffer.from("fake-image"),
contentType: "image/jpeg", contentType: "image/jpeg",
fileName: "photo.jpg", fileName: "photo.jpg",
@@ -491,7 +519,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage; sendMessage: typeof sendMessage;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-video"), buffer: Buffer.from("fake-video"),
contentType: "video/mp4", contentType: "video/mp4",
fileName: "video.mp4", fileName: "video.mp4",
@@ -521,7 +549,7 @@ describe("sendMessageTelegram", () => {
sendVideo: typeof sendVideo; sendVideo: typeof sendVideo;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-video"), buffer: Buffer.from("fake-video"),
contentType: "video/mp4", contentType: "video/mp4",
fileName: "video.mp4", fileName: "video.mp4",
@@ -590,7 +618,7 @@ describe("sendMessageTelegram", () => {
sendMessage: typeof sendMessage; sendMessage: typeof sendMessage;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-video"), buffer: Buffer.from("fake-video"),
contentType: "video/mp4", contentType: "video/mp4",
fileName: "video.mp4", fileName: "video.mp4",
@@ -680,7 +708,7 @@ describe("sendMessageTelegram", () => {
sendAnimation: typeof sendAnimation; sendAnimation: typeof sendAnimation;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("GIF89a"), buffer: Buffer.from("GIF89a"),
fileName: "fun.gif", fileName: "fun.gif",
}); });
@@ -779,7 +807,7 @@ describe("sendMessageTelegram", () => {
sendVoice: typeof sendVoice; sendVoice: typeof sendVoice;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("audio"), buffer: Buffer.from("audio"),
contentType: testCase.contentType, contentType: testCase.contentType,
fileName: testCase.fileName, fileName: testCase.fileName,
@@ -1006,7 +1034,7 @@ describe("sendMessageTelegram", () => {
sendPhoto: typeof sendPhoto; sendPhoto: typeof sendPhoto;
}; };
loadWebMedia.mockResolvedValueOnce({ mockLoadedMedia({
buffer: Buffer.from("fake-image"), buffer: Buffer.from("fake-image"),
contentType: "image/jpeg", contentType: "image/jpeg",
fileName: "photo.jpg", fileName: "photo.jpg",
@@ -1075,26 +1103,18 @@ describe("reactMessageTelegram", () => {
}); });
describe("sendStickerTelegram", () => { describe("sendStickerTelegram", () => {
beforeEach(() => {
loadConfig.mockReturnValue({});
botApi.sendSticker.mockReset();
botCtorSpy.mockReset();
});
const positiveSendCases = [ const positiveSendCases = [
{ {
name: "sends a sticker by file_id", name: "sends a sticker by file_id",
fileId: "CAACAgIAAxkBAAI...sticker_file_id", fileId: "CAACAgIAAxkBAAI...sticker_file_id",
expectedFileId: "CAACAgIAAxkBAAI...sticker_file_id", expectedFileId: "CAACAgIAAxkBAAI...sticker_file_id",
expectedMessageId: 100, expectedMessageId: 100,
assertResult: true,
}, },
{ {
name: "trims whitespace from fileId", name: "trims whitespace from fileId",
fileId: " fileId123 ", fileId: " fileId123 ",
expectedFileId: "fileId123", expectedFileId: "fileId123",
expectedMessageId: 106, expectedMessageId: 106,
assertResult: false,
}, },
] as const; ] as const;
@@ -1115,10 +1135,8 @@ describe("sendStickerTelegram", () => {
}); });
expect(sendSticker).toHaveBeenCalledWith(chatId, testCase.expectedFileId, undefined); expect(sendSticker).toHaveBeenCalledWith(chatId, testCase.expectedFileId, undefined);
if (testCase.assertResult) { expect(res.messageId).toBe(String(testCase.expectedMessageId));
expect(res.messageId).toBe(String(testCase.expectedMessageId)); expect(res.chatId).toBe(chatId);
expect(res.chatId).toBe(chatId);
}
}); });
} }
@@ -1253,11 +1271,6 @@ describe("shared send behaviors", () => {
}); });
describe("editMessageTelegram", () => { describe("editMessageTelegram", () => {
beforeEach(() => {
botApi.editMessageText.mockReset();
botCtorSpy.mockReset();
});
it("handles button payload + parse fallback behavior", async () => { it("handles button payload + parse fallback behavior", async () => {
const cases: Array<{ const cases: Array<{
name: string; name: string;