import { describe, it, expect, beforeEach, afterEach, vi, beforeAll, afterAll } from "vitest" import { writeFileSync, unlinkSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import { sendMessage, sendDocument, sendPhoto } from "../src/api.ts" const TOKEN = "TESTOKEN" const CHAT = "123" const TMP_DOC = join(tmpdir(), "tg-test-doc.txt") const TMP_PHOTO = join(tmpdir(), "tg-test-photo.png") function okResponse(result: unknown) { const payload = { ok: true as const, result } return { ...payload, json: async () => payload } } beforeAll(() => { writeFileSync(TMP_DOC, "hello document") writeFileSync(TMP_PHOTO, "fake-png-bytes") }) afterAll(() => { unlinkSync(TMP_DOC) unlinkSync(TMP_PHOTO) }) beforeEach(() => { vi.stubGlobal("fetch", vi.fn()) }) afterEach(() => { vi.unstubAllGlobals() }) describe("sendMessage", () => { it("POSTs to sendMessage with chat_id, text; parse_mode omitted when undefined", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 42, chat: { id: 999 } }) as never) await sendMessage(TOKEN, CHAT, "hi") const [url, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] expect(url).toBe(`https://api.telegram.org/bot${TOKEN}/sendMessage`) expect(opts.method).toBe("POST") expect((opts.headers as Record)["Content-Type"]).toBe("application/json") const body = JSON.parse(opts.body as string) expect(body).toEqual({ chat_id: CHAT, text: "hi" }) }) it("POSTs to sendMessage with parse_mode=MarkdownV2 when explicitly passed", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 1, chat: { id: 1 } }) as never) await sendMessage(TOKEN, CHAT, "hi", "MarkdownV2") const [, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] const body = JSON.parse(opts.body as string) expect(body).toEqual({ chat_id: CHAT, text: "hi", parse_mode: "MarkdownV2" }) }) it("parseMode='plain' omits parse_mode from body", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 1, chat: { id: 1 } }) as never) await sendMessage(TOKEN, CHAT, "hi", "plain") const [, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] const body = JSON.parse(opts.body as string) expect(body).toEqual({ chat_id: CHAT, text: "hi" }) expect(body).not.toHaveProperty("parse_mode") }) it("parseMode='HTML' sets parse_mode=HTML", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 1, chat: { id: 1 } }) as never) await sendMessage(TOKEN, CHAT, "hi", "HTML") const [, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] const body = JSON.parse(opts.body as string) expect(body.parse_mode).toBe("HTML") }) it("returns {ok, message_id, chat_id} on success", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 42, chat: { id: 999 } }) as never) const r = await sendMessage(TOKEN, CHAT, "hi") expect(r).toEqual({ ok: true, message_id: 42, chat_id: "999" }) }) it("throws on Telegram API error (ok:false)", async () => { const errPayload = { ok: false, description: "Unauthorized" } vi.mocked(fetch).mockResolvedValue({ ...errPayload, json: async () => errPayload } as never) await expect(sendMessage(TOKEN, CHAT, "hi")).rejects.toThrow(/Unauthorized/) }) it("throws on network error (fetch rejects)", async () => { vi.mocked(fetch).mockRejectedValue(new Error("network")) await expect(sendMessage(TOKEN, CHAT, "hi")).rejects.toThrow(/network/) }) }) describe("sendDocument", () => { it("POSTs to sendDocument with FormData (chat_id, document, caption)", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 7, chat: { id: CHAT } }) as never) const appendSpy = vi.spyOn(FormData.prototype, "append") await sendDocument(TOKEN, CHAT, TMP_DOC, "cap", "MarkdownV2") const [url, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] expect(url).toBe(`https://api.telegram.org/bot${TOKEN}/sendDocument`) expect(opts.method).toBe("POST") const form = opts.body as FormData expect(form.get("chat_id")).toBe(CHAT) expect(form.get("caption")).toBe("cap") expect(form.get("parse_mode")).toBe("MarkdownV2") const doc = form.get("document") expect(doc).toBeInstanceOf(Blob) expect((doc as File).name).toBe("tg-test-doc.txt") expect(appendSpy).toHaveBeenCalledWith("chat_id", CHAT) expect(appendSpy).toHaveBeenCalledWith("document", expect.any(Blob), "tg-test-doc.txt") appendSpy.mockRestore() }) }) describe("sendPhoto", () => { it("POSTs to sendPhoto with FormData (chat_id, photo field)", async () => { vi.mocked(fetch).mockResolvedValue(okResponse({ message_id: 9, chat: { id: CHAT } }) as never) const appendSpy = vi.spyOn(FormData.prototype, "append") await sendPhoto(TOKEN, CHAT, TMP_PHOTO, "cover", "HTML") const [url, opts] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] expect(url).toBe(`https://api.telegram.org/bot${TOKEN}/sendPhoto`) expect(opts.method).toBe("POST") const form = opts.body as FormData expect(form.get("chat_id")).toBe(CHAT) expect(form.get("caption")).toBe("cover") expect(form.get("parse_mode")).toBe("HTML") const photo = form.get("photo") expect(photo).toBeInstanceOf(Blob) expect((photo as File).name).toBe("tg-test-photo.png") expect(appendSpy).toHaveBeenCalledWith("photo", expect.any(Blob), "tg-test-photo.png") appendSpy.mockRestore() }) })