X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/f80b058ae0550a56b4e62f8e65c2a74349b970e4..4a931bccab5879ab708647b2df8dadc052f128bf:/telegram_test.go diff --git a/telegram_test.go b/telegram_test.go index 2578b77..81e6e18 100644 --- a/telegram_test.go +++ b/telegram_test.go @@ -14,61 +14,61 @@ import ( ) func TestOwnerTalksToTheAgentThroughTelegram(t *testing.T) { - server := newFakeResponses(t) - server.reply = func(w http.ResponseWriter, call fakeCall) { - writeEvents(w, completedEvent([]any{messageItem("Hello from the agent.")}, 100, 0, 5)) + server := newModelFake(t) + server.reply = func(w http.ResponseWriter, call ModelFakeCall) { + modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Hello from the agent.")}, 100, 0, 5)) } - telegram := newFakeBot(t) - bot, _ := startTestBot(t, telegram) - socket := startTestDaemonWithBot(t, server, bot) - terminal := startTestClient(t, socket) + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + socket := daemonStartTestWithBot(t, server, bot) + terminal := terminalStartTestClient(t, socket) terminal.WaitFor(t, "[connected to OneChat]") - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 1, - Message: ownerMessage("hi from the phone"), + Message: telegramOwnerMessage("hi from the phone"), }) terminal.WaitFor(t, "[telegram] hi from the phone\n[working]\nHello from the agent.\n[done]") - waitForSent(t, telegram, "Hello from the agent.") - require.True(t, slices.ContainsFunc(telegram.Calls(), func(call fakeBotCall) bool { + telegramWaitForSent(t, telegram, "Hello from the agent.") + require.True(t, slices.ContainsFunc(telegram.Calls(), func(call TelegramFakeCall) bool { return call.method == "sendChatAction" && call.params["action"] == "typing" })) terminal.Type("hi from the desk") - require.Equal(t, []string{"Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent."}, waitForSent(t, telegram, "Hello from the agent.", "[terminal] hi from the desk", "Hello from the agent.")) + 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.")) } func TestBotIgnoresEveryoneButTheOwner(t *testing.T) { - telegram := newFakeBot(t) - bot, logs := startTestBot(t, telegram) - socket := startTestDaemonWithBot(t, newFakeResponses(t), bot) - terminal := startTestClient(t, socket) + telegram := newTelegramFake(t) + bot, logs := telegramStartTestBot(t, telegram) + socket := daemonStartTestWithBot(t, newModelFake(t), bot) + terminal := terminalStartTestClient(t, socket) terminal.WaitFor(t, "[connected to OneChat]") - group := ownerMessage("secret in a group") - group.Chat = botChat{ + group := telegramOwnerMessage("secret in a group") + group.Chat = TelegramChat{ ID: -100, Type: "group", } - stranger := ownerMessage("secret of a stranger") + stranger := telegramOwnerMessage("secret of a stranger") stranger.Chat.ID = 77 stranger.From.ID = 77 - unsigned := ownerMessage("secret with no sender") + unsigned := telegramOwnerMessage("secret with no sender") unsigned.From = nil - for id, message := range []*botMessage{group, stranger, unsigned, nil} { - telegram.Add(botUpdate{ + for id, message := range []*TelegramMessage{group, stranger, unsigned, nil} { + telegram.Add(TelegramUpdate{ UpdateID: int64(10 + id), Message: message, }) } require.Eventually(t, func() bool { - offset, err := loadOffset(bot.offsetPath) + offset, err := telegramLoadOffset(bot.offsetPath) return err == nil && offset == 14 }, 10*time.Second, time.Millisecond) @@ -79,133 +79,133 @@ func TestBotIgnoresEveryoneButTheOwner(t *testing.T) { } func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) { - telegram := newFakeBot(t) - bot, _ := startTestBot(t, telegram) - startTestDaemonWithBot(t, newFakeResponses(t), bot) + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + daemonStartTestWithBot(t, newModelFake(t), bot) - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 1, - Message: ownerMessage(""), + Message: telegramOwnerMessage(""), }) - waitForSent(t, telegram, textOnly) + telegramWaitForSent(t, telegram, telegramTextOnly) } func TestStopFromTelegramInterruptsTheTurn(t *testing.T) { - server := newFakeResponses(t) + server := newModelFake(t) arrived := make(chan struct{}) release := make(chan struct{}) t.Cleanup(func() { close(release) }) - server.reply = func(w http.ResponseWriter, call fakeCall) { + server.reply = func(w http.ResponseWriter, call ModelFakeCall) { close(arrived) <-release } - telegram := newFakeBot(t) - bot, _ := startTestBot(t, telegram) - startTestDaemonWithBot(t, server, bot) + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + daemonStartTestWithBot(t, server, bot) - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 1, - Message: ownerMessage("a long task"), + Message: telegramOwnerMessage("a long task"), }) <-arrived - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 2, - Message: ownerMessage(stopCommand), + Message: telegramOwnerMessage(stopCommand), }) - waitForSent(t, telegram, "Error: interrupted") + telegramWaitForSent(t, telegram, "Error: interrupted") - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 3, - Message: ownerMessage(stopCommand), + Message: telegramOwnerMessage(stopCommand), }) - waitForSent(t, telegram, "Error: interrupted", "No turn runs.") + telegramWaitForSent(t, telegram, "Error: interrupted", "No turn runs.") } func TestBotStartsAtTheStoredOffset(t *testing.T) { - server := newFakeResponses(t) - server.reply = func(w http.ResponseWriter, call fakeCall) { - writeEvents(w, completedEvent([]any{messageItem("ok")}, 100, 0, 5)) + server := newModelFake(t) + server.reply = func(w http.ResponseWriter, call ModelFakeCall) { + modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5)) } - telegram := newFakeBot(t) - bot, _ := startTestBot(t, telegram) - require.NoError(t, saveOffset(bot.offsetPath, 10)) + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + require.NoError(t, telegramSaveOffset(bot.offsetPath, 10)) - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 9, - Message: ownerMessage("handled before the restart"), + Message: telegramOwnerMessage("handled before the restart"), }) - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 10, - Message: ownerMessage("new after the restart"), + Message: telegramOwnerMessage("new after the restart"), }) - socket := startTestDaemonWithBot(t, server, bot) - terminal := startTestClient(t, socket) + socket := daemonStartTestWithBot(t, server, bot) + terminal := terminalStartTestClient(t, socket) terminal.WaitFor(t, "[connected to OneChat]") - waitForSent(t, telegram, "ok") + telegramWaitForSent(t, telegram, "ok") require.Equal(t, float64(10), telegram.Calls()[0].params["offset"]) require.NotContains(t, terminal.Output(), "handled before the restart") require.Eventually(t, func() bool { - offset, err := loadOffset(bot.offsetPath) + offset, err := telegramLoadOffset(bot.offsetPath) return err == nil && offset == 11 }, 10*time.Second, time.Millisecond) } func TestBotTriesAgainAfterFailures(t *testing.T) { - server := newFakeResponses(t) - server.reply = func(w http.ResponseWriter, call fakeCall) { - writeEvents(w, completedEvent([]any{messageItem("ok")}, 100, 0, 5)) + server := newModelFake(t) + server.reply = func(w http.ResponseWriter, call ModelFakeCall) { + modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5)) } - telegram := newFakeBot(t) + telegram := newTelegramFake(t) failures := map[string]int{ - "getUpdates": 2, - "sendMessage": 1, + "getUpdates": 2, + "sendRichMessage": 1, } - telegram.fail = func(call fakeBotCall) (int, string) { + telegram.fail = func(call TelegramFakeCall) (int, string) { if failures[call.method] == 0 { return 0, "" } failures[call.method]-- - if call.method == "sendMessage" { + if call.method == "sendRichMessage" { return http.StatusTooManyRequests, "Too Many Requests: retry after 1" } return http.StatusBadGateway, "Bad Gateway" } - bot, logs := startTestBot(t, telegram) - startTestDaemonWithBot(t, server, bot) + bot, logs := telegramStartTestBot(t, telegram) + daemonStartTestWithBot(t, server, bot) - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 1, - Message: ownerMessage("hello"), + Message: telegramOwnerMessage("hello"), }) - waitForSent(t, telegram, "ok") + telegramWaitForSent(t, telegram, "ok") require.Contains(t, logs.String(), "answered 502") require.Contains(t, logs.String(), "answered 429") - require.NotContains(t, logs.String(), testBotToken) + require.NotContains(t, logs.String(), telegramTestToken) } func TestDeliverTriesAgainAfterATimeout(t *testing.T) { - telegram := newFakeBot(t) + telegram := newTelegramFake(t) stalls := 1 - telegram.stall = func(call fakeBotCall) bool { + telegram.stall = func(call TelegramFakeCall) bool { if call.method != "sendMessage" || stalls == 0 { return false } @@ -215,78 +215,299 @@ func TestDeliverTriesAgainAfterATimeout(t *testing.T) { return true } - bot, logs := startTestBot(t, telegram) + bot, logs := telegramStartTestBot(t, telegram) bot.api.sendTimeout = 50 * time.Millisecond - bot.deliver(t.Context(), botPart{ + bot.deliver(t.Context(), TelegramPart{ text: "hello", }) require.Equal(t, []string{"hello"}, telegram.Sent()) require.Len(t, telegram.Calls(), 2) require.Contains(t, logs.String(), "no valid answer") - require.NotContains(t, logs.String(), testBotToken) + require.NotContains(t, logs.String(), telegramTestToken) } -func TestOnlyRepliesGetEntities(t *testing.T) { - server := newFakeResponses(t) - server.reply = func(w http.ResponseWriter, call fakeCall) { - writeEvents(w, completedEvent([]any{messageItem("**Done** with `x`.")}, 100, 0, 5)) +func TestRepliesGoAsRichMessages(t *testing.T) { + reply := "**Done.** The table:\n\n| Name | Size |\n|:-----|-----:|\n| `a.go` | 10 |\n\n- one\n- two\n\n```go\nx := 1\n```" + server := newModelFake(t) + server.reply = func(w http.ResponseWriter, call ModelFakeCall) { + modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(reply)}, 100, 0, 5)) } - telegram := newFakeBot(t) - bot, _ := startTestBot(t, telegram) - socket := startTestDaemonWithBot(t, server, bot) - terminal := startTestClient(t, socket) + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + socket := daemonStartTestWithBot(t, server, bot) + terminal := terminalStartTestClient(t, socket) terminal.WaitFor(t, "[connected to OneChat]") terminal.Type("make **it** so") - waitForSent(t, telegram, "[terminal] make **it** so", "Done with x.") + telegramWaitForSent(t, telegram, "[terminal] make **it** so", reply) - telegram.Add(botUpdate{ + telegram.Add(TelegramUpdate{ UpdateID: 1, - Message: ownerMessage(""), + Message: telegramOwnerMessage(""), }) - waitForSent(t, telegram, textOnly) + telegramWaitForSent(t, telegram, telegramTextOnly) - entities := map[string]any{} + var sends []TelegramFakeCall for _, call := range telegram.Calls() { - if call.method == "sendMessage" { - entities[call.params["text"].(string)] = call.params["entities"] + if call.method == "sendMessage" || call.method == "sendRichMessage" { + sends = append(sends, call) } } - require.Equal(t, map[string]any{ - "[terminal] make **it** so": nil, - "Done with x.": []any{ - map[string]any{ - "type": "bold", - "offset": float64(0), - "length": float64(4), + require.Equal(t, []TelegramFakeCall{ + { + method: "sendMessage", + params: map[string]any{ + "chat_id": float64(telegramTestOwner), + "text": "[terminal] make **it** so", }, - map[string]any{ - "type": "code", - "offset": float64(10), - "length": float64(1), + ok: true, + }, + { + method: "sendRichMessage", + params: map[string]any{ + "chat_id": float64(telegramTestOwner), + "rich_message": map[string]any{ + "markdown": reply, + }, + }, + ok: true, + }, + { + method: "sendMessage", + params: map[string]any{ + "chat_id": float64(telegramTestOwner), + "text": telegramTextOnly, }, + ok: true, }, - textOnly: nil, - }, entities) + }, sends) +} + +func TestRichMessageFailures(t *testing.T) { + testCases := []struct { + name string + status int + always bool + methods []string + sent []string + logText string + }{ + { + name: "bad request sends the reply once with entities", + status: http.StatusBadRequest, + always: true, + methods: []string{"sendRichMessage", "sendMessage"}, + sent: []string{"a secret word"}, + logText: "rich message refused", + }, + { + name: "unknown method sends the reply once with entities", + status: http.StatusNotFound, + always: true, + methods: []string{"sendRichMessage", "sendMessage"}, + sent: []string{"a secret word"}, + logText: "rich message refused", + }, + { + name: "rate limit tries the rich message again", + status: http.StatusTooManyRequests, + methods: []string{"sendRichMessage", "sendRichMessage"}, + sent: []string{"a **secret** word"}, + logText: "answered 429", + }, + { + name: "server error tries the rich message again", + status: http.StatusInternalServerError, + methods: []string{"sendRichMessage", "sendRichMessage"}, + sent: []string{"a **secret** word"}, + logText: "answered 500", + }, + { + name: "other refusal drops the reply", + status: http.StatusForbidden, + always: true, + methods: []string{"sendRichMessage"}, + logText: "message dropped", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + telegram := newTelegramFake(t) + failed := false + telegram.fail = func(call TelegramFakeCall) (int, string) { + if call.method != "sendRichMessage" || failed && !tc.always { + return 0, "" + } + + failed = true + + return tc.status, "refused for bot" + telegramTestToken + } + + bot, logs := telegramStartTestBot(t, telegram) + bot.post(t.Context(), Event{ + Kind: "reply", + Text: "a **secret** word", + }) + + var methods []string + for _, call := range telegram.Calls() { + methods = append(methods, call.method) + } + + require.Equal(t, tc.methods, methods) + require.Equal(t, tc.sent, telegram.Sent()) + require.Equal(t, 1, strings.Count(logs.String(), "\n")) + require.Contains(t, logs.String(), tc.logText) + require.NotContains(t, logs.String(), telegramTestToken) + require.NotContains(t, logs.String(), "secret word") + + calls := telegram.Calls() + if calls[len(calls)-1].method == "sendMessage" { + require.NotNil(t, calls[len(calls)-1].params["entities"]) + } + }) + } +} + +func TestLongReplyGoesAsRichMessagesSplitBetweenBlocks(t *testing.T) { + paragraphs := strings.Repeat("A paragraph of the reply.\n\n", telegramRichBytes/len("A paragraph of the reply.\n\n")) + table := "| a | b |\n|---|---|\n| 1 | 2 |\n" + reply := paragraphs + table + + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + bot.post(t.Context(), Event{ + Kind: "reply", + Text: reply, + }) + + require.Equal(t, []string{paragraphs, table}, telegram.Sent()) +} + +func TestReplyTagsGoAsText(t *testing.T) { + testCases := []struct { + name string + status int + sent []string + }{ + { + name: "rich message escapes the tag outside code", + sent: []string{"a <details> & tag and ``"}, + }, + { + name: "entity fallback shows the tag as written", + status: http.StatusBadRequest, + sent: []string{"a
& tag and "}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + telegram := newTelegramFake(t) + telegram.fail = func(call TelegramFakeCall) (int, string) { + if call.method != "sendRichMessage" { + return 0, "" + } + + return tc.status, "Bad Request" + } + + bot, _ := telegramStartTestBot(t, telegram) + bot.post(t.Context(), Event{ + Kind: "reply", + Text: "a
& tag and ``", + }) + + require.Equal(t, tc.sent, telegram.Sent()) + }) + } +} + +func TestLongReplySplitCountsTheEscapedTags(t *testing.T) { + paragraph := "A tag.\n\n" + count := telegramRichBytes / len(paragraph) + reply := strings.Repeat(paragraph, count) + require.LessOrEqual(t, len(reply), telegramRichBytes) + + telegram := newTelegramFake(t) + bot, _ := telegramStartTestBot(t, telegram) + bot.post(t.Context(), Event{ + Kind: "reply", + Text: reply, + }) + + sent := telegram.Sent() + require.Len(t, sent, 2) + require.Equal(t, telegramEscapeTags(reply), strings.Join(sent, "")) + + for _, markdown := range sent { + require.LessOrEqual(t, len(markdown), telegramRichBytes) + } +} + +func TestRefusedRichPartGoesWithTheTextAsWritten(t *testing.T) { + paragraph := "A & c.\n\n" + reply := strings.Repeat(paragraph, telegramRichBytes/len(paragraph)) + + telegram := newTelegramFake(t) + refused := false + telegram.fail = func(call TelegramFakeCall) (int, string) { + if call.method != "sendRichMessage" || refused { + return 0, "" + } + + refused = true + + return http.StatusBadRequest, "Bad Request" + } + + bot, _ := telegramStartTestBot(t, telegram) + bot.post(t.Context(), Event{ + Kind: "reply", + Text: reply, + }) + + var fallback, rich []string + for _, call := range telegram.Calls() { + switch { + case !call.ok: + case call.method == "sendMessage": + fallback = append(fallback, call.params["text"].(string)) + case call.method == "sendRichMessage": + rich = append(rich, call.params["rich_message"].(map[string]any)["markdown"].(string)) + } + } + + require.Greater(t, len(fallback), 1) + require.Len(t, rich, 1) + require.True(t, strings.HasPrefix(reply, strings.Join(fallback, "")), "fallback text is not the reply as written") + + for _, text := range fallback { + require.LessOrEqual(t, telegramUTF16Units(text), telegramMessageUnits) + } + + require.True(t, reply == strings.Join(fallback, "")+telegramUnescapeTags(rich[0]), "reply is not sent once and whole") } func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) { - telegram := newFakeBot(t) - telegram.fail = func(call fakeBotCall) (int, string) { + telegram := newTelegramFake(t) + telegram.fail = func(call TelegramFakeCall) (int, string) { if call.params["entities"] == nil { return 0, "" } - return http.StatusBadRequest, "Bad Request: can't parse entities of bot" + testBotToken + return http.StatusBadRequest, "Bad Request: can't parse entities of bot" + telegramTestToken } - bot, logs := startTestBot(t, telegram) - text, entities := markdownEntities("a **secret** word") - bot.deliver(t.Context(), botPart{ + bot, logs := telegramStartTestBot(t, telegram) + text, entities := telegramMarkdownEntities("a **secret** word") + bot.deliver(t.Context(), TelegramPart{ text: text, entities: entities, }) @@ -298,21 +519,21 @@ func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) { require.Equal(t, []string{"a secret word"}, telegram.Sent()) require.Equal(t, 1, strings.Count(logs.String(), "\n")) require.Contains(t, logs.String(), "entities refused") - require.NotContains(t, logs.String(), testBotToken) + require.NotContains(t, logs.String(), telegramTestToken) require.NotContains(t, logs.String(), "secret word") } func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) { - telegram := newFakeBot(t) + telegram := newTelegramFake(t) telegram.Close() - bot, logs := startTestBot(t, telegram) - startTestDaemonWithBot(t, newFakeResponses(t), bot) + bot, logs := telegramStartTestBot(t, telegram) + daemonStartTestWithBot(t, newModelFake(t), bot) require.Eventually(t, func() bool { return strings.Count(logs.String(), "no valid answer") >= 2 }, 10*time.Second, time.Millisecond) - require.NotContains(t, logs.String(), testBotToken) + require.NotContains(t, logs.String(), telegramTestToken) } func TestSplitMessage(t *testing.T) { @@ -356,18 +577,18 @@ func TestSplitMessage(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.parts, splitMessage(tc.text, tc.limit)) + require.Equal(t, tc.parts, telegramSplitMessage(tc.text, tc.limit)) }) } long := strings.Repeat("word ", 2000) - parts := splitMessage(long, messageUnits) + parts := telegramSplitMessage(long, telegramMessageUnits) require.Len(t, parts, 3) require.Equal(t, long, strings.Join(parts, "")) } func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) { - parts := splitEntities("aabbbbcc", []botEntity{ + parts := telegramSplitEntities("aabbbbcc", []TelegramEntity{ { Type: "bold", Offset: 2, @@ -380,10 +601,10 @@ func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) { }, }, 5) - require.Equal(t, []botPart{ + require.Equal(t, []TelegramPart{ { text: "aabbb", - entities: []botEntity{ + entities: []TelegramEntity{ { Type: "bold", Offset: 2, @@ -393,7 +614,7 @@ func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) { }, { text: "bcc", - entities: []botEntity{ + entities: []TelegramEntity{ { Type: "bold", Offset: 0, @@ -409,6 +630,70 @@ func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) { }, parts) } +func TestSplitRich(t *testing.T) { + testCases := []struct { + name string + markdown string + limit int + parts []string + }{ + { + name: "short reply stays whole", + markdown: "| a | b |", + limit: 100, + parts: []string{"| a | b |"}, + }, + { + name: "cut after a blank line keeps a table whole", + markdown: "Intro.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n", + limit: 30, + parts: []string{"Intro.\n\n", "| a | b |\n|---|---|\n| 1 | 2 |\n"}, + }, + { + name: "blank line in a code block is not a cut", + markdown: "Text.\n\n```go\nx\n\ny\n```\n\nEnd.\n", + limit: 20, + parts: []string{"Text.\n\n", "```go\nx\n\ny\n```\n\n", "End.\n"}, + }, + { + name: "paragraph with no blank line is cut after a line", + markdown: "aaa\nbbb\nccc\n", + limit: 9, + parts: []string{"aaa\nbbb\n", "ccc\n"}, + }, + { + name: "long code block is closed and opened again", + markdown: "```sh\none\ntwo\nthree\n```\n", + limit: 16, + parts: []string{"```sh\none\n```\n", "```sh\ntwo\n```\n", "```sh\nthree\n```\n"}, + }, + { + name: "long line is cut at the limit and not inside a character", + markdown: "ab\u00e9cd", + limit: 3, + parts: []string{"ab", "\u00e9c", "d"}, + }, + { + name: "long line is not cut inside an entity", + markdown: "abc&def", + limit: 6, + parts: []string{"abc", "&d", "ef"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.parts, telegramSplitRich(tc.markdown, tc.limit)) + }) + } + + long := strings.Repeat("A paragraph of the reply.\n\n", 2000) + parts := telegramSplitRich(long, telegramRichBytes) + require.Len(t, parts, 2) + require.Len(t, parts[0], telegramRichBytes-telegramRichBytes%len("A paragraph of the reply.\n\n")) + require.Equal(t, long, strings.Join(parts, "")) +} + func TestLoadBotConfig(t *testing.T) { testCases := []struct { name string @@ -418,12 +703,12 @@ func TestLoadBotConfig(t *testing.T) { }{ { name: "owner file is read", - content: `{"token": "` + testBotToken + `", "owner": 4242}`, + content: `{"token": "` + telegramTestToken + `", "owner": 4242}`, mode: 0o600, }, { name: "file that others can read is refused", - content: `{"token": "` + testBotToken + `", "owner": 4242}`, + content: `{"token": "` + telegramTestToken + `", "owner": 4242}`, mode: 0o640, errorText: "mode must be 0600", }, @@ -435,13 +720,13 @@ func TestLoadBotConfig(t *testing.T) { }, { name: "missing owner is refused", - content: `{"token": "` + testBotToken + `"}`, + content: `{"token": "` + telegramTestToken + `"}`, mode: 0o600, errorText: "numeric Telegram user ID", }, { name: "unknown field is refused", - content: `{"token": "` + testBotToken + `", "owner": 4242, "chat": 1}`, + content: `{"token": "` + telegramTestToken + `", "owner": 4242, "chat": 1}`, mode: 0o600, errorText: "not valid JSON", }, @@ -450,47 +735,49 @@ func TestLoadBotConfig(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, botConfigFileName) + path := filepath.Join(dir, telegramConfigFileName) require.NoError(t, os.WriteFile(path, []byte(tc.content), tc.mode)) require.NoError(t, os.Chmod(path, tc.mode)) - config, found, err := loadBotConfig(dir) + config, found, err := loadTelegramConfig(dir) if tc.errorText != "" { require.ErrorContains(t, err, tc.errorText) - require.NotContains(t, err.Error(), testBotToken) + require.NotContains(t, err.Error(), telegramTestToken) return } require.NoError(t, err) require.True(t, found) - require.Equal(t, botConfig{ - Token: testBotToken, - Owner: testOwner, + require.Equal(t, TelegramConfig{ + Token: telegramTestToken, + Owner: telegramTestOwner, }, config) }) } - _, found, err := loadBotConfig(t.TempDir()) + _, found, err := loadTelegramConfig(t.TempDir()) require.NoError(t, err) require.False(t, found) } -// startTestBot returns a bot of testOwner on server, with its offset file in a -// temporary directory and short waits, and the buffer of its log. -func startTestBot(t *testing.T, server *fakeBot) (*telegramBot, *syncBuffer) { +// telegramStartTestBot returns a bot of telegramTestOwner on server, with its +// offset file in a temporary directory and short waits, and the buffer of its +// log. +func telegramStartTestBot(t *testing.T, server *TelegramFake) (*TelegramBot, *SyncBuffer) { t.Helper() - logs := &syncBuffer{} - bot := newTelegramBot(newBotAPI(server.URL, testBotToken), testOwner, filepath.Join(t.TempDir(), offsetFileName), log.New(logs, "", 0)) + logs := &SyncBuffer{} + bot := newTelegramBot(newTelegramAPI(server.URL, telegramTestToken), telegramTestOwner, filepath.Join(t.TempDir(), telegramOffsetFileName), log.New(logs, "", 0)) bot.minWait = time.Millisecond bot.maxWait = 10 * time.Millisecond return bot, logs } -// waitForSent waits until the bot sent the texts in order, and returns them. -func waitForSent(t *testing.T, server *fakeBot, texts ...string) []string { +// telegramWaitForSent waits until the bot sent the texts in order, and returns +// them. +func telegramWaitForSent(t *testing.T, server *TelegramFake, texts ...string) []string { t.Helper() require.Eventually(t, func() bool {