@xbibzlibrary/telebibz 0.4.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -138
- package/LICENSE +1 -1
- package/NOTICE.md +9 -4
- package/README.md +173 -256
- package/examples/01-quickstart.js +13 -0
- package/examples/02-menu-tombol.js +22 -0
- package/examples/03-wizard.js +29 -0
- package/examples/04-broadcast.js +25 -0
- package/examples/05-kirim-file.js +19 -0
- package/examples/06-menu.js +35 -0
- package/examples/07-inline-query.js +18 -0
- package/index.d.ts +81 -0
- package/index.js +42 -0
- package/lib/api.js +154 -0
- package/lib/broadcast.js +35 -0
- package/lib/composer.js +174 -0
- package/lib/context.js +190 -0
- package/lib/errors.js +35 -0
- package/lib/file.js +41 -0
- package/lib/inline-query.js +28 -0
- package/lib/keyboard.js +83 -0
- package/lib/logger.js +38 -0
- package/lib/menus.js +90 -0
- package/lib/net.js +115 -0
- package/lib/ratelimit.js +61 -0
- package/lib/runner.js +45 -0
- package/lib/session.js +36 -0
- package/lib/telebibz.js +190 -0
- package/lib/wizard.js +81 -0
- package/package.json +35 -97
- package/test/all.test.js +334 -0
- package/CODE_OF_CONDUCT.md +0 -37
- package/CONTRIBUTING.md +0 -59
- package/CONTRIBUTION_RULES.md +0 -41
- package/GOVERNANCE.md +0 -47
- package/README.id.md +0 -306
- package/README.zh-CN.md +0 -306
- package/RELEASE_AUTOMATION.md +0 -78
- package/RELEASE_POLICY.md +0 -32
- package/SECURITY.md +0 -47
- package/SHOWCASE.md +0 -29
- package/SUPPORT.md +0 -30
- package/assets/readme-preview.html +0 -75
- package/assets/telebibz-logo.png +0 -0
- package/assets/telebibz-readme-preview.png +0 -0
- package/bin/telebibz.mjs +0 -3
- package/dist/generated/api.d.ts +0 -13
- package/dist/generated/api.d.ts.map +0 -1
- package/dist/generated/api.js +0 -192
- package/dist/generated/api.js.map +0 -1
- package/dist/src/api/client.d.ts +0 -62
- package/dist/src/api/client.d.ts.map +0 -1
- package/dist/src/api/client.js +0 -104
- package/dist/src/api/client.js.map +0 -1
- package/dist/src/api/errors.d.ts +0 -45
- package/dist/src/api/errors.d.ts.map +0 -1
- package/dist/src/api/errors.js +0 -65
- package/dist/src/api/errors.js.map +0 -1
- package/dist/src/api/index.d.ts +0 -6
- package/dist/src/api/index.d.ts.map +0 -1
- package/dist/src/api/index.js +0 -6
- package/dist/src/api/index.js.map +0 -1
- package/dist/src/api/telegram-types/LICENSE +0 -21
- package/dist/src/api/telegram-types/api.d.ts +0 -22
- package/dist/src/api/telegram-types/checklist.d.ts +0 -72
- package/dist/src/api/telegram-types/inline.d.ts +0 -692
- package/dist/src/api/telegram-types/langs.d.ts +0 -193
- package/dist/src/api/telegram-types/manage.d.ts +0 -1144
- package/dist/src/api/telegram-types/markup.d.ts +0 -268
- package/dist/src/api/telegram-types/message.d.ts +0 -1537
- package/dist/src/api/telegram-types/methods.d.ts +0 -2870
- package/dist/src/api/telegram-types/mod.d.ts +0 -14
- package/dist/src/api/telegram-types/passport.d.ts +0 -163
- package/dist/src/api/telegram-types/payment.d.ts +0 -570
- package/dist/src/api/telegram-types/rich.d.ts +0 -1010
- package/dist/src/api/telegram-types/settings.d.ts +0 -120
- package/dist/src/api/telegram-types/story.d.ts +0 -89
- package/dist/src/api/telegram-types/update.d.ts +0 -84
- package/dist/src/api/telegram.d.ts +0 -7
- package/dist/src/api/telegram.d.ts.map +0 -1
- package/dist/src/api/telegram.js +0 -2
- package/dist/src/api/telegram.js.map +0 -1
- package/dist/src/api/transport.d.ts +0 -68
- package/dist/src/api/transport.d.ts.map +0 -1
- package/dist/src/api/transport.js +0 -264
- package/dist/src/api/transport.js.map +0 -1
- package/dist/src/api/types.d.ts +0 -466
- package/dist/src/api/types.d.ts.map +0 -1
- package/dist/src/api/types.js +0 -2
- package/dist/src/api/types.js.map +0 -1
- package/dist/src/branding/terminal.d.ts +0 -77
- package/dist/src/branding/terminal.d.ts.map +0 -1
- package/dist/src/branding/terminal.js +0 -328
- package/dist/src/branding/terminal.js.map +0 -1
- package/dist/src/broadcast/broadcast.d.ts +0 -50
- package/dist/src/broadcast/broadcast.d.ts.map +0 -1
- package/dist/src/broadcast/broadcast.js +0 -56
- package/dist/src/broadcast/broadcast.js.map +0 -1
- package/dist/src/cache/cache.d.ts +0 -34
- package/dist/src/cache/cache.d.ts.map +0 -1
- package/dist/src/cache/cache.js +0 -41
- package/dist/src/cache/cache.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -84
- package/dist/src/cli.js.map +0 -1
- package/dist/src/context/context.d.ts +0 -124
- package/dist/src/context/context.d.ts.map +0 -1
- package/dist/src/context/context.js +0 -302
- package/dist/src/context/context.js.map +0 -1
- package/dist/src/core/bot.d.ts +0 -204
- package/dist/src/core/bot.d.ts.map +0 -1
- package/dist/src/core/bot.js +0 -506
- package/dist/src/core/bot.js.map +0 -1
- package/dist/src/core/events.d.ts +0 -75
- package/dist/src/core/events.d.ts.map +0 -1
- package/dist/src/core/events.js +0 -35
- package/dist/src/core/events.js.map +0 -1
- package/dist/src/core/webhook-reply.d.ts +0 -34
- package/dist/src/core/webhook-reply.d.ts.map +0 -1
- package/dist/src/core/webhook-reply.js +0 -37
- package/dist/src/core/webhook-reply.js.map +0 -1
- package/dist/src/index.d.ts +0 -24
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -24
- package/dist/src/index.js.map +0 -1
- package/dist/src/keyboard/index.d.ts +0 -46
- package/dist/src/keyboard/index.d.ts.map +0 -1
- package/dist/src/keyboard/index.js +0 -55
- package/dist/src/keyboard/index.js.map +0 -1
- package/dist/src/middleware/compose.d.ts +0 -5
- package/dist/src/middleware/compose.d.ts.map +0 -1
- package/dist/src/middleware/compose.js +0 -17
- package/dist/src/middleware/compose.js.map +0 -1
- package/dist/src/observability/logger.d.ts +0 -78
- package/dist/src/observability/logger.d.ts.map +0 -1
- package/dist/src/observability/logger.js +0 -285
- package/dist/src/observability/logger.js.map +0 -1
- package/dist/src/plugins/plugin.d.ts +0 -38
- package/dist/src/plugins/plugin.d.ts.map +0 -1
- package/dist/src/plugins/plugin.js +0 -59
- package/dist/src/plugins/plugin.js.map +0 -1
- package/dist/src/queue/queue.d.ts +0 -77
- package/dist/src/queue/queue.d.ts.map +0 -1
- package/dist/src/queue/queue.js +0 -213
- package/dist/src/queue/queue.js.map +0 -1
- package/dist/src/router/router.d.ts +0 -61
- package/dist/src/router/router.d.ts.map +0 -1
- package/dist/src/router/router.js +0 -183
- package/dist/src/router/router.js.map +0 -1
- package/dist/src/state/conversation.d.ts +0 -56
- package/dist/src/state/conversation.d.ts.map +0 -1
- package/dist/src/state/conversation.js +0 -133
- package/dist/src/state/conversation.js.map +0 -1
- package/dist/src/state/forms.d.ts +0 -34
- package/dist/src/state/forms.d.ts.map +0 -1
- package/dist/src/state/forms.js +0 -44
- package/dist/src/state/forms.js.map +0 -1
- package/dist/src/state/menu.d.ts +0 -78
- package/dist/src/state/menu.d.ts.map +0 -1
- package/dist/src/state/menu.js +0 -127
- package/dist/src/state/menu.js.map +0 -1
- package/dist/src/storage/storage.d.ts +0 -146
- package/dist/src/storage/storage.d.ts.map +0 -1
- package/dist/src/storage/storage.js +0 -195
- package/dist/src/storage/storage.js.map +0 -1
- package/dist/src/telegram-features.d.ts +0 -33
- package/dist/src/telegram-features.d.ts.map +0 -1
- package/dist/src/telegram-features.js +0 -71
- package/dist/src/telegram-features.js.map +0 -1
- package/dist/src/testing.d.ts +0 -24
- package/dist/src/testing.d.ts.map +0 -1
- package/dist/src/testing.js +0 -38
- package/dist/src/testing.js.map +0 -1
- package/dist/src/utils/concurrency.d.ts +0 -25
- package/dist/src/utils/concurrency.d.ts.map +0 -1
- package/dist/src/utils/concurrency.js +0 -52
- package/dist/src/utils/concurrency.js.map +0 -1
- package/dist/src/utils/files.d.ts +0 -45
- package/dist/src/utils/files.d.ts.map +0 -1
- package/dist/src/utils/files.js +0 -53
- package/dist/src/utils/files.js.map +0 -1
- package/dist/src/utils/text.d.ts +0 -39
- package/dist/src/utils/text.d.ts.map +0 -1
- package/dist/src/utils/text.js +0 -56
- package/dist/src/utils/text.js.map +0 -1
- package/dist/src/webhook/handler.d.ts +0 -19
- package/dist/src/webhook/handler.d.ts.map +0 -1
- package/dist/src/webhook/handler.js +0 -141
- package/dist/src/webhook/handler.js.map +0 -1
- package/dist-cjs/generated/api.js +0 -194
- package/dist-cjs/package.json +0 -3
- package/dist-cjs/src/api/client.js +0 -107
- package/dist-cjs/src/api/errors.js +0 -74
- package/dist-cjs/src/api/index.js +0 -21
- package/dist-cjs/src/api/telegram-types/LICENSE +0 -21
- package/dist-cjs/src/api/telegram-types/api.d.ts +0 -22
- package/dist-cjs/src/api/telegram-types/checklist.d.ts +0 -72
- package/dist-cjs/src/api/telegram-types/inline.d.ts +0 -692
- package/dist-cjs/src/api/telegram-types/langs.d.ts +0 -193
- package/dist-cjs/src/api/telegram-types/manage.d.ts +0 -1144
- package/dist-cjs/src/api/telegram-types/markup.d.ts +0 -268
- package/dist-cjs/src/api/telegram-types/message.d.ts +0 -1537
- package/dist-cjs/src/api/telegram-types/methods.d.ts +0 -2870
- package/dist-cjs/src/api/telegram-types/mod.d.ts +0 -14
- package/dist-cjs/src/api/telegram-types/passport.d.ts +0 -163
- package/dist-cjs/src/api/telegram-types/payment.d.ts +0 -570
- package/dist-cjs/src/api/telegram-types/rich.d.ts +0 -1010
- package/dist-cjs/src/api/telegram-types/settings.d.ts +0 -120
- package/dist-cjs/src/api/telegram-types/story.d.ts +0 -89
- package/dist-cjs/src/api/telegram-types/update.d.ts +0 -84
- package/dist-cjs/src/api/telegram.js +0 -2
- package/dist-cjs/src/api/transport.js +0 -267
- package/dist-cjs/src/api/types.js +0 -2
- package/dist-cjs/src/branding/terminal.js +0 -338
- package/dist-cjs/src/broadcast/broadcast.js +0 -58
- package/dist-cjs/src/cache/cache.js +0 -45
- package/dist-cjs/src/cli.js +0 -86
- package/dist-cjs/src/context/context.js +0 -305
- package/dist-cjs/src/core/bot.js +0 -510
- package/dist-cjs/src/core/events.js +0 -38
- package/dist-cjs/src/core/webhook-reply.js +0 -42
- package/dist-cjs/src/index.js +0 -47
- package/dist-cjs/src/keyboard/index.js +0 -61
- package/dist-cjs/src/middleware/compose.js +0 -20
- package/dist-cjs/src/observability/logger.js +0 -293
- package/dist-cjs/src/plugins/plugin.js +0 -63
- package/dist-cjs/src/queue/queue.js +0 -219
- package/dist-cjs/src/router/router.js +0 -186
- package/dist-cjs/src/state/conversation.js +0 -139
- package/dist-cjs/src/state/forms.js +0 -47
- package/dist-cjs/src/state/menu.js +0 -133
- package/dist-cjs/src/storage/storage.js +0 -202
- package/dist-cjs/src/telegram-features.js +0 -76
- package/dist-cjs/src/testing.js +0 -45
- package/dist-cjs/src/utils/concurrency.js +0 -57
- package/dist-cjs/src/utils/files.js +0 -58
- package/dist-cjs/src/utils/text.js +0 -63
- package/dist-cjs/src/webhook/handler.js +0 -144
- package/docs/API.id.md +0 -1935
- package/docs/API.md +0 -1969
- package/docs/API.zh-CN.md +0 -1929
- package/docs/COOKBOOK.id.md +0 -321
- package/docs/COOKBOOK.md +0 -321
- package/docs/COOKBOOK.zh-CN.md +0 -321
- package/docs/ERRORS.id.md +0 -194
- package/docs/ERRORS.md +0 -194
- package/docs/ERRORS.zh-CN.md +0 -194
- package/docs/FILES.id.md +0 -243
- package/docs/FILES.md +0 -243
- package/docs/FILES.zh-CN.md +0 -243
- package/docs/GETTING_STARTED.id.md +0 -89
- package/docs/GETTING_STARTED.md +0 -89
- package/docs/GETTING_STARTED.zh-CN.md +0 -89
- package/docs/GITHUB_PACKAGES.id.md +0 -82
- package/docs/GITHUB_PACKAGES.md +0 -82
- package/docs/GITHUB_PACKAGES.zh-CN.md +0 -82
- package/docs/MIGRATION_TELEGRAF.id.md +0 -147
- package/docs/MIGRATION_TELEGRAF.md +0 -154
- package/docs/MIGRATION_TELEGRAF.zh-CN.md +0 -147
- package/docs/README.md +0 -67
- package/docs/STORAGE.id.md +0 -105
- package/docs/STORAGE.md +0 -105
- package/docs/STORAGE.zh-CN.md +0 -105
- package/docs/TESTING.id.md +0 -203
- package/docs/TESTING.md +0 -203
- package/docs/TESTING.zh-CN.md +0 -203
- package/docs/WEBHOOK.id.md +0 -212
- package/docs/WEBHOOK.md +0 -215
- package/docs/WEBHOOK.zh-CN.md +0 -212
- package/examples/README.md +0 -37
- package/examples/files.ts +0 -35
- package/examples/minimal.ts +0 -12
- package/examples/tsconfig.json +0 -9
- package/examples/webhook.ts +0 -42
- package/examples/wizard-registration.ts +0 -42
package/docs/TESTING.id.md
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# Panduan testing (Bahasa Indonesia)
|
|
2
|
-
|
|
3
|
-
Uji bot telebibz sepenuhnya offline: palsukan transport, rakit update, asersikan panggilan keluar, dan jalankan conversation utuh end-to-end.
|
|
4
|
-
|
|
5
|
-
## Daftar isi
|
|
6
|
-
|
|
7
|
-
1. [Subpath testing](#1-subpath-testing)
|
|
8
|
-
2. [MockTransport](#2-mocktransport)
|
|
9
|
-
3. [Men-drive update ke dalam bot](#3-men-drive-update-ke-dalam-bot)
|
|
10
|
-
4. [Asersi panggilan keluar](#4-asersi-panggilan-keluar)
|
|
11
|
-
5. [Menguji wizard end-to-end](#5-menguji-wizard-end-to-end)
|
|
12
|
-
6. [Menguji unduhan file](#6-menguji-unduhan-file)
|
|
13
|
-
7. [Menguji webhook](#7-menguji-webhook)
|
|
14
|
-
8. [Menguji jalur error](#8-menguji-jalur-error)
|
|
15
|
-
9. [Pola Vitest / Jest](#9-pola-vitest--jest)
|
|
16
|
-
|
|
17
|
-
## 1. Subpath testing
|
|
18
|
-
|
|
19
|
-
Semuanya tersedia dari `@xbibzlibrary/telebibz/testing`:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { MockTransport, createTestBot, createMockUpdate, createMockCallbackUpdate, createMockContext } from "@xbibzlibrary/telebibz/testing";
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
- `createTestBot()` → `{ bot, transport }` — sebuah `Bot` yang terhubung ke `MockTransport`, branding dimatikan.
|
|
26
|
-
- `createMockUpdate(overrides?)` → update pesan teks `/start` sederhana.
|
|
27
|
-
- `createMockCallbackUpdate(overrides?)` → update callback-query.
|
|
28
|
-
- `createMockContext(bot, update?)` → sebuah `Context` tanpa me-routing apa pun.
|
|
29
|
-
|
|
30
|
-
## 2. MockTransport
|
|
31
|
-
|
|
32
|
-
`MockTransport` merekam setiap request keluar dan menjawab dari response yang dikonfigurasi:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const { bot, transport } = createTestBot();
|
|
36
|
-
|
|
37
|
-
transport.respond("getMe", { ok: true, result: { id: 99, is_bot: true, first_name: "TestBot", username: "test_bot" } });
|
|
38
|
-
transport.respond("sendMessage", { ok: true, result: { message_id: 1, date: 0, chat: { id: 1, type: "private" } } });
|
|
39
|
-
transport.respond("getFile", (payload) => ({ // jawaban dinamis
|
|
40
|
-
ok: true,
|
|
41
|
-
result: { file_id: (payload as { file_id: string }).file_id, file_unique_id: "u", file_path: "documents/a.pdf" },
|
|
42
|
-
}));
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Method yang tidak dikonfigurasi menjawab `{ ok: true, result: true }` — cukup untuk panggilan yang hasilnya Anda abaikan.
|
|
46
|
-
|
|
47
|
-
Ia juga mengimplementasikan member download:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
transport.downloadBytes = new TextEncoder().encode("file-content");
|
|
51
|
-
transport.downloads; // setiap file_path yang diberikan ke download(), berurutan
|
|
52
|
-
transport.fileUrl("a.pdf"); // "mock://files/a.pdf"
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## 3. Men-drive update ke dalam bot
|
|
56
|
-
|
|
57
|
-
Daftarkan handler, lalu kirim update — tanpa jaringan, tanpa token:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
bot.on("message", async (ctx) => { await ctx.reply("hi"); });
|
|
61
|
-
await bot.init(); // tepat satu getMe, seperti produksi
|
|
62
|
-
|
|
63
|
-
await bot.handleUpdate(createMockUpdate({ message: { ...createMockUpdate().message!, text: "hello" } }));
|
|
64
|
-
await bot.handleUpdates([updateA, updateB, updateC]); // satu batch penuh, paralel antar chat
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Untuk mensimulasikan beberapa chat, variasikan chat id:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
function textUpdate(updateId: number, chatId: number, text: string) {
|
|
71
|
-
const base = createMockUpdate();
|
|
72
|
-
return { ...base, update_id: updateId, message: { ...base.message!, chat: { id: chatId, type: "private" as const }, text } };
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 4. Asersi panggilan keluar
|
|
77
|
-
|
|
78
|
-
Setiap request tercatat di `transport.calls` lengkap dengan method dan payload-nya:
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
const replies = transport.calls
|
|
82
|
-
.filter((call) => call.method === "sendMessage")
|
|
83
|
-
.map((call) => (call.payload as { text?: string }).text);
|
|
84
|
-
|
|
85
|
-
expect(replies).toEqual(["Siapa nama Anda?", "Berapa usia Anda?"]);
|
|
86
|
-
|
|
87
|
-
// Keyboard callback:
|
|
88
|
-
const markup = (transport.calls.at(-1)?.payload as { reply_markup?: { inline_keyboard: unknown[][] } }).reply_markup;
|
|
89
|
-
expect(markup?.inline_keyboard).toHaveLength(2);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 5. Menguji wizard end-to-end
|
|
93
|
-
|
|
94
|
-
Berikan conversation satu pesan demi satu pesan dan asersikan balasan yang akan dilihat user:
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { Bot, Wizard } from "@xbibzlibrary/telebibz";
|
|
98
|
-
|
|
99
|
-
const wizard = new Wizard()
|
|
100
|
-
.step({ id: "ask", run: async (flow) => { flow.next(); await flow.ctx.reply("Siapa nama Anda?"); } })
|
|
101
|
-
.step({ id: "save", run: async (flow) => { await flow.ctx.reply(`Halo, ${flow.ctx.message?.text}!`); } });
|
|
102
|
-
bot.useWizard(wizard);
|
|
103
|
-
bot.command("start", async (ctx) => { await wizard.run(ctx); });
|
|
104
|
-
|
|
105
|
-
await bot.handleUpdate(textUpdate(1, 7, "/start"));
|
|
106
|
-
await bot.handleUpdate(textUpdate(2, 7, "Dewi"));
|
|
107
|
-
|
|
108
|
-
expect(sentTexts()).toEqual(["Siapa nama Anda?", "Halo, Dewi!"]);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Dua chat, dua wizard independen — update yang berselang-seling tetap terisolasi karena update dari chat yang sama diserialisasi:
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
await bot.handleUpdate(textUpdate(1, 7, "/start"));
|
|
115
|
-
await bot.handleUpdate(textUpdate(2, 8, "/start"));
|
|
116
|
-
await bot.handleUpdate(textUpdate(3, 7, "Dewi"));
|
|
117
|
-
await bot.handleUpdate(textUpdate(4, 8, "Budi"));
|
|
118
|
-
expect(sentTexts()).toEqual(["Siapa nama Anda?", "Siapa nama Anda?", "Halo, Dewi!", "Halo, Budi!"]);
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 6. Menguji unduhan file
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
transport.respond("getFile", { ok: true, result: { file_id: "F1", file_unique_id: "U1", file_path: "photos/pic.jpg" } });
|
|
125
|
-
transport.downloadBytes = new TextEncoder().encode("jpeg-bytes");
|
|
126
|
-
|
|
127
|
-
const file = await bot.downloadFile("F1", { destination: tmpFile });
|
|
128
|
-
expect(file.fileName).toBe("pic.jpg");
|
|
129
|
-
expect(transport.downloads).toEqual(["photos/pic.jpg"]);
|
|
130
|
-
expect(await readFile(tmpFile, "utf8")).toBe("jpeg-bytes");
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## 7. Menguji webhook
|
|
134
|
-
|
|
135
|
-
`createWebhookHandler` menerima `Request` Web sungguhan:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const handler = createWebhookHandler(bot, { secretToken: "s3cret" });
|
|
139
|
-
|
|
140
|
-
const response = await handler(new Request("https://example.com/telegram", {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "s3cret" },
|
|
143
|
-
body: JSON.stringify(createMockUpdate()),
|
|
144
|
-
}));
|
|
145
|
-
expect(response.status).toBe(200);
|
|
146
|
-
|
|
147
|
-
// Secret salah → ditolak sebelum handler mana pun berjalan
|
|
148
|
-
const rejected = await handler(new Request("https://example.com/telegram", {
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "salah" },
|
|
151
|
-
body: JSON.stringify(createMockUpdate()),
|
|
152
|
-
}));
|
|
153
|
-
expect(rejected.status).toBe(401);
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## 8. Menguji jalur error
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
// Telegram menolak panggilan
|
|
160
|
-
transport.respond("sendMessage", { ok: false, error_code: 400, description: "Bad Request: chat not found" });
|
|
161
|
-
await expect(ctx.reply("hi")).rejects.toThrow(/chat not found/);
|
|
162
|
-
|
|
163
|
-
// Handler melempar — asersikan error boundary melihatnya
|
|
164
|
-
const errors: unknown[] = [];
|
|
165
|
-
bot.events.on("update:error", ({ error }) => errors.push(error));
|
|
166
|
-
bot.onText("boom", async () => { throw new Error("boom"); });
|
|
167
|
-
await bot.handleUpdate(textUpdate(1, 7, "boom"));
|
|
168
|
-
expect(errors).toHaveLength(1);
|
|
169
|
-
|
|
170
|
-
// handlerTimeout: update yang menggantung melempar UpdateTimeoutError sementara handler tetap berjalan
|
|
171
|
-
const slow = new Bot({ token: "123456:TEST", transport, handlerTimeout: 50, branding: false, logger: { level: "silent" } });
|
|
172
|
-
let finished = false;
|
|
173
|
-
slow.on("message", async () => { await sleep(200); finished = true; });
|
|
174
|
-
await expect(slow.handleUpdate(createMockUpdate())).rejects.toBeInstanceOf(UpdateTimeoutError);
|
|
175
|
-
// nanti:
|
|
176
|
-
expect(finished).toBe(true);
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## 9. Pola Vitest / Jest
|
|
180
|
-
|
|
181
|
-
**Senyapkan logger di test** — `logger: { level: "silent" }` tidak memancarkan apa pun (diuji regresi):
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
const bot = new Bot({ token: "123456:TEST", transport, branding: false, logger: { level: "silent" } });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
**Transport segar per test** — jangan pernah berbagi state antar kasus:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
beforeEach(() => { ({ bot, transport } = createTestBot()); });
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Mempercepat waktu** — untuk test `Scheduler`/TTL pakai fake timer atau interval kecil (`1ms`); parser cron murni dan bisa diuji tanpa timer:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { nextCronOccurrence, parseCronExpression } from "@xbibzlibrary/telebibz";
|
|
197
|
-
expect(parseCronExpression("*/15 * * * *")).toBeDefined();
|
|
198
|
-
expect(nextCronOccurrence("0 9 * * 1", new Date("2026-08-29T00:00:00Z")).toISOString()).toContain("T09:00");
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Jalankan suite**: `npm test` — test E2E yang membutuhkan token asli otomatis di-skip ketika `TELEGRAM_BOT_TOKEN` tidak ada.
|
|
202
|
-
|
|
203
|
-
English: [TESTING.md](TESTING.md) · 简体中文: [TESTING.zh-CN.md](TESTING.zh-CN.md)
|
package/docs/TESTING.md
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# Testing guide (English)
|
|
2
|
-
|
|
3
|
-
Test telebibz bots fully offline: fake the transport, craft updates, assert outgoing calls, and drive whole conversations end to end.
|
|
4
|
-
|
|
5
|
-
## Contents
|
|
6
|
-
|
|
7
|
-
1. [The testing subpath](#1-the-testing-subpath)
|
|
8
|
-
2. [MockTransport](#2-mocktransport)
|
|
9
|
-
3. [Driving updates through the bot](#3-driving-updates-through-the-bot)
|
|
10
|
-
4. [Asserting outgoing calls](#4-asserting-outgoing-calls)
|
|
11
|
-
5. [Testing wizards end to end](#5-testing-wizards-end-to-end)
|
|
12
|
-
6. [Testing file downloads](#6-testing-file-downloads)
|
|
13
|
-
7. [Testing webhooks](#7-testing-webhooks)
|
|
14
|
-
8. [Testing error paths](#8-testing-error-paths)
|
|
15
|
-
9. [Vitest / Jest patterns](#9-vitest--jest-patterns)
|
|
16
|
-
|
|
17
|
-
## 1. The testing subpath
|
|
18
|
-
|
|
19
|
-
Everything ships from `@xbibzlibrary/telebibz/testing`:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { MockTransport, createTestBot, createMockUpdate, createMockCallbackUpdate, createMockContext } from "@xbibzlibrary/telebibz/testing";
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
- `createTestBot()` → `{ bot, transport }` — a `Bot` wired to a `MockTransport`, branding off.
|
|
26
|
-
- `createMockUpdate(overrides?)` → a plain text `/start` message update.
|
|
27
|
-
- `createMockCallbackUpdate(overrides?)` → a callback-query update.
|
|
28
|
-
- `createMockContext(bot, update?)` → a `Context` without routing anything.
|
|
29
|
-
|
|
30
|
-
## 2. MockTransport
|
|
31
|
-
|
|
32
|
-
`MockTransport` records every outgoing request and answers from configured responses:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const { bot, transport } = createTestBot();
|
|
36
|
-
|
|
37
|
-
transport.respond("getMe", { ok: true, result: { id: 99, is_bot: true, first_name: "TestBot", username: "test_bot" } });
|
|
38
|
-
transport.respond("sendMessage", { ok: true, result: { message_id: 1, date: 0, chat: { id: 1, type: "private" } } });
|
|
39
|
-
transport.respond("getFile", (payload) => ({ // dynamic answers
|
|
40
|
-
ok: true,
|
|
41
|
-
result: { file_id: (payload as { file_id: string }).file_id, file_unique_id: "u", file_path: "documents/a.pdf" },
|
|
42
|
-
}));
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Unconfigured methods answer `{ ok: true, result: true }` — enough for calls whose result you ignore.
|
|
46
|
-
|
|
47
|
-
It also implements the download members:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
transport.downloadBytes = new TextEncoder().encode("file-content");
|
|
51
|
-
transport.downloads; // every file_path passed to download(), in order
|
|
52
|
-
transport.fileUrl("a.pdf"); // "mock://files/a.pdf"
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## 3. Driving updates through the bot
|
|
56
|
-
|
|
57
|
-
Register handlers, then feed updates — no network, no token:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
bot.on("message", async (ctx) => { await ctx.reply("hi"); });
|
|
61
|
-
await bot.init(); // exactly one getMe, like production
|
|
62
|
-
|
|
63
|
-
await bot.handleUpdate(createMockUpdate({ message: { ...createMockUpdate().message!, text: "hello" } }));
|
|
64
|
-
await bot.handleUpdates([updateA, updateB, updateC]); // whole batch, parallel across chats
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
To simulate several chats, vary the chat id:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
function textUpdate(updateId: number, chatId: number, text: string) {
|
|
71
|
-
const base = createMockUpdate();
|
|
72
|
-
return { ...base, update_id: updateId, message: { ...base.message!, chat: { id: chatId, type: "private" as const }, text } };
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 4. Asserting outgoing calls
|
|
77
|
-
|
|
78
|
-
Every request is recorded in `transport.calls` with its method and payload:
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
const replies = transport.calls
|
|
82
|
-
.filter((call) => call.method === "sendMessage")
|
|
83
|
-
.map((call) => (call.payload as { text?: string }).text);
|
|
84
|
-
|
|
85
|
-
expect(replies).toEqual(["What is your name?", "How old are you?"]);
|
|
86
|
-
|
|
87
|
-
// Callback keyboards:
|
|
88
|
-
const markup = (transport.calls.at(-1)?.payload as { reply_markup?: { inline_keyboard: unknown[][] } }).reply_markup;
|
|
89
|
-
expect(markup?.inline_keyboard).toHaveLength(2);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 5. Testing wizards end to end
|
|
93
|
-
|
|
94
|
-
Feed the conversation one message at a time and assert the replies the user would see:
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { Bot, Wizard } from "@xbibzlibrary/telebibz";
|
|
98
|
-
|
|
99
|
-
const wizard = new Wizard()
|
|
100
|
-
.step({ id: "ask", run: async (flow) => { flow.next(); await flow.ctx.reply("Name?"); } })
|
|
101
|
-
.step({ id: "save", run: async (flow) => { await flow.ctx.reply(`Hi ${flow.ctx.message?.text}!`); } });
|
|
102
|
-
bot.useWizard(wizard);
|
|
103
|
-
bot.command("start", async (ctx) => { await wizard.run(ctx); });
|
|
104
|
-
|
|
105
|
-
await bot.handleUpdate(textUpdate(1, 7, "/start"));
|
|
106
|
-
await bot.handleUpdate(textUpdate(2, 7, "Alice"));
|
|
107
|
-
|
|
108
|
-
expect(sentTexts()).toEqual(["Name?", "Hi Alice!"]);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Two chats, two independent wizards — interleaved updates stay isolated because same-chat updates serialize:
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
await bot.handleUpdate(textUpdate(1, 7, "/start"));
|
|
115
|
-
await bot.handleUpdate(textUpdate(2, 8, "/start"));
|
|
116
|
-
await bot.handleUpdate(textUpdate(3, 7, "Alice"));
|
|
117
|
-
await bot.handleUpdate(textUpdate(4, 8, "Bob"));
|
|
118
|
-
expect(sentTexts()).toEqual(["Name?", "Name?", "Hi Alice!", "Hi Bob!"]);
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 6. Testing file downloads
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
transport.respond("getFile", { ok: true, result: { file_id: "F1", file_unique_id: "U1", file_path: "photos/pic.jpg" } });
|
|
125
|
-
transport.downloadBytes = new TextEncoder().encode("jpeg-bytes");
|
|
126
|
-
|
|
127
|
-
const file = await bot.downloadFile("F1", { destination: tmpFile });
|
|
128
|
-
expect(file.fileName).toBe("pic.jpg");
|
|
129
|
-
expect(transport.downloads).toEqual(["photos/pic.jpg"]);
|
|
130
|
-
expect(await readFile(tmpFile, "utf8")).toBe("jpeg-bytes");
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## 7. Testing webhooks
|
|
134
|
-
|
|
135
|
-
`createWebhookHandler` accepts a real Web `Request`:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const handler = createWebhookHandler(bot, { secretToken: "s3cret" });
|
|
139
|
-
|
|
140
|
-
const response = await handler(new Request("https://example.com/telegram", {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "s3cret" },
|
|
143
|
-
body: JSON.stringify(createMockUpdate()),
|
|
144
|
-
}));
|
|
145
|
-
expect(response.status).toBe(200);
|
|
146
|
-
|
|
147
|
-
// Wrong secret → rejected before any handler runs
|
|
148
|
-
const rejected = await handler(new Request("https://example.com/telegram", {
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "wrong" },
|
|
151
|
-
body: JSON.stringify(createMockUpdate()),
|
|
152
|
-
}));
|
|
153
|
-
expect(rejected.status).toBe(401);
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## 8. Testing error paths
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
// Telegram rejects the call
|
|
160
|
-
transport.respond("sendMessage", { ok: false, error_code: 400, description: "Bad Request: chat not found" });
|
|
161
|
-
await expect(ctx.reply("hi")).rejects.toThrow(/chat not found/);
|
|
162
|
-
|
|
163
|
-
// A handler throws — assert the error boundary saw it
|
|
164
|
-
const errors: unknown[] = [];
|
|
165
|
-
bot.events.on("update:error", ({ error }) => errors.push(error));
|
|
166
|
-
bot.onText("boom", async () => { throw new Error("boom"); });
|
|
167
|
-
await bot.handleUpdate(textUpdate(1, 7, "boom"));
|
|
168
|
-
expect(errors).toHaveLength(1);
|
|
169
|
-
|
|
170
|
-
// handlerTimeout: hung updates reject with UpdateTimeoutError while the handler keeps running
|
|
171
|
-
const slow = new Bot({ token: "123456:TEST", transport, handlerTimeout: 50, branding: false, logger: { level: "silent" } });
|
|
172
|
-
let finished = false;
|
|
173
|
-
slow.on("message", async () => { await sleep(200); finished = true; });
|
|
174
|
-
await expect(slow.handleUpdate(createMockUpdate())).rejects.toBeInstanceOf(UpdateTimeoutError);
|
|
175
|
-
// later:
|
|
176
|
-
expect(finished).toBe(true);
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## 9. Vitest / Jest patterns
|
|
180
|
-
|
|
181
|
-
**Silence the logger in tests** — `logger: { level: "silent" }` emits nothing (regression-tested):
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
const bot = new Bot({ token: "123456:TEST", transport, branding: false, logger: { level: "silent" } });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
**Fresh transport per test** — never share state between cases:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
beforeEach(() => { ({ bot, transport } = createTestBot()); });
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Fast-forwarding time** — for `Scheduler`/TTL tests use fake timers or small intervals (`1ms`); the cron parser is pure and testable without timers:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { nextCronOccurrence, parseCronExpression } from "@xbibzlibrary/telebibz";
|
|
197
|
-
expect(parseCronExpression("*/15 * * * *")).toBeDefined();
|
|
198
|
-
expect(nextCronOccurrence("0 9 * * 1", new Date("2026-08-29T00:00:00Z")).toISOString()).toContain("T09:00");
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**Run the suite**: `npm test` — E2E tests requiring a real token are skipped automatically when `TELEGRAM_BOT_TOKEN` is absent.
|
|
202
|
-
|
|
203
|
-
Bahasa Indonesia: [TESTING.id.md](TESTING.id.md) · 简体中文: [TESTING.zh-CN.md](TESTING.zh-CN.md)
|
package/docs/TESTING.zh-CN.md
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# 测试指南(简体中文)
|
|
2
|
-
|
|
3
|
-
完全离线地测试 telebibz bot:伪造传输层、构造更新、断言外发调用,并端到端驱动整段对话。
|
|
4
|
-
|
|
5
|
-
## 目录
|
|
6
|
-
|
|
7
|
-
1. [testing 子路径](#1-testing-子路径)
|
|
8
|
-
2. [MockTransport](#2-mocktransport)
|
|
9
|
-
3. [把更新送进 bot](#3-把更新送进-bot)
|
|
10
|
-
4. [断言外发调用](#4-断言外发调用)
|
|
11
|
-
5. [端到端测试向导](#5-端到端测试向导)
|
|
12
|
-
6. [测试文件下载](#6-测试文件下载)
|
|
13
|
-
7. [测试 webhook](#7-测试-webhook)
|
|
14
|
-
8. [测试错误路径](#8-测试错误路径)
|
|
15
|
-
9. [Vitest / Jest 模式](#9-vitest--jest-模式)
|
|
16
|
-
|
|
17
|
-
## 1. testing 子路径
|
|
18
|
-
|
|
19
|
-
所有工具都从 `@xbibzlibrary/telebibz/testing` 导出:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { MockTransport, createTestBot, createMockUpdate, createMockCallbackUpdate, createMockContext } from "@xbibzlibrary/telebibz/testing";
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
- `createTestBot()` → `{ bot, transport }` —— 连接到 `MockTransport` 的 `Bot`,branding 已关闭。
|
|
26
|
-
- `createMockUpdate(overrides?)` → 一条普通 `/start` 文本消息更新。
|
|
27
|
-
- `createMockCallbackUpdate(overrides?)` → 一条回调查询更新。
|
|
28
|
-
- `createMockContext(bot, update?)` → 不经过路由的 `Context`。
|
|
29
|
-
|
|
30
|
-
## 2. MockTransport
|
|
31
|
-
|
|
32
|
-
`MockTransport` 记录每个外发请求,并按配置应答:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const { bot, transport } = createTestBot();
|
|
36
|
-
|
|
37
|
-
transport.respond("getMe", { ok: true, result: { id: 99, is_bot: true, first_name: "TestBot", username: "test_bot" } });
|
|
38
|
-
transport.respond("sendMessage", { ok: true, result: { message_id: 1, date: 0, chat: { id: 1, type: "private" } } });
|
|
39
|
-
transport.respond("getFile", (payload) => ({ // 动态应答
|
|
40
|
-
ok: true,
|
|
41
|
-
result: { file_id: (payload as { file_id: string }).file_id, file_unique_id: "u", file_path: "documents/a.pdf" },
|
|
42
|
-
}));
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
未配置的方法应答 `{ ok: true, result: true }` —— 对结果无所谓的调用足够了。
|
|
46
|
-
|
|
47
|
-
它还实现了下载成员:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
transport.downloadBytes = new TextEncoder().encode("file-content");
|
|
51
|
-
transport.downloads; // 每个传给 download() 的 file_path,按顺序
|
|
52
|
-
transport.fileUrl("a.pdf"); // "mock://files/a.pdf"
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## 3. 把更新送进 bot
|
|
56
|
-
|
|
57
|
-
注册 handler,然后投喂更新 —— 无网络、无 token:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
bot.on("message", async (ctx) => { await ctx.reply("hi"); });
|
|
61
|
-
await bot.init(); // 恰好一次 getMe,与生产一致
|
|
62
|
-
|
|
63
|
-
await bot.handleUpdate(createMockUpdate({ message: { ...createMockUpdate().message!, text: "hello" } }));
|
|
64
|
-
await bot.handleUpdates([updateA, updateB, updateC]); // 整批投喂,跨 chat 并行
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
模拟多个 chat 时,变换 chat id:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
function textUpdate(updateId: number, chatId: number, text: string) {
|
|
71
|
-
const base = createMockUpdate();
|
|
72
|
-
return { ...base, update_id: updateId, message: { ...base.message!, chat: { id: chatId, type: "private" as const }, text } };
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## 4. 断言外发调用
|
|
77
|
-
|
|
78
|
-
每个请求连同 method 与 payload 记录在 `transport.calls`:
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
const replies = transport.calls
|
|
82
|
-
.filter((call) => call.method === "sendMessage")
|
|
83
|
-
.map((call) => (call.payload as { text?: string }).text);
|
|
84
|
-
|
|
85
|
-
expect(replies).toEqual(["你叫什么名字?", "你多大了?"]);
|
|
86
|
-
|
|
87
|
-
// 回调键盘:
|
|
88
|
-
const markup = (transport.calls.at(-1)?.payload as { reply_markup?: { inline_keyboard: unknown[][] } }).reply_markup;
|
|
89
|
-
expect(markup?.inline_keyboard).toHaveLength(2);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 5. 端到端测试向导
|
|
93
|
-
|
|
94
|
-
逐条消息推进对话,并断言用户将看到的回复:
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { Bot, Wizard } from "@xbibzlibrary/telebibz";
|
|
98
|
-
|
|
99
|
-
const wizard = new Wizard()
|
|
100
|
-
.step({ id: "ask", run: async (flow) => { flow.next(); await flow.ctx.reply("你叫什么名字?"); } })
|
|
101
|
-
.step({ id: "save", run: async (flow) => { await flow.ctx.reply(`你好,${flow.ctx.message?.text}!`); } });
|
|
102
|
-
bot.useWizard(wizard);
|
|
103
|
-
bot.command("start", async (ctx) => { await wizard.run(ctx); });
|
|
104
|
-
|
|
105
|
-
await bot.handleUpdate(textUpdate(1, 7, "/start"));
|
|
106
|
-
await bot.handleUpdate(textUpdate(2, 7, "小明"));
|
|
107
|
-
|
|
108
|
-
expect(sentTexts()).toEqual(["你叫什么名字?", "你好,小明!"]);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
两个 chat、两个独立向导 —— 交错投喂依然隔离,因为同一 chat 的更新按序串行:
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
await bot.handleUpdate(textUpdate(1, 7, "/start"));
|
|
115
|
-
await bot.handleUpdate(textUpdate(2, 8, "/start"));
|
|
116
|
-
await bot.handleUpdate(textUpdate(3, 7, "小明"));
|
|
117
|
-
await bot.handleUpdate(textUpdate(4, 8, "小红"));
|
|
118
|
-
expect(sentTexts()).toEqual(["你叫什么名字?", "你叫什么名字?", "你好,小明!", "你好,小红!"]);
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 6. 测试文件下载
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
transport.respond("getFile", { ok: true, result: { file_id: "F1", file_unique_id: "U1", file_path: "photos/pic.jpg" } });
|
|
125
|
-
transport.downloadBytes = new TextEncoder().encode("jpeg-bytes");
|
|
126
|
-
|
|
127
|
-
const file = await bot.downloadFile("F1", { destination: tmpFile });
|
|
128
|
-
expect(file.fileName).toBe("pic.jpg");
|
|
129
|
-
expect(transport.downloads).toEqual(["photos/pic.jpg"]);
|
|
130
|
-
expect(await readFile(tmpFile, "utf8")).toBe("jpeg-bytes");
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## 7. 测试 webhook
|
|
134
|
-
|
|
135
|
-
`createWebhookHandler` 接受真实的 Web `Request`:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
const handler = createWebhookHandler(bot, { secretToken: "s3cret" });
|
|
139
|
-
|
|
140
|
-
const response = await handler(new Request("https://example.com/telegram", {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "s3cret" },
|
|
143
|
-
body: JSON.stringify(createMockUpdate()),
|
|
144
|
-
}));
|
|
145
|
-
expect(response.status).toBe(200);
|
|
146
|
-
|
|
147
|
-
// 错误 secret → 在任何 handler 运行前被拒绝
|
|
148
|
-
const rejected = await handler(new Request("https://example.com/telegram", {
|
|
149
|
-
method: "POST",
|
|
150
|
-
headers: { "content-type": "application/json", "x-telegram-bot-api-secret-token": "wrong" },
|
|
151
|
-
body: JSON.stringify(createMockUpdate()),
|
|
152
|
-
}));
|
|
153
|
-
expect(rejected.status).toBe(401);
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## 8. 测试错误路径
|
|
157
|
-
|
|
158
|
-
```ts
|
|
159
|
-
// Telegram 拒绝调用
|
|
160
|
-
transport.respond("sendMessage", { ok: false, error_code: 400, description: "Bad Request: chat not found" });
|
|
161
|
-
await expect(ctx.reply("hi")).rejects.toThrow(/chat not found/);
|
|
162
|
-
|
|
163
|
-
// handler 抛异常 —— 断言错误边界看到了它
|
|
164
|
-
const errors: unknown[] = [];
|
|
165
|
-
bot.events.on("update:error", ({ error }) => errors.push(error));
|
|
166
|
-
bot.onText("boom", async () => { throw new Error("boom"); });
|
|
167
|
-
await bot.handleUpdate(textUpdate(1, 7, "boom"));
|
|
168
|
-
expect(errors).toHaveLength(1);
|
|
169
|
-
|
|
170
|
-
// handlerTimeout:挂起的更新以 UpdateTimeoutError 拒绝,而 handler 继续运行
|
|
171
|
-
const slow = new Bot({ token: "123456:TEST", transport, handlerTimeout: 50, branding: false, logger: { level: "silent" } });
|
|
172
|
-
let finished = false;
|
|
173
|
-
slow.on("message", async () => { await sleep(200); finished = true; });
|
|
174
|
-
await expect(slow.handleUpdate(createMockUpdate())).rejects.toBeInstanceOf(UpdateTimeoutError);
|
|
175
|
-
// 稍后:
|
|
176
|
-
expect(finished).toBe(true);
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## 9. Vitest / Jest 模式
|
|
180
|
-
|
|
181
|
-
**测试中静音 logger** —— `logger: { level: "silent" }` 不输出任何内容(有回归测试):
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
const bot = new Bot({ token: "123456:TEST", transport, branding: false, logger: { level: "silent" } });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
**每个测试全新 transport** —— 不要在用例间共享状态:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
beforeEach(() => { ({ bot, transport } = createTestBot()); });
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**快进时间** —— `Scheduler`/TTL 测试用假定时器或小间隔(`1ms`);cron 解析器是纯函数,无需定时器:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { nextCronOccurrence, parseCronExpression } from "@xbibzlibrary/telebibz";
|
|
197
|
-
expect(parseCronExpression("*/15 * * * *")).toBeDefined();
|
|
198
|
-
expect(nextCronOccurrence("0 9 * * 1", new Date("2026-08-29T00:00:00Z")).toISOString()).toContain("T09:00");
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**运行套件**:`npm test` —— 缺少 `TELEGRAM_BOT_TOKEN` 时,需要真实 token 的 E2E 测试自动跳过。
|
|
202
|
-
|
|
203
|
-
English: [TESTING.md](TESTING.md) · Bahasa Indonesia: [TESTING.id.md](TESTING.id.md)
|