]> git.giacomolli.uk - onechat/blob - telegram_fake_helper_test.go
Send terminal images by local path
[onechat] / telegram_fake_helper_test.go
1 package main
2
3 import (
4         "encoding/json"
5         "fmt"
6         "io"
7         "net/http"
8         "net/http/httptest"
9         "strings"
10         "sync"
11         "testing"
12         "time"
13 )
14
15 const (
16         // telegramTestToken is the bot token of the tests. It has the form of a
17         // real token.
18         telegramTestToken = "123456:TEST-secret_token"
19
20         // telegramTestOwner is the user ID of the owner of the test bot.
21         telegramTestOwner = 4242
22 )
23
24 // TelegramFakeCall is one call of the fake Bot API. OK tells if the call got a
25 // success answer.
26 type TelegramFakeCall struct {
27         method string
28         params map[string]any
29         ok     bool
30 }
31
32 // TelegramFake is a fake Bot API server for telegramTestToken. getUpdates
33 // returns the added updates from the offset on, or waits a short time when
34 // there are none. The other methods succeed, unless fail gives an error answer
35 // for a call. An answer with HTTP 429 asks for a wait of one second. When stall
36 // is true for a call, the call gets no answer until the client gives it up or
37 // the test ends. Fail and stall run with the mutex held.
38 type TelegramFake struct {
39         *httptest.Server
40
41         mutex   sync.Mutex
42         updates []TelegramUpdate
43         calls   []TelegramFakeCall
44         fail    func(call TelegramFakeCall) (int, string)
45         stall   func(call TelegramFakeCall) bool
46         added   chan struct{}
47         stopped chan struct{}
48         images  map[string][]byte
49 }
50
51 // Add makes update available to getUpdates.
52 func (f *TelegramFake) Add(update TelegramUpdate) {
53         f.mutex.Lock()
54
55         f.updates = append(f.updates, update)
56         f.mutex.Unlock()
57
58         select {
59         case f.added <- struct{}{}:
60         default:
61         }
62 }
63
64 // Calls returns the calls so far.
65 func (f *TelegramFake) Calls() []TelegramFakeCall {
66         f.mutex.Lock()
67         defer f.mutex.Unlock()
68
69         return append([]TelegramFakeCall(nil), f.calls...)
70 }
71
72 // Sent returns the texts of the sendMessage calls and the Markdown of the
73 // sendRichMessage calls that succeeded, in order.
74 func (f *TelegramFake) Sent() []string {
75         var texts []string
76
77         for _, call := range f.Calls() {
78                 switch {
79                 case !call.ok:
80                 case call.method == "sendMessage":
81                         texts = append(texts, call.params["text"].(string))
82                 case call.method == "sendRichMessage":
83                         texts = append(texts, call.params["rich_message"].(map[string]any)["markdown"].(string))
84                 }
85         }
86
87         return texts
88 }
89
90 func (f *TelegramFake) Image(id string, data []byte) {
91         f.mutex.Lock()
92         defer f.mutex.Unlock()
93
94         if f.images == nil {
95                 f.images = make(map[string][]byte)
96         }
97
98         f.images[id] = data
99 }
100
101 func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) {
102         id, file := strings.CutPrefix(r.URL.Path, "/file/bot"+telegramTestToken+"/")
103         if file {
104                 f.mutex.Lock()
105
106                 data, exists := f.images[id]
107                 f.mutex.Unlock()
108
109                 if !exists {
110                         http.NotFound(w, r)
111
112                         return
113                 }
114
115                 _, _ = w.Write(data)
116
117                 return
118         }
119
120         method, found := strings.CutPrefix(r.URL.Path, "/bot"+telegramTestToken+"/")
121         if !found {
122                 http.Error(w, `{"ok":false,"error_code":404,"description":"Not Found"}`, http.StatusNotFound)
123
124                 return
125         }
126
127         var params map[string]any
128
129         err := json.NewDecoder(r.Body).Decode(&params)
130         if err != nil {
131                 http.Error(w, err.Error(), http.StatusBadRequest)
132
133                 return
134         }
135
136         call := TelegramFakeCall{
137                 method: method,
138                 params: params,
139         }
140
141         f.mutex.Lock()
142
143         status, description := 0, ""
144         if f.fail != nil {
145                 status, description = f.fail(call)
146         }
147
148         stall := f.stall != nil && f.stall(call)
149         f.mutex.Unlock()
150
151         if stall {
152                 f.record(call)
153
154                 select {
155                 case <-r.Context().Done():
156                 case <-f.stopped:
157                 }
158
159                 return
160         }
161
162         if status != 0 {
163                 f.record(call)
164                 w.WriteHeader(status)
165
166                 wait := 0
167                 if status == http.StatusTooManyRequests {
168                         wait = 1
169                 }
170
171                 _, _ = fmt.Fprintf(w, `{"ok":false,"error_code":%d,"description":%q,"parameters":{"retry_after":%d}}`, status, description, wait)
172
173                 return
174         }
175
176         call.ok = true
177         f.record(call)
178
179         if method == "getFile" {
180                 _ = json.NewEncoder(w).Encode(map[string]any{
181                         "ok":     true,
182                         "result": map[string]any{"file_path": params["file_id"]},
183                 })
184
185                 return
186         }
187
188         if method != "getUpdates" {
189                 _, _ = io.WriteString(w, `{"ok":true,"result":true}`)
190                 return
191         }
192
193         offset := int64(params["offset"].(float64))
194         updates := f.from(offset)
195         if len(updates) == 0 {
196                 select {
197                 case <-f.added:
198                 case <-time.After(50 * time.Millisecond):
199                 case <-r.Context().Done():
200                 }
201
202                 updates = f.from(offset)
203         }
204
205         _ = json.NewEncoder(w).Encode(map[string]any{
206                 "ok":     true,
207                 "result": updates,
208         })
209 }
210
211 // record keeps call.
212 func (f *TelegramFake) record(call TelegramFakeCall) {
213         f.mutex.Lock()
214         defer f.mutex.Unlock()
215
216         f.calls = append(f.calls, call)
217 }
218
219 // from returns the updates from offset on, and forgets the ones before it, as
220 // Telegram does.
221 func (f *TelegramFake) from(offset int64) []TelegramUpdate {
222         f.mutex.Lock()
223         defer f.mutex.Unlock()
224
225         var updates []TelegramUpdate
226
227         kept := f.updates[:0]
228         for _, update := range f.updates {
229                 if update.UpdateID >= offset {
230                         updates = append(updates, update)
231                         kept = append(kept, update)
232                 }
233         }
234
235         f.updates = kept
236
237         return updates
238 }
239
240 // newTelegramFake starts a fake Bot API server that stops at the end of the
241 // test.
242 func newTelegramFake(t *testing.T) *TelegramFake {
243         t.Helper()
244
245         f := &TelegramFake{
246                 added:   make(chan struct{}, 1),
247                 stopped: make(chan struct{}),
248         }
249
250         f.Server = httptest.NewServer(http.HandlerFunc(f.serve))
251         t.Cleanup(f.Close)
252         t.Cleanup(func() {
253                 close(f.stopped)
254         })
255
256         return f
257 }
258
259 // telegramOwnerMessage returns a text message of the owner in the private chat.
260 func telegramOwnerMessage(text string) *TelegramMessage {
261         return &TelegramMessage{
262                 Chat: TelegramChat{
263                         ID:   telegramTestOwner,
264                         Type: "private",
265                 },
266                 From: &TelegramUser{
267                         ID: telegramTestOwner,
268                 },
269                 Text: text,
270         }
271 }