]> git.giacomolli.uk - onechat/blobdiff - botapi_test.go
Name package-level code for its feature
[onechat] / botapi_test.go
index b571c6ae0e625fc1d6b1704ad40f9f60be901a82..9dfbcad8a11d2734bb2b2ba16ea47ce5c633c846 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
 package main
 
 import (
+       "context"
        "encoding/json"
        "fmt"
        "io"
        "encoding/json"
        "fmt"
        "io"
@@ -47,7 +48,7 @@ func TestGetUpdatesAsksForMessagesFromTheOffset(t *testing.T) {
 
 func TestSendMessageSendsPlainText(t *testing.T) {
        server := newFakeBot(t)
 
 func TestSendMessageSendsPlainText(t *testing.T) {
        server := newFakeBot(t)
-       err := newBotAPI(server.URL, testBotToken).SendMessage(t.Context(), testOwner, "*not bold*")
+       err := newBotAPI(server.URL, testBotToken).SendMessage(t.Context(), testOwner, "*not bold*", nil)
        require.NoError(t, err)
 
        require.Equal(t, map[string]any{
        require.NoError(t, err)
 
        require.Equal(t, map[string]any{
@@ -56,12 +57,100 @@ func TestSendMessageSendsPlainText(t *testing.T) {
        }, server.Calls()[0].params)
 }
 
        }, server.Calls()[0].params)
 }
 
+func TestSendMessageSendsTheEntities(t *testing.T) {
+       server := newFakeBot(t)
+       err := newBotAPI(server.URL, testBotToken).SendMessage(t.Context(), testOwner, "a link", []botEntity{
+               {
+                       Type:   "text_link",
+                       Offset: 2,
+                       Length: 4,
+                       URL:    "https://example.com",
+               },
+       })
+       require.NoError(t, err)
+
+       require.Equal(t, []any{
+               map[string]any{
+                       "type":   "text_link",
+                       "offset": float64(2),
+                       "length": float64(4),
+                       "url":    "https://example.com",
+               },
+       }, server.Calls()[0].params["entities"])
+}
+
+func TestSendRichMessageSendsTheMarkdownUnchanged(t *testing.T) {
+       server := newFakeBot(t)
+       markdown := "| a | b |\n|---|---|\n| **1** | `2` |"
+       err := newBotAPI(server.URL, testBotToken).SendRichMessage(t.Context(), testOwner, markdown)
+       require.NoError(t, err)
+
+       call := server.Calls()[0]
+       require.Equal(t, "sendRichMessage", call.method)
+       require.Equal(t, map[string]any{
+               "chat_id": float64(testOwner),
+               "rich_message": map[string]any{
+                       "markdown": markdown,
+               },
+       }, call.params)
+}
+
+func TestSendCallsTimeOutOnAStalledConnection(t *testing.T) {
+       testCases := []struct {
+               method string
+               send   func(ctx context.Context, api *botAPI) error
+       }{
+               {
+                       method: "sendMessage",
+                       send: func(ctx context.Context, api *botAPI) error {
+                               return api.SendMessage(ctx, testOwner, "hi", nil)
+                       },
+               },
+               {
+                       method: "sendRichMessage",
+                       send: func(ctx context.Context, api *botAPI) error {
+                               return api.SendRichMessage(ctx, testOwner, "hi")
+                       },
+               },
+       }
+
+       for _, tc := range testCases {
+               t.Run(tc.method, func(t *testing.T) {
+                       server := newFakeBot(t)
+                       server.stall = func(call fakeBotCall) bool {
+                               return call.method == tc.method
+                       }
+
+                       api := newBotAPI(server.URL, testBotToken)
+                       api.sendTimeout = 50 * time.Millisecond
+                       start := time.Now()
+                       err := tc.send(t.Context(), api)
+                       require.ErrorIs(t, err, errBotNetwork)
+                       require.Less(t, time.Since(start), 5*time.Second)
+                       require.NotContains(t, err.Error(), testBotToken)
+                       require.NotContains(t, err.Error(), server.URL)
+
+                       retry, _ := retryable(err)
+                       require.True(t, retry)
+
+                       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)
+                       require.Less(t, time.Since(start), 5*time.Second)
+               })
+       }
+}
+
 func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
        testCases := []struct {
                name      string
                reply     func(w http.ResponseWriter)
                retry     bool
                wait      time.Duration
 func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
        testCases := []struct {
                name      string
                reply     func(w http.ResponseWriter)
                retry     bool
                wait      time.Duration
+               rejected  bool
                errorText string
        }{
                {
                errorText string
        }{
                {
@@ -103,6 +192,35 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
                        retry:     false,
                        errorText: "Unauthorized: botTOKEN",
                },
                        retry:     false,
                        errorText: "Unauthorized: botTOKEN",
                },
+               {
+                       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",
+               },
+               {
+                       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",
+               },
+               {
+                       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",
+               },
                {
                        name: "broken answer is retried",
                        reply: func(w http.ResponseWriter) {
                {
                        name: "broken answer is retried",
                        reply: func(w http.ResponseWriter) {
@@ -120,7 +238,7 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
                        }))
                        t.Cleanup(server.Close)
 
                        }))
                        t.Cleanup(server.Close)
 
-                       err := newBotAPI(server.URL, testBotToken).SendMessage(t.Context(), testOwner, "hi")
+                       err := newBotAPI(server.URL, testBotToken).SendMessage(t.Context(), testOwner, "hi", nil)
                        require.ErrorContains(t, err, tc.errorText)
                        require.NotContains(t, err.Error(), testBotToken)
                        require.NotContains(t, err.Error(), server.URL)
                        require.ErrorContains(t, err, tc.errorText)
                        require.NotContains(t, err.Error(), testBotToken)
                        require.NotContains(t, err.Error(), server.URL)
@@ -128,6 +246,7 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
                        retry, wait := retryable(err)
                        require.Equal(t, tc.retry, retry)
                        require.Equal(t, tc.wait, wait)
                        retry, wait := retryable(err)
                        require.Equal(t, tc.retry, retry)
                        require.Equal(t, tc.wait, wait)
+                       require.Equal(t, tc.rejected, rejected(err))
                })
        }
 
                })
        }
 
