]> git.giacomolli.uk - onechat/blob - telegram_test.go
Escape tags in rich replies as HTML entities
[onechat] / telegram_test.go
1 package main
2
3 import (
4 "log"
5 "net/http"
6 "os"
7 "path/filepath"
8 "slices"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/require"
14 )
15
16 func TestOwnerTalksToTheAgentThroughTelegram(t *testing.T) {
17 server := newModelFake(t)
18 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
19 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Hello from the agent.")}, 100, 0, 5))
20 }
21
22 telegram := newTelegramFake(t)
23 bot, _ := telegramStartTestBot(t, telegram)
24 socket := daemonStartTestWithBot(t, server, bot)
25 terminal := terminalStartTestClient(t, socket)
26 terminal.WaitFor(t, "[connected to OneChat]")
27
28 telegram.Add(TelegramUpdate{
29 UpdateID: 1,
30 Message: telegramOwnerMessage("hi from the phone"),
31 })
32
33 terminal.WaitFor(t, "[telegram] hi from the phone\n[working]\nHello from the agent.\n[done]")
34 telegramWaitForSent(t, telegram, "Hello from the agent.")
35 require.True(t, slices.ContainsFunc(telegram.Calls(), func(call TelegramFakeCall) bool {
36 return call.method == "sendChatAction" && call.params["action"] == "typing"
37 }))
38
39 terminal.Type("hi from the desk")
40 require.Equal(t, []string{"Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent."}, telegramWaitForSent(t, telegram, "Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent."))
41 }
42
43 func TestBotIgnoresEveryoneButTheOwner(t *testing.T) {
44 telegram := newTelegramFake(t)
45 bot, logs := telegramStartTestBot(t, telegram)
46 socket := daemonStartTestWithBot(t, newModelFake(t), bot)
47 terminal := terminalStartTestClient(t, socket)
48 terminal.WaitFor(t, "[connected to OneChat]")
49
50 group := telegramOwnerMessage("secret in a group")
51 group.Chat = TelegramChat{
52 ID: -100,
53 Type: "group",
54 }
55
56 stranger := telegramOwnerMessage("secret of a stranger")
57 stranger.Chat.ID = 77
58 stranger.From.ID = 77
59
60 unsigned := telegramOwnerMessage("secret with no sender")
61 unsigned.From = nil
62
63 for id, message := range []*TelegramMessage{group, stranger, unsigned, nil} {
64 telegram.Add(TelegramUpdate{
65 UpdateID: int64(10 + id),
66 Message: message,
67 })
68 }
69
70 require.Eventually(t, func() bool {
71 offset, err := telegramLoadOffset(bot.offsetPath)
72 return err == nil && offset == 14
73 }, 10*time.Second, time.Millisecond)
74
75 require.Empty(t, telegram.Sent())
76 require.NotContains(t, terminal.Output(), "secret")
77 require.NotContains(t, logs.String(), "secret")
78 require.Contains(t, logs.String(), "update ignored")
79 }
80
81 func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) {
82 telegram := newTelegramFake(t)
83 bot, _ := telegramStartTestBot(t, telegram)
84 daemonStartTestWithBot(t, newModelFake(t), bot)
85
86 telegram.Add(TelegramUpdate{
87 UpdateID: 1,
88 Message: telegramOwnerMessage(""),
89 })
90
91 telegramWaitForSent(t, telegram, telegramTextOnly)
92 }
93
94 func TestStopFromTelegramInterruptsTheTurn(t *testing.T) {
95 server := newModelFake(t)
96 arrived := make(chan struct{})
97 release := make(chan struct{})
98 t.Cleanup(func() {
99 close(release)
100 })
101
102 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
103 close(arrived)
104 <-release
105 }
106
107 telegram := newTelegramFake(t)
108 bot, _ := telegramStartTestBot(t, telegram)
109 daemonStartTestWithBot(t, server, bot)
110
111 telegram.Add(TelegramUpdate{
112 UpdateID: 1,
113 Message: telegramOwnerMessage("a long task"),
114 })
115
116 <-arrived
117 telegram.Add(TelegramUpdate{
118 UpdateID: 2,
119 Message: telegramOwnerMessage(stopCommand),
120 })
121
122 telegramWaitForSent(t, telegram, "Error: interrupted")
123
124 telegram.Add(TelegramUpdate{
125 UpdateID: 3,
126 Message: telegramOwnerMessage(stopCommand),
127 })
128
129 telegramWaitForSent(t, telegram, "Error: interrupted", "No turn runs.")
130 }
131
132 func TestBotStartsAtTheStoredOffset(t *testing.T) {
133 server := newModelFake(t)
134 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
135 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
136 }
137
138 telegram := newTelegramFake(t)
139 bot, _ := telegramStartTestBot(t, telegram)
140 require.NoError(t, telegramSaveOffset(bot.offsetPath, 10))
141
142 telegram.Add(TelegramUpdate{
143 UpdateID: 9,
144 Message: telegramOwnerMessage("handled before the restart"),
145 })
146
147 telegram.Add(TelegramUpdate{
148 UpdateID: 10,
149 Message: telegramOwnerMessage("new after the restart"),
150 })
151
152 socket := daemonStartTestWithBot(t, server, bot)
153 terminal := terminalStartTestClient(t, socket)
154 terminal.WaitFor(t, "[connected to OneChat]")
155 telegramWaitForSent(t, telegram, "ok")
156
157 require.Equal(t, float64(10), telegram.Calls()[0].params["offset"])
158 require.NotContains(t, terminal.Output(), "handled before the restart")
159
160 require.Eventually(t, func() bool {
161 offset, err := telegramLoadOffset(bot.offsetPath)
162 return err == nil && offset == 11
163 }, 10*time.Second, time.Millisecond)
164 }
165
166 func TestBotTriesAgainAfterFailures(t *testing.T) {
167 server := newModelFake(t)
168 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
169 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
170 }
171
172 telegram := newTelegramFake(t)
173 failures := map[string]int{
174 "getUpdates": 2,
175 "sendRichMessage": 1,
176 }
177
178 telegram.fail = func(call TelegramFakeCall) (int, string) {
179 if failures[call.method] == 0 {
180 return 0, ""
181 }
182
183 failures[call.method]--
184 if call.method == "sendRichMessage" {
185 return http.StatusTooManyRequests, "Too Many Requests: retry after 1"
186 }
187
188 return http.StatusBadGateway, "Bad Gateway"
189 }
190
191 bot, logs := telegramStartTestBot(t, telegram)
192 daemonStartTestWithBot(t, server, bot)
193
194 telegram.Add(TelegramUpdate{
195 UpdateID: 1,
196 Message: telegramOwnerMessage("hello"),
197 })
198
199 telegramWaitForSent(t, telegram, "ok")
200 require.Contains(t, logs.String(), "answered 502")
201 require.Contains(t, logs.String(), "answered 429")
202 require.NotContains(t, logs.String(), telegramTestToken)
203 }
204
205 func TestDeliverTriesAgainAfterATimeout(t *testing.T) {
206 telegram := newTelegramFake(t)
207 stalls := 1
208 telegram.stall = func(call TelegramFakeCall) bool {
209 if call.method != "sendMessage" || stalls == 0 {
210 return false
211 }
212
213 stalls--
214
215 return true
216 }
217
218 bot, logs := telegramStartTestBot(t, telegram)
219 bot.api.sendTimeout = 50 * time.Millisecond
220 bot.deliver(t.Context(), TelegramPart{
221 text: "hello",
222 })
223
224 require.Equal(t, []string{"hello"}, telegram.Sent())
225 require.Len(t, telegram.Calls(), 2)
226 require.Contains(t, logs.String(), "no valid answer")
227 require.NotContains(t, logs.String(), telegramTestToken)
228 }
229
230 func TestRepliesGoAsRichMessages(t *testing.T) {
231 reply := "**Done.** The table:\n\n| Name | Size |\n|:-----|-----:|\n| `a.go` | 10 |\n\n- one\n- two\n\n```go\nx := 1\n```"
232 server := newModelFake(t)
233 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
234 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(reply)}, 100, 0, 5))
235 }
236
237 telegram := newTelegramFake(t)
238 bot, _ := telegramStartTestBot(t, telegram)
239 socket := daemonStartTestWithBot(t, server, bot)
240 terminal := terminalStartTestClient(t, socket)
241 terminal.WaitFor(t, "[connected to OneChat]")
242
243 terminal.Type("make **it** so")
244 telegramWaitForSent(t, telegram, "[terminal] make **it** so", reply)
245
246 telegram.Add(TelegramUpdate{
247 UpdateID: 1,
248 Message: telegramOwnerMessage(""),
249 })
250
251 telegramWaitForSent(t, telegram, telegramTextOnly)
252
253 var sends []TelegramFakeCall
254 for _, call := range telegram.Calls() {
255 if call.method == "sendMessage" || call.method == "sendRichMessage" {
256 sends = append(sends, call)
257 }
258 }
259
260 require.Equal(t, []TelegramFakeCall{
261 {
262 method: "sendMessage",
263 params: map[string]any{
264 "chat_id": float64(telegramTestOwner),
265 "text": "[terminal] make **it** so",
266 },
267 ok: true,
268 },
269 {
270 method: "sendRichMessage",
271 params: map[string]any{
272 "chat_id": float64(telegramTestOwner),
273 "rich_message": map[string]any{
274 "markdown": reply,
275 },
276 },
277 ok: true,
278 },
279 {
280 method: "sendMessage",
281 params: map[string]any{
282 "chat_id": float64(telegramTestOwner),
283 "text": telegramTextOnly,
284 },
285 ok: true,
286 },
287 }, sends)
288 }
289
290 func TestRichMessageFailures(t *testing.T) {
291 testCases := []struct {
292 name string
293 status int
294 always bool
295 methods []string
296 sent []string
297 logText string
298 }{
299 {
300 name: "bad request sends the reply once with entities",
301 status: http.StatusBadRequest,
302 always: true,
303 methods: []string{"sendRichMessage", "sendMessage"},
304 sent: []string{"a secret word"},
305 logText: "rich message refused",
306 },
307 {
308 name: "unknown method sends the reply once with entities",
309 status: http.StatusNotFound,
310 always: true,
311 methods: []string{"sendRichMessage", "sendMessage"},
312 sent: []string{"a secret word"},
313 logText: "rich message refused",
314 },
315 {
316 name: "rate limit tries the rich message again",
317 status: http.StatusTooManyRequests,
318 methods: []string{"sendRichMessage", "sendRichMessage"},
319 sent: []string{"a **secret** word"},
320 logText: "answered 429",
321 },
322 {
323 name: "server error tries the rich message again",
324 status: http.StatusInternalServerError,
325 methods: []string{"sendRichMessage", "sendRichMessage"},
326 sent: []string{"a **secret** word"},
327 logText: "answered 500",
328 },
329 {
330 name: "other refusal drops the reply",
331 status: http.StatusForbidden,
332 always: true,
333 methods: []string{"sendRichMessage"},
334 logText: "message dropped",
335 },
336 }
337
338 for _, tc := range testCases {
339 t.Run(tc.name, func(t *testing.T) {
340 telegram := newTelegramFake(t)
341 failed := false
342 telegram.fail = func(call TelegramFakeCall) (int, string) {
343 if call.method != "sendRichMessage" || failed && !tc.always {
344 return 0, ""
345 }
346
347 failed = true
348
349 return tc.status, "refused for bot" + telegramTestToken
350 }
351
352 bot, logs := telegramStartTestBot(t, telegram)
353 bot.post(t.Context(), Event{
354 Kind: "reply",
355 Text: "a **secret** word",
356 })
357
358 var methods []string
359 for _, call := range telegram.Calls() {
360 methods = append(methods, call.method)
361 }
362
363 require.Equal(t, tc.methods, methods)
364 require.Equal(t, tc.sent, telegram.Sent())
365 require.Equal(t, 1, strings.Count(logs.String(), "\n"))
366 require.Contains(t, logs.String(), tc.logText)
367 require.NotContains(t, logs.String(), telegramTestToken)
368 require.NotContains(t, logs.String(), "secret word")
369
370 calls := telegram.Calls()
371 if calls[len(calls)-1].method == "sendMessage" {
372 require.NotNil(t, calls[len(calls)-1].params["entities"])
373 }
374 })
375 }
376 }
377
378 func TestLongReplyGoesAsRichMessagesSplitBetweenBlocks(t *testing.T) {
379 paragraphs := strings.Repeat("A paragraph of the reply.\n\n", telegramRichBytes/len("A paragraph of the reply.\n\n"))
380 table := "| a | b |\n|---|---|\n| 1 | 2 |\n"
381 reply := paragraphs + table
382
383 telegram := newTelegramFake(t)
384 bot, _ := telegramStartTestBot(t, telegram)
385 bot.post(t.Context(), Event{
386 Kind: "reply",
387 Text: reply,
388 })
389
390 require.Equal(t, []string{paragraphs, table}, telegram.Sent())
391 }
392
393 func TestReplyTagsGoAsText(t *testing.T) {
394 testCases := []struct {
395 name string
396 status int
397 sent []string
398 }{
399 {
400 name: "rich message escapes the tag outside code",
401 sent: []string{"a &lt;details> &amp; tag and `<b>`"},
402 },
403 {
404 name: "entity fallback shows the tag as written",
405 status: http.StatusBadRequest,
406 sent: []string{"a <details> & tag and <b>"},
407 },
408 }
409
410 for _, tc := range testCases {
411 t.Run(tc.name, func(t *testing.T) {
412 telegram := newTelegramFake(t)
413 telegram.fail = func(call TelegramFakeCall) (int, string) {
414 if call.method != "sendRichMessage" {
415 return 0, ""
416 }
417
418 return tc.status, "Bad Request"
419 }
420
421 bot, _ := telegramStartTestBot(t, telegram)
422 bot.post(t.Context(), Event{
423 Kind: "reply",
424 Text: "a <details> & tag and `<b>`",
425 })
426
427 require.Equal(t, tc.sent, telegram.Sent())
428 })
429 }
430 }
431
432 func TestLongReplySplitCountsTheEscapedTags(t *testing.T) {
433 paragraph := "A <b> tag.\n\n"
434 count := telegramRichBytes / len(paragraph)
435 reply := strings.Repeat(paragraph, count)
436 require.LessOrEqual(t, len(reply), telegramRichBytes)
437
438 telegram := newTelegramFake(t)
439 bot, _ := telegramStartTestBot(t, telegram)
440 bot.post(t.Context(), Event{
441 Kind: "reply",
442 Text: reply,
443 })
444
445 sent := telegram.Sent()
446 require.Len(t, sent, 2)
447 require.Equal(t, telegramEscapeTags(reply), strings.Join(sent, ""))
448
449 for _, markdown := range sent {
450 require.LessOrEqual(t, len(markdown), telegramRichBytes)
451 }
452 }
453
454 func TestRefusedRichPartGoesWithTheTextAsWritten(t *testing.T) {
455 paragraph := "A <b> & c.\n\n"
456 reply := strings.Repeat(paragraph, telegramRichBytes/len(paragraph))
457
458 telegram := newTelegramFake(t)
459 refused := false
460 telegram.fail = func(call TelegramFakeCall) (int, string) {
461 if call.method != "sendRichMessage" || refused {
462 return 0, ""
463 }
464
465 refused = true
466
467 return http.StatusBadRequest, "Bad Request"
468 }
469
470 bot, _ := telegramStartTestBot(t, telegram)
471 bot.post(t.Context(), Event{
472 Kind: "reply",
473 Text: reply,
474 })
475
476 var fallback, rich []string
477 for _, call := range telegram.Calls() {
478 switch {
479 case !call.ok:
480 case call.method == "sendMessage":
481 fallback = append(fallback, call.params["text"].(string))
482 case call.method == "sendRichMessage":
483 rich = append(rich, call.params["rich_message"].(map[string]any)["markdown"].(string))
484 }
485 }
486
487 require.Greater(t, len(fallback), 1)
488 require.Len(t, rich, 1)
489 require.True(t, strings.HasPrefix(reply, strings.Join(fallback, "")), "fallback text is not the reply as written")
490
491 for _, text := range fallback {
492 require.LessOrEqual(t, telegramUTF16Units(text), telegramMessageUnits)
493 }
494
495 require.True(t, reply == strings.Join(fallback, "")+telegramUnescapeTags(rich[0]), "reply is not sent once and whole")
496 }
497
498 func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) {
499 telegram := newTelegramFake(t)
500 telegram.fail = func(call TelegramFakeCall) (int, string) {
501 if call.params["entities"] == nil {
502 return 0, ""
503 }
504
505 return http.StatusBadRequest, "Bad Request: can't parse entities of bot" + telegramTestToken
506 }
507
508 bot, logs := telegramStartTestBot(t, telegram)
509 text, entities := telegramMarkdownEntities("a **secret** word")
510 bot.deliver(t.Context(), TelegramPart{
511 text: text,
512 entities: entities,
513 })
514
515 calls := telegram.Calls()
516 require.Len(t, calls, 2)
517 require.NotNil(t, calls[0].params["entities"])
518 require.Nil(t, calls[1].params["entities"])
519 require.Equal(t, []string{"a secret word"}, telegram.Sent())
520 require.Equal(t, 1, strings.Count(logs.String(), "\n"))
521 require.Contains(t, logs.String(), "entities refused")
522 require.NotContains(t, logs.String(), telegramTestToken)
523 require.NotContains(t, logs.String(), "secret word")
524 }
525
526 func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) {
527 telegram := newTelegramFake(t)
528 telegram.Close()
529
530 bot, logs := telegramStartTestBot(t, telegram)
531 daemonStartTestWithBot(t, newModelFake(t), bot)
532
533 require.Eventually(t, func() bool {
534 return strings.Count(logs.String(), "no valid answer") >= 2
535 }, 10*time.Second, time.Millisecond)
536 require.NotContains(t, logs.String(), telegramTestToken)
537 }
538
539 func TestSplitMessage(t *testing.T) {
540 testCases := []struct {
541 name string
542 text string
543 limit int
544 parts []string
545 }{
546 {
547 name: "short text stays whole",
548 text: "one two",
549 limit: 10,
550 parts: []string{"one two"},
551 },
552 {
553 name: "cut after the last line break",
554 text: "first line\nsecond line",
555 limit: 16,
556 parts: []string{"first line\n", "second line"},
557 },
558 {
559 name: "cut after the last space without a line break",
560 text: "aaaa bbbb cccc",
561 limit: 12,
562 parts: []string{"aaaa bbbb ", "cccc"},
563 },
564 {
565 name: "hard cut of one long word",
566 text: "abcdefghij",
567 limit: 4,
568 parts: []string{"abcd", "efgh", "ij"},
569 },
570 {
571 name: "a character outside the BMP counts two units and is not cut",
572 text: "ab\U0001F600cd",
573 limit: 3,
574 parts: []string{"ab", "\U0001F600c", "d"},
575 },
576 }
577
578 for _, tc := range testCases {
579 t.Run(tc.name, func(t *testing.T) {
580 require.Equal(t, tc.parts, telegramSplitMessage(tc.text, tc.limit))
581 })
582 }
583
584 long := strings.Repeat("word ", 2000)
585 parts := telegramSplitMessage(long, telegramMessageUnits)
586 require.Len(t, parts, 3)
587 require.Equal(t, long, strings.Join(parts, ""))
588 }
589
590 func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) {
591 parts := telegramSplitEntities("aabbbbcc", []TelegramEntity{
592 {
593 Type: "bold",
594 Offset: 2,
595 Length: 4,
596 },
597 {
598 Type: "italic",
599 Offset: 6,
600 Length: 2,
601 },
602 }, 5)
603
604 require.Equal(t, []TelegramPart{
605 {
606 text: "aabbb",
607 entities: []TelegramEntity{
608 {
609 Type: "bold",
610 Offset: 2,
611 Length: 3,
612 },
613 },
614 },
615 {
616 text: "bcc",
617 entities: []TelegramEntity{
618 {
619 Type: "bold",
620 Offset: 0,
621 Length: 1,
622 },
623 {
624 Type: "italic",
625 Offset: 1,
626 Length: 2,
627 },
628 },
629 },
630 }, parts)
631 }
632
633 func TestSplitRich(t *testing.T) {
634 testCases := []struct {
635 name string
636 markdown string
637 limit int
638 parts []string
639 }{
640 {
641 name: "short reply stays whole",
642 markdown: "| a | b |",
643 limit: 100,
644 parts: []string{"| a | b |"},
645 },
646 {
647 name: "cut after a blank line keeps a table whole",
648 markdown: "Intro.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n",
649 limit: 30,
650 parts: []string{"Intro.\n\n", "| a | b |\n|---|---|\n| 1 | 2 |\n"},
651 },
652 {
653 name: "blank line in a code block is not a cut",
654 markdown: "Text.\n\n```go\nx\n\ny\n```\n\nEnd.\n",
655 limit: 20,
656 parts: []string{"Text.\n\n", "```go\nx\n\ny\n```\n\n", "End.\n"},
657 },
658 {
659 name: "paragraph with no blank line is cut after a line",
660 markdown: "aaa\nbbb\nccc\n",
661 limit: 9,
662 parts: []string{"aaa\nbbb\n", "ccc\n"},
663 },
664 {
665 name: "long code block is closed and opened again",
666 markdown: "```sh\none\ntwo\nthree\n```\n",
667 limit: 16,
668 parts: []string{"```sh\none\n```\n", "```sh\ntwo\n```\n", "```sh\nthree\n```\n"},
669 },
670 {
671 name: "long line is cut at the limit and not inside a character",
672 markdown: "ab\u00e9cd",
673 limit: 3,
674 parts: []string{"ab", "\u00e9c", "d"},
675 },
676 {
677 name: "long line is not cut inside an entity",
678 markdown: "abc&amp;def",
679 limit: 6,
680 parts: []string{"abc", "&amp;d", "ef"},
681 },
682 }
683
684 for _, tc := range testCases {
685 t.Run(tc.name, func(t *testing.T) {
686 require.Equal(t, tc.parts, telegramSplitRich(tc.markdown, tc.limit))
687 })
688 }
689
690 long := strings.Repeat("A paragraph of the reply.\n\n", 2000)
691 parts := telegramSplitRich(long, telegramRichBytes)
692 require.Len(t, parts, 2)
693 require.Len(t, parts[0], telegramRichBytes-telegramRichBytes%len("A paragraph of the reply.\n\n"))
694 require.Equal(t, long, strings.Join(parts, ""))
695 }
696
697 func TestLoadBotConfig(t *testing.T) {
698 testCases := []struct {
699 name string
700 content string
701 mode os.FileMode
702 errorText string
703 }{
704 {
705 name: "owner file is read",
706 content: `{"token": "` + telegramTestToken + `", "owner": 4242}`,
707 mode: 0o600,
708 },
709 {
710 name: "file that others can read is refused",
711 content: `{"token": "` + telegramTestToken + `", "owner": 4242}`,
712 mode: 0o640,
713 errorText: "mode must be 0600",
714 },
715 {
716 name: "token with a path character is refused",
717 content: `{"token": "123:abc/../x", "owner": 4242}`,
718 mode: 0o600,
719 errorText: "form of a bot token",
720 },
721 {
722 name: "missing owner is refused",
723 content: `{"token": "` + telegramTestToken + `"}`,
724 mode: 0o600,
725 errorText: "numeric Telegram user ID",
726 },
727 {
728 name: "unknown field is refused",
729 content: `{"token": "` + telegramTestToken + `", "owner": 4242, "chat": 1}`,
730 mode: 0o600,
731 errorText: "not valid JSON",
732 },
733 }
734
735 for _, tc := range testCases {
736 t.Run(tc.name, func(t *testing.T) {
737 dir := t.TempDir()
738 path := filepath.Join(dir, telegramConfigFileName)
739 require.NoError(t, os.WriteFile(path, []byte(tc.content), tc.mode))
740 require.NoError(t, os.Chmod(path, tc.mode))
741
742 config, found, err := loadTelegramConfig(dir)
743 if tc.errorText != "" {
744 require.ErrorContains(t, err, tc.errorText)
745 require.NotContains(t, err.Error(), telegramTestToken)
746
747 return
748 }
749
750 require.NoError(t, err)
751 require.True(t, found)
752 require.Equal(t, TelegramConfig{
753 Token: telegramTestToken,
754 Owner: telegramTestOwner,
755 }, config)
756 })
757 }
758
759 _, found, err := loadTelegramConfig(t.TempDir())
760 require.NoError(t, err)
761 require.False(t, found)
762 }
763
764 // telegramStartTestBot returns a bot of telegramTestOwner on server, with its
765 // offset file in a temporary directory and short waits, and the buffer of its
766 // log.
767 func telegramStartTestBot(t *testing.T, server *TelegramFake) (*TelegramBot, *SyncBuffer) {
768 t.Helper()
769
770 logs := &SyncBuffer{}
771 bot := newTelegramBot(newTelegramAPI(server.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), log.New(logs, "", 0))
772 bot.minWait = time.Millisecond
773 bot.maxWait = 10 * time.Millisecond
774
775 return bot, logs
776 }
777
778 // telegramWaitForSent waits until the bot sent the texts in order, and returns
779 // them.
780 func telegramWaitForSent(t *testing.T, server *TelegramFake, texts ...string) []string {
781 t.Helper()
782
783 require.Eventually(t, func() bool {
784 sent := server.Sent()
785 return len(sent) >= len(texts) && slices.Equal(sent[len(sent)-len(texts):], texts)
786 }, 10*time.Second, time.Millisecond, "sent so far: %q", server.Sent())
787
788 return server.Sent()
789 }