@crewhaus/channel-adapter-telegram 0.1.3 → 0.1.5

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/src/index.test.ts DELETED
@@ -1,620 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- import {
6
- type ChannelAdapter,
7
- type InboundEvent,
8
- type ParsedInbound,
9
- type RawRequest,
10
- TelegramAdapterError,
11
- createTelegramAdapter,
12
- verifyTelegramSecret,
13
- } from "./index";
14
-
15
- // `tsc -b` also compiles this file into `dist/`; resolve fixtures from the
16
- // source tree so both the src and dist test copies find them.
17
- const FIXTURES_DIR = join(import.meta.dir.replace(/([/\\])dist$/, "$1src"), "fixtures");
18
- const fixture = (name: string) => readFileSync(join(FIXTURES_DIR, `${name}.json`), "utf8");
19
-
20
- const SECRET = "tg-secret-shhh";
21
- const BOT_TOKEN = "1234:test-token";
22
- const ADAPTER_OPTS = (extras: Record<string, unknown> = {}) => ({
23
- apiBaseUrl: "https://test.telegram.local",
24
- fetch: extras["fetch"] as typeof fetch | undefined,
25
- selfBotId: "9999",
26
- });
27
-
28
- function withHeaders(token: string | undefined, body: string): RawRequest {
29
- const h = new Headers();
30
- if (token !== undefined) h.set("X-Telegram-Bot-Api-Secret-Token", token);
31
- return { headers: h, body };
32
- }
33
-
34
- function adapter(extra: Record<string, unknown> = {}): ChannelAdapter {
35
- return createTelegramAdapter({ botToken: BOT_TOKEN, secretToken: SECRET }, ADAPTER_OPTS(extra));
36
- }
37
-
38
- describe("verifyTelegramSecret (T8)", () => {
39
- test("matches a valid secret token", () => {
40
- const h = new Headers({ "X-Telegram-Bot-Api-Secret-Token": SECRET });
41
- expect(verifyTelegramSecret({ headers: h, secretToken: SECRET })).toBe(true);
42
- });
43
-
44
- test("rejects a tampered secret", () => {
45
- const h = new Headers({ "X-Telegram-Bot-Api-Secret-Token": `${SECRET}-tampered` });
46
- expect(verifyTelegramSecret({ headers: h, secretToken: SECRET })).toBe(false);
47
- });
48
-
49
- test("rejects a missing header", () => {
50
- expect(verifyTelegramSecret({ headers: new Headers(), secretToken: SECRET })).toBe(false);
51
- });
52
-
53
- test("constant-time guard rejects different lengths", () => {
54
- const h = new Headers({ "X-Telegram-Bot-Api-Secret-Token": "x" });
55
- expect(verifyTelegramSecret({ headers: h, secretToken: SECRET })).toBe(false);
56
- });
57
-
58
- test("rejects empty supplied token", () => {
59
- const h = new Headers({ "X-Telegram-Bot-Api-Secret-Token": "" });
60
- expect(verifyTelegramSecret({ headers: h, secretToken: SECRET })).toBe(false);
61
- });
62
-
63
- test("rejects when UTF-16 length matches but UTF-8 byte length differs", () => {
64
- // "é" is one UTF-16 code unit but two UTF-8 bytes; "e" is one unit / one
65
- // byte. They pass the `.length` check yet must be rejected by the
66
- // byte-length guard before timingSafeEqual (which throws on size mismatch).
67
- const h = new Headers({ "X-Telegram-Bot-Api-Secret-Token": "é" });
68
- expect(verifyTelegramSecret({ headers: h, secretToken: "e" })).toBe(false);
69
- });
70
- });
71
-
72
- describe("createTelegramAdapter.verify()", () => {
73
- test("forwards header to verifyTelegramSecret", () => {
74
- const a = adapter();
75
- expect(a.verify(withHeaders(SECRET, "{}"))).toBe(true);
76
- expect(a.verify(withHeaders("wrong-secret-here", "{}"))).toBe(false);
77
- expect(a.verify(withHeaders(undefined, "{}"))).toBe(false);
78
- });
79
- });
80
-
81
- describe("parseInbound — fixtures (T2)", () => {
82
- test("private_message → event with workspaceId=chat.id, channelId=chat.id", () => {
83
- const a = adapter();
84
- const result = a.parseInbound(withHeaders(SECRET, fixture("private_message"))) as Extract<
85
- ParsedInbound,
86
- { kind: "event" }
87
- >;
88
- expect(result.kind).toBe("event");
89
- expect(result.event.workspaceId).toBe("4242");
90
- expect(result.event.channelId).toBe("4242");
91
- expect(result.event.userId).toBe("4242");
92
- expect(result.event.text).toBe("hello bot");
93
- expect(result.event.idempotencyKey).toBe("100001");
94
- expect(result.event.subtype).toBe("message");
95
- expect(result.event.threadTs).toBeUndefined();
96
- });
97
-
98
- test("group_message → event with workspaceId = supergroup chat.id", () => {
99
- const a = adapter();
100
- const r = a.parseInbound(withHeaders(SECRET, fixture("group_message"))) as Extract<
101
- ParsedInbound,
102
- { kind: "event" }
103
- >;
104
- expect(r.kind).toBe("event");
105
- expect(r.event.workspaceId).toBe("-100123");
106
- expect(r.event.channelId).toBe("-100123");
107
- expect(r.event.threadTs).toBeUndefined();
108
- });
109
-
110
- test("group_topic_message → channelId = chatId:topicId, threadTs set", () => {
111
- const a = adapter();
112
- const r = a.parseInbound(withHeaders(SECRET, fixture("group_topic_message"))) as Extract<
113
- ParsedInbound,
114
- { kind: "event" }
115
- >;
116
- expect(r.event.channelId).toBe("-100123:17");
117
- expect(r.event.threadTs).toBe("17");
118
- });
119
-
120
- test("edited_message produces a normal event (gateway dedups via update_id)", () => {
121
- const a = adapter();
122
- const r = a.parseInbound(withHeaders(SECRET, fixture("edited_message"))) as Extract<
123
- ParsedInbound,
124
- { kind: "event" }
125
- >;
126
- expect(r.kind).toBe("event");
127
- expect(r.event.idempotencyKey).toBe("100004");
128
- expect(r.event.text).toBe("hello bot (edited)");
129
- });
130
-
131
- test("callback_query → event whose text is the callback `data`", () => {
132
- const a = adapter();
133
- const r = a.parseInbound(withHeaders(SECRET, fixture("callback_query"))) as Extract<
134
- ParsedInbound,
135
- { kind: "event" }
136
- >;
137
- expect(r.kind).toBe("event");
138
- expect(r.event.text).toBe("approve:123");
139
- expect(r.event.idempotencyKey).toBe("100005");
140
- });
141
-
142
- test("bot_mention with mention entity → subtype app_mention", () => {
143
- const a = adapter();
144
- const r = a.parseInbound(withHeaders(SECRET, fixture("bot_mention"))) as Extract<
145
- ParsedInbound,
146
- { kind: "event" }
147
- >;
148
- expect(r.event.subtype).toBe("app_mention");
149
- });
150
-
151
- test("sticker_only (no text/caption) → skip", () => {
152
- const a = adapter();
153
- expect(a.parseInbound(withHeaders(SECRET, fixture("sticker_only"))).kind).toBe("skip");
154
- });
155
-
156
- test("photo_with_caption → event with caption as text", () => {
157
- const a = adapter();
158
- const r = a.parseInbound(withHeaders(SECRET, fixture("photo_with_caption"))) as Extract<
159
- ParsedInbound,
160
- { kind: "event" }
161
- >;
162
- expect(r.event.text).toBe("look at this graph");
163
- });
164
-
165
- test("bot_self matching selfBotId → skip", () => {
166
- const a = adapter();
167
- expect(a.parseInbound(withHeaders(SECRET, fixture("bot_self"))).kind).toBe("skip");
168
- });
169
-
170
- test("missing_chat → skip", () => {
171
- const a = adapter();
172
- expect(a.parseInbound(withHeaders(SECRET, fixture("missing_chat"))).kind).toBe("skip");
173
- });
174
-
175
- test("non_message_update (poll) → skip", () => {
176
- const a = adapter();
177
- expect(a.parseInbound(withHeaders(SECRET, fixture("non_message_update"))).kind).toBe("skip");
178
- });
179
-
180
- test("malformed JSON body → skip", () => {
181
- const a = adapter();
182
- expect(a.parseInbound(withHeaders(SECRET, "{not-json")).kind).toBe("skip");
183
- });
184
-
185
- test("empty body → skip", () => {
186
- const a = adapter();
187
- expect(a.parseInbound(withHeaders(SECRET, "null")).kind).toBe("skip");
188
- });
189
-
190
- test("non-numeric update_id → skip", () => {
191
- const a = adapter();
192
- const body = JSON.stringify({ update_id: "not-a-number", message: {} });
193
- expect(a.parseInbound(withHeaders(SECRET, body)).kind).toBe("skip");
194
- });
195
-
196
- test("JSON array body (object, not null, but not an Update) → skip", () => {
197
- const a = adapter();
198
- // Arrays are typeof "object" and non-null; they lack a numeric update_id.
199
- expect(a.parseInbound(withHeaders(SECRET, "[1,2,3]")).kind).toBe("skip");
200
- });
201
-
202
- test("message with chat but no `from` (userId undefined) → skip", () => {
203
- const a = adapter();
204
- const body = JSON.stringify({
205
- update_id: 200001,
206
- message: { message_id: 5, chat: { id: 9, type: "private" }, text: "hi" },
207
- });
208
- expect(a.parseInbound(withHeaders(SECRET, body)).kind).toBe("skip");
209
- });
210
-
211
- test("message text prefers `text` over `caption`", () => {
212
- const a = adapter();
213
- const body = JSON.stringify({
214
- update_id: 200002,
215
- message: {
216
- message_id: 6,
217
- from: { id: 1 },
218
- chat: { id: 9, type: "private" },
219
- text: "the-text",
220
- caption: "the-caption",
221
- },
222
- });
223
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
224
- ParsedInbound,
225
- { kind: "event" }
226
- >;
227
- expect(r.event.text).toBe("the-text");
228
- });
229
-
230
- test("bot_command entity → subtype app_mention", () => {
231
- const a = adapter();
232
- const body = JSON.stringify({
233
- update_id: 200003,
234
- message: {
235
- message_id: 7,
236
- from: { id: 1 },
237
- chat: { id: 9, type: "private" },
238
- text: "/start",
239
- entities: [{ type: "bot_command", offset: 0, length: 6 }],
240
- },
241
- });
242
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
243
- ParsedInbound,
244
- { kind: "event" }
245
- >;
246
- expect(r.event.subtype).toBe("app_mention");
247
- });
248
-
249
- test("entity with unrelated type (e.g. `url`) → subtype message", () => {
250
- const a = adapter();
251
- const body = JSON.stringify({
252
- update_id: 200004,
253
- message: {
254
- message_id: 8,
255
- from: { id: 1 },
256
- chat: { id: 9, type: "private" },
257
- text: "see https://example.com",
258
- entities: [{ type: "url", offset: 4, length: 19 }],
259
- },
260
- });
261
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
262
- ParsedInbound,
263
- { kind: "event" }
264
- >;
265
- expect(r.event.subtype).toBe("message");
266
- });
267
-
268
- test("bot message is NOT skipped when no selfBotId is configured", () => {
269
- // selfBotId-less adapter must let bot-authored messages through (the
270
- // `opts.selfBotId !== undefined` short-circuit guards this branch).
271
- const a = createTelegramAdapter(
272
- { botToken: BOT_TOKEN, secretToken: SECRET },
273
- { apiBaseUrl: "https://test.telegram.local" },
274
- );
275
- const body = JSON.stringify({
276
- update_id: 200005,
277
- message: {
278
- message_id: 9,
279
- from: { id: 9999, is_bot: true, username: "crewhaus_bot" },
280
- chat: { id: 9, type: "private" },
281
- text: "I am the bot",
282
- },
283
- });
284
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
285
- ParsedInbound,
286
- { kind: "event" }
287
- >;
288
- expect(r.kind).toBe("event");
289
- expect(r.event.userId).toBe("9999");
290
- });
291
-
292
- test("non-self bot message is NOT skipped even with selfBotId set", () => {
293
- // selfBotId is set, message is from a bot, but a *different* bot id →
294
- // exercises the `String(from.id) === selfBotId` false branch.
295
- const a = adapter();
296
- const body = JSON.stringify({
297
- update_id: 200006,
298
- message: {
299
- message_id: 10,
300
- from: { id: 8888, is_bot: true, username: "other_bot" },
301
- chat: { id: 9, type: "private" },
302
- text: "different bot",
303
- },
304
- });
305
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
306
- ParsedInbound,
307
- { kind: "event" }
308
- >;
309
- expect(r.kind).toBe("event");
310
- expect(r.event.userId).toBe("8888");
311
- });
312
-
313
- test("edited_message with neither text nor caption → skip", () => {
314
- const a = adapter();
315
- const body = JSON.stringify({
316
- update_id: 200007,
317
- edited_message: { message_id: 11, from: { id: 1 }, chat: { id: 9, type: "private" } },
318
- });
319
- expect(a.parseInbound(withHeaders(SECRET, body)).kind).toBe("skip");
320
- });
321
-
322
- test("edited_message in a topic → channelId chatId:topicId, threadTs set", () => {
323
- const a = adapter();
324
- const body = JSON.stringify({
325
- update_id: 200008,
326
- edited_message: {
327
- message_id: 12,
328
- from: { id: 1 },
329
- chat: { id: -100123, type: "supergroup" },
330
- message_thread_id: 21,
331
- text: "edited in topic",
332
- },
333
- });
334
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
335
- ParsedInbound,
336
- { kind: "event" }
337
- >;
338
- expect(r.event.channelId).toBe("-100123:21");
339
- expect(r.event.threadTs).toBe("21");
340
- });
341
-
342
- test("callback_query without `message` → skip", () => {
343
- const a = adapter();
344
- const body = JSON.stringify({
345
- update_id: 200009,
346
- callback_query: { id: "cq", from: { id: 7 }, data: "go" },
347
- });
348
- expect(a.parseInbound(withHeaders(SECRET, body)).kind).toBe("skip");
349
- });
350
-
351
- test("callback_query without `from` (userId undefined) → skip", () => {
352
- const a = adapter();
353
- const body = JSON.stringify({
354
- update_id: 200010,
355
- callback_query: {
356
- id: "cq",
357
- data: "go",
358
- message: { message_id: 3, chat: { id: 9, type: "private" } },
359
- },
360
- });
361
- expect(a.parseInbound(withHeaders(SECRET, body)).kind).toBe("skip");
362
- });
363
-
364
- test("callback_query whose message lacks `chat` → skip", () => {
365
- const a = adapter();
366
- const body = JSON.stringify({
367
- update_id: 200011,
368
- callback_query: { id: "cq", from: { id: 7 }, data: "go", message: { message_id: 3 } },
369
- });
370
- expect(a.parseInbound(withHeaders(SECRET, body)).kind).toBe("skip");
371
- });
372
-
373
- test("callback_query without `data` → text defaults to empty string", () => {
374
- const a = adapter();
375
- const body = JSON.stringify({
376
- update_id: 200012,
377
- callback_query: {
378
- id: "cq",
379
- from: { id: 7 },
380
- message: { message_id: 3, chat: { id: 9, type: "private" } },
381
- },
382
- });
383
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
384
- ParsedInbound,
385
- { kind: "event" }
386
- >;
387
- expect(r.kind).toBe("event");
388
- expect(r.event.text).toBe("");
389
- });
390
-
391
- test("callback_query in a topic → channelId chatId:topicId, threadTs set", () => {
392
- const a = adapter();
393
- const body = JSON.stringify({
394
- update_id: 200013,
395
- callback_query: {
396
- id: "cq",
397
- from: { id: 7 },
398
- data: "approve",
399
- message: {
400
- message_id: 3,
401
- chat: { id: -100123, type: "supergroup" },
402
- message_thread_id: 42,
403
- },
404
- },
405
- });
406
- const r = a.parseInbound(withHeaders(SECRET, body)) as Extract<
407
- ParsedInbound,
408
- { kind: "event" }
409
- >;
410
- expect(r.event.channelId).toBe("-100123:42");
411
- expect(r.event.threadTs).toBe("42");
412
- expect(r.event.text).toBe("approve");
413
- });
414
- });
415
-
416
- describe("sendReply / setTyping (T3)", () => {
417
- function captureFetch() {
418
- const calls: Array<{ url: string; init: RequestInit }> = [];
419
- const f = (async (input: string | Request | URL, init?: RequestInit) => {
420
- calls.push({ url: String(input), init: init ?? {} });
421
- return new Response(JSON.stringify({ ok: true, result: { message_id: 999 } }), {
422
- status: 200,
423
- headers: { "content-type": "application/json" },
424
- });
425
- }) as unknown as typeof fetch;
426
- return { calls, fetch: f };
427
- }
428
-
429
- const event: InboundEvent = {
430
- idempotencyKey: "100001",
431
- workspaceId: "4242",
432
- channelId: "4242",
433
- userId: "4242",
434
- ts: "7",
435
- text: "hello",
436
- subtype: "message",
437
- };
438
-
439
- test("sendReply POSTs sendMessage with correct chat_id + text", async () => {
440
- const { calls, fetch: f } = captureFetch();
441
- const a = createTelegramAdapter(
442
- { botToken: BOT_TOKEN, secretToken: SECRET },
443
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
444
- );
445
- await a.sendReply({ event, text: "hi back" });
446
- expect(calls.length).toBe(1);
447
- expect(calls[0]?.url).toBe(`https://test.telegram.local/bot${BOT_TOKEN}/sendMessage`);
448
- const body = JSON.parse(String(calls[0]?.init.body));
449
- expect(body.chat_id).toBe(4242);
450
- expect(body.text).toBe("hi back");
451
- expect(body.message_thread_id).toBeUndefined();
452
- });
453
-
454
- test("sendReply forwards message_thread_id when threadTs set", async () => {
455
- const { calls, fetch: f } = captureFetch();
456
- const a = createTelegramAdapter(
457
- { botToken: BOT_TOKEN, secretToken: SECRET },
458
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
459
- );
460
- await a.sendReply({
461
- event: { ...event, threadTs: "17", channelId: "4242:17" },
462
- text: "topic reply",
463
- });
464
- const body = JSON.parse(String(calls[0]?.init.body));
465
- expect(body.message_thread_id).toBe(17);
466
- });
467
-
468
- test("sendReply throws on HTTP error", async () => {
469
- const f = (async () =>
470
- new Response("boom", { status: 502, statusText: "Bad Gateway" })) as unknown as typeof fetch;
471
- const a = createTelegramAdapter(
472
- { botToken: BOT_TOKEN, secretToken: SECRET },
473
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
474
- );
475
- await expect(a.sendReply({ event, text: "x" })).rejects.toThrow(TelegramAdapterError);
476
- });
477
-
478
- test("sendReply throws on Telegram-side ok:false", async () => {
479
- const f = (async () =>
480
- new Response(JSON.stringify({ ok: false, description: "chat not found" }), {
481
- status: 200,
482
- headers: { "content-type": "application/json" },
483
- })) as unknown as typeof fetch;
484
- const a = createTelegramAdapter(
485
- { botToken: BOT_TOKEN, secretToken: SECRET },
486
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
487
- );
488
- await expect(a.sendReply({ event, text: "x" })).rejects.toThrow(/chat not found/);
489
- });
490
-
491
- test("sendReply rejects malformed workspaceId", async () => {
492
- const a = adapter();
493
- await expect(
494
- a.sendReply({ event: { ...event, workspaceId: "not-a-number" }, text: "x" }),
495
- ).rejects.toThrow(/invalid workspaceId/);
496
- });
497
-
498
- test("setTyping POSTs sendChatAction with action=typing", async () => {
499
- const { calls, fetch: f } = captureFetch();
500
- const a = createTelegramAdapter(
501
- { botToken: BOT_TOKEN, secretToken: SECRET },
502
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
503
- );
504
- await a.setTyping({ event });
505
- expect(calls.length).toBe(1);
506
- expect(calls[0]?.url).toBe(`https://test.telegram.local/bot${BOT_TOKEN}/sendChatAction`);
507
- const body = JSON.parse(String(calls[0]?.init.body));
508
- expect(body.chat_id).toBe(4242);
509
- expect(body.action).toBe("typing");
510
- });
511
-
512
- test("sendReply tolerates a 2xx response without a JSON content-type", async () => {
513
- // res.ok is true but content-type is not application/json, so the
514
- // ok:false body inspection is skipped and no error is thrown.
515
- const f = (async () =>
516
- new Response("OK", { status: 200, statusText: "OK" })) as unknown as typeof fetch;
517
- const a = createTelegramAdapter(
518
- { botToken: BOT_TOKEN, secretToken: SECRET },
519
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
520
- );
521
- await expect(a.sendReply({ event, text: "x" })).resolves.toBeUndefined();
522
- });
523
-
524
- test("sendReply accepts a 2xx JSON response with ok:true", async () => {
525
- const f = (async () =>
526
- new Response(JSON.stringify({ ok: true, result: { message_id: 1 } }), {
527
- status: 200,
528
- headers: { "content-type": "application/json" },
529
- })) as unknown as typeof fetch;
530
- const a = createTelegramAdapter(
531
- { botToken: BOT_TOKEN, secretToken: SECRET },
532
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
533
- );
534
- await expect(a.sendReply({ event, text: "x" })).resolves.toBeUndefined();
535
- });
536
-
537
- test("setTyping forwards message_thread_id when threadTs is set", async () => {
538
- const { calls, fetch: f } = captureFetch();
539
- const a = createTelegramAdapter(
540
- { botToken: BOT_TOKEN, secretToken: SECRET },
541
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
542
- );
543
- await a.setTyping({ event: { ...event, threadTs: "17", channelId: "4242:17" } });
544
- const body = JSON.parse(String(calls[0]?.init.body));
545
- expect(body.message_thread_id).toBe(17);
546
- });
547
-
548
- test("setTyping rejects malformed workspaceId", async () => {
549
- const a = adapter();
550
- await expect(a.setTyping({ event: { ...event, workspaceId: "not-a-number" } })).rejects.toThrow(
551
- /invalid workspaceId/,
552
- );
553
- });
554
-
555
- test("setTyping ignores a non-200 response (best-effort)", async () => {
556
- // setTyping never checks res.ok; a 500 must NOT reject.
557
- let called = false;
558
- const f = (async () => {
559
- called = true;
560
- return new Response("nope", { status: 500, statusText: "Server Error" });
561
- }) as unknown as typeof fetch;
562
- const a = createTelegramAdapter(
563
- { botToken: BOT_TOKEN, secretToken: SECRET },
564
- { apiBaseUrl: "https://test.telegram.local", fetch: f },
565
- );
566
- await expect(a.setTyping({ event })).resolves.toBeUndefined();
567
- expect(called).toBe(true);
568
- });
569
- });
570
-
571
- describe("apiBaseUrl resolution", () => {
572
- const event: InboundEvent = {
573
- idempotencyKey: "1",
574
- workspaceId: "4242",
575
- channelId: "4242",
576
- userId: "4242",
577
- ts: "7",
578
- text: "hi",
579
- subtype: "message",
580
- };
581
-
582
- function captureUrlFetch() {
583
- const calls: string[] = [];
584
- const f = (async (input: string | Request | URL) => {
585
- calls.push(String(input));
586
- return new Response(JSON.stringify({ ok: true }), {
587
- status: 200,
588
- headers: { "content-type": "application/json" },
589
- });
590
- }) as unknown as typeof fetch;
591
- return { calls, fetch: f };
592
- }
593
-
594
- test("falls back to TELEGRAM_API_BASE_URL env var when no apiBaseUrl option", async () => {
595
- const prev = process.env["TELEGRAM_API_BASE_URL"];
596
- process.env["TELEGRAM_API_BASE_URL"] = "https://env.telegram.local";
597
- try {
598
- const { calls, fetch: f } = captureUrlFetch();
599
- const a = createTelegramAdapter({ botToken: BOT_TOKEN, secretToken: SECRET }, { fetch: f });
600
- await a.sendReply({ event, text: "x" });
601
- expect(calls[0]).toBe(`https://env.telegram.local/bot${BOT_TOKEN}/sendMessage`);
602
- } finally {
603
- if (prev === undefined) Reflect.deleteProperty(process.env, "TELEGRAM_API_BASE_URL");
604
- else process.env["TELEGRAM_API_BASE_URL"] = prev;
605
- }
606
- });
607
-
608
- test("falls back to the public Telegram API host when nothing is configured", async () => {
609
- const prev = process.env["TELEGRAM_API_BASE_URL"];
610
- Reflect.deleteProperty(process.env, "TELEGRAM_API_BASE_URL");
611
- try {
612
- const { calls, fetch: f } = captureUrlFetch();
613
- const a = createTelegramAdapter({ botToken: BOT_TOKEN, secretToken: SECRET }, { fetch: f });
614
- await a.sendReply({ event, text: "x" });
615
- expect(calls[0]).toBe(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`);
616
- } finally {
617
- if (prev !== undefined) process.env["TELEGRAM_API_BASE_URL"] = prev;
618
- }
619
- });
620
- });