X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/09cde69fdffe2721434392036f98dc241177f5a1..4c199e7d93b6a4529ed5956807911b5f6c5489a7:/telegram_api_test.go?ds=sidebyside diff --git a/telegram_api_test.go b/telegram_api_test.go index bbfd93e..98aa232 100644 --- a/telegram_api_test.go +++ b/telegram_api_test.go @@ -2,28 +2,15 @@ package main import ( "context" - "encoding/json" - "fmt" "io" "net/http" "net/http/httptest" - "strings" - "sync" "testing" "time" "github.com/stretchr/testify/require" ) -const ( - // telegramTestToken is the bot token of the tests. It has the form of a - // real token. - telegramTestToken = "123456:TEST-secret_token" - - // telegramTestOwner is the user ID of the owner of the test bot. - telegramTestOwner = 4242 -) - func TestGetUpdatesAsksForMessagesFromTheOffset(t *testing.T) { server := newTelegramFake(t) server.Add(TelegramUpdate{ @@ -125,7 +112,7 @@ func TestSendCallsTimeOutOnAStalledConnection(t *testing.T) { api.sendTimeout = 50 * time.Millisecond start := time.Now() err := tc.send(t.Context(), api) - require.ErrorIs(t, err, errTelegramNetwork) + require.EqualError(t, err, "no answer in 50ms") require.Less(t, time.Since(start), 5*time.Second) require.NotContains(t, err.Error(), telegramTestToken) require.NotContains(t, err.Error(), server.URL) @@ -136,6 +123,7 @@ func TestSendCallsTimeOutOnAStalledConnection(t *testing.T) { api.sendTimeout = time.Hour ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(50*time.Millisecond, cancel) + start = time.Now() err = tc.send(ctx, api) require.Error(t, err) @@ -162,31 +150,34 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) { } }, retry: true, - errorText: "no valid answer", + errorText: "no answer", }, { name: "rate limit waits as asked", reply: func(w http.ResponseWriter) { w.WriteHeader(http.StatusTooManyRequests) + _, _ = io.WriteString(w, `{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 3","parameters":{"retry_after":3}}`) }, retry: true, wait: 3 * time.Second, - errorText: "answered 429", + errorText: "429", }, { name: "server error page is retried", reply: func(w http.ResponseWriter) { w.WriteHeader(http.StatusBadGateway) + _, _ = io.WriteString(w, "bad gateway") }, retry: true, - errorText: "answered 502: Bad Gateway", + errorText: "502 Bad Gateway", }, { name: "token echoed in a description is hidden", reply: func(w http.ResponseWriter) { w.WriteHeader(http.StatusUnauthorized) + _, _ = io.WriteString(w, `{"ok":false,"error_code":401,"description":"Unauthorized: bot`+telegramTestToken+`"}`) }, retry: false, @@ -196,30 +187,33 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) { name: "bad request is a refusal", reply: func(w http.ResponseWriter) { w.WriteHeader(http.StatusBadRequest) + _, _ = io.WriteString(w, `{"ok":false,"error_code":400,"description":"Bad Request: can't parse"}`) }, retry: false, rejected: true, - errorText: "answered 400: Bad Request: can't parse", + errorText: "400 Bad Request: can't parse", }, { name: "unknown method is a refusal", reply: func(w http.ResponseWriter) { w.WriteHeader(http.StatusNotFound) + _, _ = io.WriteString(w, `{"ok":false,"error_code":404,"description":"Not Found"}`) }, retry: false, rejected: true, - errorText: "answered 404: Not Found", + errorText: "404 Not Found", }, { name: "not found page of a proxy is not a refusal", reply: func(w http.ResponseWriter) { w.WriteHeader(http.StatusNotFound) + _, _ = io.WriteString(w, "not found") }, retry: false, - errorText: "answered 404: Not Found", + errorText: "404 Not Found", }, { name: "broken answer is retried", @@ -227,13 +221,13 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) { _, _ = io.WriteString(w, `{"ok":`) }, retry: true, - errorText: "decode answer", + errorText: "invalid JSON", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { tc.reply(w) })) t.Cleanup(server.Close) @@ -254,206 +248,6 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) { server.Close() _, err := newTelegramAPI(server.URL, telegramTestToken).GetUpdates(t.Context(), 0) - require.ErrorIs(t, err, errTelegramNetwork) + require.ErrorAs(t, err, new(*TelegramNetworkError)) require.NotContains(t, err.Error(), telegramTestToken) } - -// TelegramFakeCall is one call of the fake Bot API. OK tells if the call got a -// success answer. -type TelegramFakeCall struct { - method string - params map[string]any - ok bool -} - -// TelegramFake is a fake Bot API server for telegramTestToken. getUpdates -// returns the added updates from the offset on, or waits a short time when -// there are none. The other methods succeed, unless fail gives an error answer -// for a call. An answer with HTTP 429 asks for a wait of one second. When stall -// is true for a call, the call gets no answer until the client gives it up or -// the test ends. Fail and stall run with the mutex held. -type TelegramFake struct { - *httptest.Server - - mutex sync.Mutex - updates []TelegramUpdate - calls []TelegramFakeCall - fail func(call TelegramFakeCall) (int, string) - stall func(call TelegramFakeCall) bool - added chan struct{} - stopped chan struct{} -} - -// Add makes update available to getUpdates. -func (f *TelegramFake) Add(update TelegramUpdate) { - f.mutex.Lock() - f.updates = append(f.updates, update) - f.mutex.Unlock() - - select { - case f.added <- struct{}{}: - default: - } -} - -// Calls returns the calls so far. -func (f *TelegramFake) Calls() []TelegramFakeCall { - f.mutex.Lock() - defer f.mutex.Unlock() - - return append([]TelegramFakeCall(nil), f.calls...) -} - -// Sent returns the texts of the sendMessage calls and the Markdown of the -// sendRichMessage calls that succeeded, in order. -func (f *TelegramFake) Sent() []string { - var texts []string - for _, call := range f.Calls() { - switch { - case !call.ok: - case call.method == "sendMessage": - texts = append(texts, call.params["text"].(string)) - case call.method == "sendRichMessage": - texts = append(texts, call.params["rich_message"].(map[string]any)["markdown"].(string)) - } - } - - return texts -} - -func (f *TelegramFake) serve(w http.ResponseWriter, r *http.Request) { - method, found := strings.CutPrefix(r.URL.Path, "/bot"+telegramTestToken+"/") - if !found { - http.Error(w, `{"ok":false,"error_code":404,"description":"Not Found"}`, http.StatusNotFound) - return - } - - var params map[string]any - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - call := TelegramFakeCall{ - method: method, - params: params, - } - - f.mutex.Lock() - status, description := 0, "" - if f.fail != nil { - status, description = f.fail(call) - } - - stall := f.stall != nil && f.stall(call) - f.mutex.Unlock() - - if stall { - f.record(call) - select { - case <-r.Context().Done(): - case <-f.stopped: - } - - return - } - - if status != 0 { - f.record(call) - w.WriteHeader(status) - wait := 0 - if status == http.StatusTooManyRequests { - wait = 1 - } - - _, _ = fmt.Fprintf(w, `{"ok":false,"error_code":%d,"description":%q,"parameters":{"retry_after":%d}}`, status, description, wait) - - return - } - - call.ok = true - f.record(call) - if method != "getUpdates" { - _, _ = io.WriteString(w, `{"ok":true,"result":true}`) - return - } - - offset := int64(params["offset"].(float64)) - updates := f.from(offset) - if len(updates) == 0 { - select { - case <-f.added: - case <-time.After(50 * time.Millisecond): - case <-r.Context().Done(): - } - - updates = f.from(offset) - } - - _ = json.NewEncoder(w).Encode(map[string]any{ - "ok": true, - "result": updates, - }) -} - -// record keeps call. -func (f *TelegramFake) record(call TelegramFakeCall) { - f.mutex.Lock() - defer f.mutex.Unlock() - - f.calls = append(f.calls, call) -} - -// from returns the updates from offset on, and forgets the ones before it, as -// Telegram does. -func (f *TelegramFake) from(offset int64) []TelegramUpdate { - f.mutex.Lock() - defer f.mutex.Unlock() - - var updates []TelegramUpdate - kept := f.updates[:0] - for _, update := range f.updates { - if update.UpdateID >= offset { - updates = append(updates, update) - kept = append(kept, update) - } - } - - f.updates = kept - - return updates -} - -// newTelegramFake starts a fake Bot API server that stops at the end of the -// test. -func newTelegramFake(t *testing.T) *TelegramFake { - t.Helper() - - f := &TelegramFake{ - added: make(chan struct{}, 1), - stopped: make(chan struct{}), - } - - f.Server = httptest.NewServer(http.HandlerFunc(f.serve)) - t.Cleanup(f.Close) - t.Cleanup(func() { - close(f.stopped) - }) - - return f -} - -// telegramOwnerMessage returns a text message of the owner in the private chat. -func telegramOwnerMessage(text string) *TelegramMessage { - return &TelegramMessage{ - Chat: TelegramChat{ - ID: telegramTestOwner, - Type: "private", - }, - From: &TelegramUser{ - ID: telegramTestOwner, - }, - Text: text, - } -}