]> git.giacomolli.uk - onechat/blob - telegram_api_test.go
f789101a2bf6a86e15ac226258ef60900fb4c80c
[onechat] / telegram_api_test.go
1 package main
2
3 import (
4 "context"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "time"
10
11 "github.com/stretchr/testify/require"
12 )
13
14 func TestGetUpdatesAsksForMessagesFromTheOffset(t *testing.T) {
15 server := newTelegramFake(t)
16 server.Add(TelegramUpdate{
17 UpdateID: 7,
18 Message: telegramOwnerMessage("hello"),
19 })
20
21 updates, err := newTelegramAPI(server.URL, telegramTestToken).GetUpdates(t.Context(), 7)
22 require.NoError(t, err)
23 require.Len(t, updates, 1)
24 require.Equal(t, int64(7), updates[0].UpdateID)
25 require.Equal(t, "hello", updates[0].Message.Text)
26
27 call := server.Calls()[0]
28 require.Equal(t, "getUpdates", call.method)
29 require.Equal(t, map[string]any{
30 "offset": float64(7),
31 "timeout": float64(telegramPollSeconds),
32 "allowed_updates": []any{"message"},
33 }, call.params)
34 }
35
36 func TestSendMessageSendsPlainText(t *testing.T) {
37 server := newTelegramFake(t)
38 err := newTelegramAPI(server.URL, telegramTestToken).SendMessage(t.Context(), telegramTestOwner, "*not bold*", nil)
39 require.NoError(t, err)
40
41 require.Equal(t, map[string]any{
42 "chat_id": float64(telegramTestOwner),
43 "text": "*not bold*",
44 }, server.Calls()[0].params)
45 }
46
47 func TestSendMessageSendsTheEntities(t *testing.T) {
48 server := newTelegramFake(t)
49 err := newTelegramAPI(server.URL, telegramTestToken).SendMessage(t.Context(), telegramTestOwner, "a link", []TelegramEntity{
50 {
51 Type: "text_link",
52 Offset: 2,
53 Length: 4,
54 URL: "https://example.com",
55 },
56 })
57 require.NoError(t, err)
58
59 require.Equal(t, []any{
60 map[string]any{
61 "type": "text_link",
62 "offset": float64(2),
63 "length": float64(4),
64 "url": "https://example.com",
65 },
66 }, server.Calls()[0].params["entities"])
67 }
68
69 func TestSendRichMessageSendsTheMarkdownUnchanged(t *testing.T) {
70 server := newTelegramFake(t)
71 markdown := "| a | b |\n|---|---|\n| **1** | `2` |"
72 err := newTelegramAPI(server.URL, telegramTestToken).SendRichMessage(t.Context(), telegramTestOwner, markdown)
73 require.NoError(t, err)
74
75 call := server.Calls()[0]
76 require.Equal(t, "sendRichMessage", call.method)
77 require.Equal(t, map[string]any{
78 "chat_id": float64(telegramTestOwner),
79 "rich_message": map[string]any{
80 "markdown": markdown,
81 },
82 }, call.params)
83 }
84
85 func TestSendCallsTimeOutOnAStalledConnection(t *testing.T) {
86 testCases := []struct {
87 method string
88 send func(ctx context.Context, api *TelegramAPI) error
89 }{
90 {
91 method: "sendMessage",
92 send: func(ctx context.Context, api *TelegramAPI) error {
93 return api.SendMessage(ctx, telegramTestOwner, "hi", nil)
94 },
95 },
96 {
97 method: "sendRichMessage",
98 send: func(ctx context.Context, api *TelegramAPI) error {
99 return api.SendRichMessage(ctx, telegramTestOwner, "hi")
100 },
101 },
102 }
103
104 for _, tc := range testCases {
105 t.Run(tc.method, func(t *testing.T) {
106 server := newTelegramFake(t)
107 server.stall = func(call TelegramFakeCall) bool {
108 return call.method == tc.method
109 }
110
111 api := newTelegramAPI(server.URL, telegramTestToken)
112 api.sendTimeout = 50 * time.Millisecond
113 start := time.Now()
114 err := tc.send(t.Context(), api)
115 require.EqualError(t, err, "no answer in 50ms")
116 require.Less(t, time.Since(start), 5*time.Second)
117 require.NotContains(t, err.Error(), telegramTestToken)
118 require.NotContains(t, err.Error(), server.URL)
119
120 retry, _ := telegramRetryable(err)
121 require.True(t, retry)
122
123 api.sendTimeout = time.Hour
124 ctx, cancel := context.WithCancel(t.Context())
125 time.AfterFunc(50*time.Millisecond, cancel)
126 start = time.Now()
127 err = tc.send(ctx, api)
128 require.Error(t, err)
129 require.Less(t, time.Since(start), 5*time.Second)
130 })
131 }
132 }
133
134 func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
135 testCases := []struct {
136 name string
137 reply func(w http.ResponseWriter)
138 retry bool
139 wait time.Duration
140 rejected bool
141 errorText string
142 }{
143 {
144 name: "refused connection is retried",
145 reply: func(w http.ResponseWriter) {
146 conn, _, err := http.NewResponseController(w).Hijack()
147 if err == nil {
148 _ = conn.Close()
149 }
150 },
151 retry: true,
152 errorText: "no answer",
153 },
154 {
155 name: "rate limit waits as asked",
156 reply: func(w http.ResponseWriter) {
157 w.WriteHeader(http.StatusTooManyRequests)
158 _, _ = io.WriteString(w, `{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 3","parameters":{"retry_after":3}}`)
159 },
160 retry: true,
161 wait: 3 * time.Second,
162 errorText: "answered 429",
163 },
164 {
165 name: "server error page is retried",
166 reply: func(w http.ResponseWriter) {
167 w.WriteHeader(http.StatusBadGateway)
168 _, _ = io.WriteString(w, "<html>bad gateway</html>")
169 },
170 retry: true,
171 errorText: "Telegram answered 502 Bad Gateway",
172 },
173 {
174 name: "token echoed in a description is hidden",
175 reply: func(w http.ResponseWriter) {
176 w.WriteHeader(http.StatusUnauthorized)
177 _, _ = io.WriteString(w, `{"ok":false,"error_code":401,"description":"Unauthorized: bot`+telegramTestToken+`"}`)
178 },
179 retry: false,
180 errorText: "Unauthorized: botTOKEN",
181 },
182 {
183 name: "bad request is a refusal",
184 reply: func(w http.ResponseWriter) {
185 w.WriteHeader(http.StatusBadRequest)
186 _, _ = io.WriteString(w, `{"ok":false,"error_code":400,"description":"Bad Request: can't parse"}`)
187 },
188 retry: false,
189 rejected: true,
190 errorText: "answered 400 Bad Request: can't parse",
191 },
192 {
193 name: "unknown method is a refusal",
194 reply: func(w http.ResponseWriter) {
195 w.WriteHeader(http.StatusNotFound)
196 _, _ = io.WriteString(w, `{"ok":false,"error_code":404,"description":"Not Found"}`)
197 },
198 retry: false,
199 rejected: true,
200 errorText: "answered 404 Not Found",
201 },
202 {
203 name: "not found page of a proxy is not a refusal",
204 reply: func(w http.ResponseWriter) {
205 w.WriteHeader(http.StatusNotFound)
206 _, _ = io.WriteString(w, "<html>not found</html>")
207 },
208 retry: false,
209 errorText: "answered 404 Not Found",
210 },
211 {
212 name: "broken answer is retried",
213 reply: func(w http.ResponseWriter) {
214 _, _ = io.WriteString(w, `{"ok":`)
215 },
216 retry: true,
217 errorText: "not valid JSON",
218 },
219 }
220
221 for _, tc := range testCases {
222 t.Run(tc.name, func(t *testing.T) {
223 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
224 tc.reply(w)
225 }))
226 t.Cleanup(server.Close)
227
228 err := newTelegramAPI(server.URL, telegramTestToken).SendMessage(t.Context(), telegramTestOwner, "hi", nil)
229 require.ErrorContains(t, err, tc.errorText)
230 require.NotContains(t, err.Error(), telegramTestToken)
231 require.NotContains(t, err.Error(), server.URL)
232
233 retry, wait := telegramRetryable(err)
234 require.Equal(t, tc.retry, retry)
235 require.Equal(t, tc.wait, wait)
236 require.Equal(t, tc.rejected, telegramRejected(err))
237 })
238 }
239
240 server := httptest.NewServer(http.NotFoundHandler())
241 server.Close()
242
243 _, err := newTelegramAPI(server.URL, telegramTestToken).GetUpdates(t.Context(), 0)
244 require.ErrorAs(t, err, new(*TelegramNetworkError))
245 require.NotContains(t, err.Error(), telegramTestToken)
246 }