@@ -142,8 +261,9 @@ func TestBotErrorsDoNotHoldTheToken(t *testing.T) {
 // fakeBot is a fake Bot API server for testBotToken. 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
 // fakeBot is a fake Bot API server for testBotToken. 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. Fail runs with the mutex
-// held.
+// 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 fakeBot struct {
        *httptest.Server
 
 type fakeBot struct {
        *httptest.Server
 
@@ -151,7 +271,9 @@ type fakeBot struct {
        updates []botUpdate
        calls   []fakeBotCall
        fail    func(call fakeBotCall) (int, string)
        updates []botUpdate
        calls   []fakeBotCall
        fail    func(call fakeBotCall) (int, string)
+       stall   func(call fakeBotCall) bool
        added   chan struct{}
        added   chan struct{}
+       stopped chan struct{}
 }
 
 // fakeBotCall is one call of the fake Bot API. OK tells if the call got a
 }
 
 // fakeBotCall is one call of the fake Bot API. OK tells if the call got a
@@ -167,11 +289,15 @@ func newFakeBot(t *testing.T) *fakeBot {
        t.Helper()
 
        f := &fakeBot{
        t.Helper()
 
        f := &fakeBot{
-               added: make(chan struct{}, 1),
+               added:   make(chan struct{}, 1),
+               stopped: make(chan struct{}),
        }
 
        f.Server = httptest.NewServer(http.HandlerFunc(f.serve))
        t.Cleanup(f.Close)
        }
 
        f.Server = httptest.NewServer(http.HandlerFunc(f.serve))
        t.Cleanup(f.Close)
+       t.Cleanup(func() {
+               close(f.stopped)
+       })
 
        return f
 }
 
        return f
 }
@@ -233,8 +359,19 @@ func (f *fakeBot) serve(w http.ResponseWriter, r *http.Request) {
                status, description = f.fail(call)
        }
 
                status, description = f.fail(call)
        }
 
+       stall := f.stall != nil && f.stall(call)
        f.mutex.Unlock()
 
        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)
        if status != 0 {
                f.record(call)
                w.WriteHeader(status)