]> git.giacomolli.uk - onechat/blobdiff - telegram_api_test.go
Give the rebuild its own call limit
[onechat] / telegram_api_test.go
index bbfd93e8614d742ec15c057c3fbab2d1b77d5134..98aa232a9539894007921f177c3622c81a88462d 100644 (file)
@@ -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, "<html>bad gateway</html>")
                        },
                        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, "<html>not found</html>")
                        },
                        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(&params)
-       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,
-       }
-}