* feat(telegram): add CLI project with Bot API client * feat(telegram): add telegram-send plugin-tool * test(telegram): add unit tests for markdown, config, api * chore(env): add TELEGRAM_CHAT_ID to .env.example * docs(handoff): add handoff and ADR for telegram-send * docs(handoff): set PR number 174 * docs: update project map for telegram-send tool --------- Co-authored-by: opencode-agent <agent@opencode.local>
130 lines
No EOL
5.5 KiB
TypeScript
130 lines
No EOL
5.5 KiB
TypeScript
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<string, string>)["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()
|
|
})
|
|
}) |