]> git.giacomolli.uk - onechat/blob - telegram_api_test.go
Write private data archives with safe extraction
[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
127                         start = time.Now()
128                         err = tc.send(ctx, api)
129                         require.Error(t, err)
130                         require.Less(t, time.Since(start), 5*time.Second)
131                 })
132         }
133 }
134
135 func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
136         testCases := []struct {
137                 name      string
138                 reply     func(w http.ResponseWriter)
139                 retry     bool
140                 wait      time.Duration
141                 rejected  bool
142                 errorText string
143         }{
144                 {
145                         name: "refused connection is retried",
146                         reply: func(w http.ResponseWriter) {
147                                 conn, _, err := http.NewResponseController(w).Hijack()
148                                 if err == nil {
149                                         _ = conn.Close()
150                                 }
151                         },
152                         retry:     true,
153                         errorText: "no answer",
154                 },
155                 {
156                         name: "rate limit waits as asked",
157                         reply: func(w http.ResponseWriter) {
158                                 w.WriteHeader(http.StatusTooManyRequests)
159
160                                 _, _ = io.WriteString(w, `{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 3","parameters":{"retry_after":3}}`)
161                         },
162                         retry:     true,
163                         wait:      3 * time.Second,
164                         errorText: "429",
165                 },
166                 {
167                         name: "server error page is retried",
168                         reply: func(w http.ResponseWriter) {
169                                 w.WriteHeader(http.StatusBadGateway)
170
171                                 _, _ = io.WriteString(w, "<html>bad gateway</html>")
172                         },
173                         retry:     true,
174                         errorText: "502 Bad Gateway",
175                 },
176                 {
177                         name: "token echoed in a description is hidden",
178                         reply: func(w http.ResponseWriter) {
179                                 w.WriteHeader(http.StatusUnauthorized)
180
181                                 _, _ = io.WriteString(w, `{"ok":false,"error_code":401,"description":"Unauthorized: bot`+telegramTestToken+`"}`)
182                         },
183                         retry:     false,
184                         errorText: "Unauthorized: botTOKEN",
185                 },
186                 {
187                         name: "bad request is a refusal",
188                         reply: func(w http.ResponseWriter) {
189                                 w.WriteHeader(http.StatusBadRequest)
190
191                                 _, _ = io.WriteString(w, `{"ok":false,"error_code":400,"description":"Bad Request: can't parse"}`)
192                         },
193                         retry:     false,
194                         rejected:  true,
195                         errorText: "400 Bad Request: can't parse",
196                 },
197                 {
198                         name: "unknown method is a refusal",
199                         reply: func(w http.ResponseWriter) {
200                                 w.WriteHeader(http.StatusNotFound)
201
202                                 _, _ = io.WriteString(w, `{"ok":false,"error_code":404,"description":"Not Found"}`)
203                         },
204                         retry:     false,
205                         rejected:  true,
206                         errorText: "404 Not Found",
207                 },
208                 {
209                         name: "not found page of a proxy is not a refusal",
210                         reply: func(w http.ResponseWriter) {
211                                 w.WriteHeader(http.StatusNotFound)
212
213                                 _, _ = io.WriteString(w, "<html>not found</html>")
214                         },
215                         retry:     false,
216                         errorText: "404 Not Found",
217                 },
218                 {
219                         name: "broken answer is retried",
220                         reply: func(w http.ResponseWriter) {
221                                 _, _ = io.WriteString(w, `{"ok":`)
222                         },
223                         retry:     true,
224                         errorText: "invalid JSON",
225                 },
226         }
227
228         for _, tc := range testCases {
229                 t.Run(tc.name, func(t *testing.T) {
230                         server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
231                                 tc.reply(w)
232                         }))
233                         t.Cleanup(server.Close)
234
235                         err := newTelegramAPI(server.URL, telegramTestToken).SendMessage(t.Context(), telegramTestOwner, "hi", nil)
236                         require.ErrorContains(t, err, tc.errorText)
237                         require.NotContains(t, err.Error(), telegramTestToken)
238                         require.NotContains(t, err.Error(), server.URL)
239
240                         retry, wait := telegramRetryable(err)
241                         require.Equal(t, tc.retry, retry)
242                         require.Equal(t, tc.wait, wait)
243                         require.Equal(t, tc.rejected, telegramRejected(err))
244                 })
245         }
246
247         server := httptest.NewServer(http.NotFoundHandler())
248         server.Close()
249
250         _, err := newTelegramAPI(server.URL, telegramTestToken).GetUpdates(t.Context(), 0)
251         require.ErrorAs(t, err, new(*TelegramNetworkError))
252         require.NotContains(t, err.Error(), telegramTestToken)
253 }