X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/4b063ca25798c78cc03d1d07f67b02c94c95248d..97729fc59a2fbd73e3f8a424cae4c825f5ba885b:/telegram_test.go?ds=inline diff --git a/telegram_test.go b/telegram_test.go index d18e794..30b2fa1 100644 --- a/telegram_test.go +++ b/telegram_test.go @@ -202,6 +202,106 @@ func TestBotTriesAgainAfterFailures(t *testing.T) { require.NotContains(t, logs.String(), testBotToken) } +func TestDeliverTriesAgainAfterATimeout(t *testing.T) { + telegram := newFakeBot(t) + stalls := 1 + telegram.stall = func(call fakeBotCall) bool { + if call.method != "sendMessage" || stalls == 0 { + return false + } + + stalls-- + + return true + } + + bot, logs := startTestBot(t, telegram) + bot.api.sendTimeout = 50 * time.Millisecond + bot.deliver(t.Context(), botPart{ + 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) +} + +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)) + } + + telegram := newFakeBot(t) + bot, _ := startTestBot(t, telegram) + socket := startTestDaemonWithBot(t, server, bot) + terminal := startTestClient(t, socket) + terminal.WaitFor(t, "[connected to OneChat]") + + terminal.Type("make **it** so") + waitForSent(t, telegram, "[terminal] make **it** so", "Done with x.") + + telegram.Add(botUpdate{ + UpdateID: 1, + Message: ownerMessage(""), + }) + + waitForSent(t, telegram, textOnly) + + entities := map[string]any{} + for _, call := range telegram.Calls() { + if call.method == "sendMessage" { + entities[call.params["text"].(string)] = call.params["entities"] + } + } + + 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), + }, + map[string]any{ + "type": "code", + "offset": float64(10), + "length": float64(1), + }, + }, + textOnly: nil, + }, entities) +} + +func TestRefusedEntitiesGoAgainAsPlainText(t *testing.T) { + telegram := newFakeBot(t) + telegram.fail = func(call fakeBotCall) (int, string) { + if call.params["entities"] == nil { + return 0, "" + } + + return http.StatusBadRequest, "Bad Request: can't parse entities of bot" + testBotToken + } + + bot, logs := startTestBot(t, telegram) + text, entities := markdownEntities("a **secret** word") + bot.deliver(t.Context(), botPart{ + text: text, + entities: entities, + }) + + calls := telegram.Calls() + require.Len(t, calls, 2) + require.NotNil(t, calls[0].params["entities"]) + require.Nil(t, calls[1].params["entities"]) + 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(), "secret word") +} + func TestBotLogsNoTokenWhenTheNetworkFails(t *testing.T) { telegram := newFakeBot(t) telegram.Close() @@ -266,6 +366,107 @@ func TestSplitMessage(t *testing.T) { require.Equal(t, long, strings.Join(parts, "")) } +func TestSplitEntitiesCutsAnEntityAcrossASplit(t *testing.T) { + parts := splitEntities("aabbbbcc", []botEntity{ + { + Type: "bold", + Offset: 2, + Length: 4, + }, + { + Type: "italic", + Offset: 6, + Length: 2, + }, + }, 5) + + require.Equal(t, []botPart{ + { + text: "aabbb", + entities: []botEntity{ + { + Type: "bold", + Offset: 2, + Length: 3, + }, + }, + }, + { + text: "bcc", + entities: []botEntity{ + { + Type: "bold", + Offset: 0, + Length: 1, + }, + { + Type: "italic", + Offset: 1, + Length: 2, + }, + }, + }, + }, 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"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.parts, splitRich(tc.markdown, tc.limit)) + }) + } + + long := strings.Repeat("A paragraph of the reply.\n\n", 2000) + parts := splitRich(long, richMessageBytes) + require.Len(t, parts, 2) + require.Len(t, parts[0], richMessageBytes-richMessageBytes%len("A paragraph of the reply.\n\n")) + require.Equal(t, long, strings.Join(parts, "")) +} + func TestLoadBotConfig(t *testing.T) { testCases := []struct { name string