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