]> git.giacomolli.uk - onechat/blob - telegram_fake_helper_test.go
Send the compaction view in block messages
[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 }
49
50 // Add makes update available to getUpdates.
51 func (f *TelegramFake) Add(update TelegramUpdate) {
52         f.mutex.Lock()
53         f.updates = append(f.updates, update)
54         f.mutex.Unlock()
55
56         select {
57         case f.added <- struct{}{}:
58         default:
59         }
60 }
61
62 // Calls returns the calls so far.
63 func (f *TelegramFake) Calls() []TelegramFakeCall {
64         f.mutex.Lock()
65         defer f.mutex.Unlock()
66
67         return append([]TelegramFakeCall(nil), f.calls...)
68 }
69
70 // Sent returns the texts of the sendMessage calls and the Markdown of the
71 // sendRichMessage calls that succeeded, in order.
72 func (f *TelegramFake) Sent() []string {
73         var texts []string
74         for _, call := range f.Calls() {
75                 switch {
76                 case !call.ok:
77                 case call.method == "sendMessage":
78                         texts = append(texts, call.params["text"].(string))
79                 case call.method == "sendRichMessage":
80                         texts = append(texts, call.params["rich_message"].(map[string]any)["markdown"].(string))
81                 }
82         }
83
84         return texts
85 }
86
87 func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) {
88         method, found := strings.CutPrefix(r.URL.Path, "/bot"+telegramTestToken+"/")
89         if !found {
90                 http.Error(w, `{"ok":false,"error_code":404,"description":"Not Found"}`, http.StatusNotFound)
91                 return
92         }
93
94         var params map[string]any
95         err := json.NewDecoder(r.Body).Decode(&params)
96         if err != nil {
97                 http.Error(w, err.Error(), http.StatusBadRequest)
98                 return
99         }
100
101         call := TelegramFakeCall{
102                 method: method,
103                 params: params,
104         }
105
106         f.mutex.Lock()
107         status, description := 0, ""
108         if f.fail != nil {
109                 status, description = f.fail(call)
110         }
111
112         stall := f.stall != nil && f.stall(call)
113         f.mutex.Unlock()
114
115         if stall {
116                 f.record(call)
117                 select {
118                 case <-r.Context().Done():
119                 case <-f.stopped:
120                 }
121
122                 return
123         }
124
125         if status != 0 {
126                 f.record(call)
127                 w.WriteHeader(status)
128                 wait := 0
129                 if status == http.StatusTooManyRequests {
130                         wait = 1
131                 }
132
133                 _, _ = fmt.Fprintf(w, `{"ok":false,"error_code":%d,"description":%q,"parameters":{"retry_after":%d}}`, status, description, wait)
134
135                 return
136         }
137
138         call.ok = true
139         f.record(call)
140         if method != "getUpdates" {
141                 _, _ = io.WriteString(w, `{"ok":true,"result":true}`)
142                 return
143         }
144
145         offset := int64(params["offset"].(float64))
146         updates := f.from(offset)
147         if len(updates) == 0 {
148                 select {
149                 case <-f.added:
150                 case <-time.After(50 * time.Millisecond):
151                 case <-r.Context().Done():
152                 }
153
154                 updates = f.from(offset)
155         }
156
157         _ = json.NewEncoder(w).Encode(map[string]any{
158                 "ok":     true,
159                 "result": updates,
160         })
161 }
162
163 // record keeps call.
164 func (f *TelegramFake) record(call TelegramFakeCall) {
165         f.mutex.Lock()
166         defer f.mutex.Unlock()
167
168         f.calls = append(f.calls, call)
169 }
170
171 // from returns the updates from offset on, and forgets the ones before it, as
172 // Telegram does.
173 func (f *TelegramFake) from(offset int64) []TelegramUpdate {
174         f.mutex.Lock()
175         defer f.mutex.Unlock()
176
177         var updates []TelegramUpdate
178         kept := f.updates[:0]
179         for _, update := range f.updates {
180                 if update.UpdateID >= offset {
181                         updates = append(updates, update)
182                         kept = append(kept, update)
183                 }
184         }
185
186         f.updates = kept
187
188         return updates
189 }
190
191 // newTelegramFake starts a fake Bot API server that stops at the end of the
192 // test.
193 func newTelegramFake(t *testing.T) *TelegramFake {
194         t.Helper()
195
196         f := &TelegramFake{
197                 added:   make(chan struct{}, 1),
198                 stopped: make(chan struct{}),
199         }
200
201         f.Server = httptest.NewServer(http.HandlerFunc(f.serve))
202         t.Cleanup(f.Close)
203         t.Cleanup(func() {
204                 close(f.stopped)
205         })
206
207         return f
208 }
209
210 // telegramOwnerMessage returns a text message of the owner in the private chat.
211 func telegramOwnerMessage(text string) *TelegramMessage {
212         return &TelegramMessage{
213                 Chat: TelegramChat{
214                         ID:   telegramTestOwner,
215                         Type: "private",
216                 },
217                 From: &TelegramUser{
218                         ID: telegramTestOwner,
219                 },
220                 Text: text,
221         }
222 }