mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
ac7cae8fa7
A fully-generated, strongly-typed Go client for the Telegram Bot API. * 176 methods + 301 types generated from Bot API v10.0 * 1408 auto-generated tests (8 scenarios per method) * Typed unions throughout — no 'any' in the public surface * Pluggable HTTP transport and JSON codec (default goccy/go-json) * Built-in retry middleware honouring Telegram's retry_after * Generic dispatcher with filters and conversation handlers * Self-verifying codegen pipeline (regen → audit → emit → run tests) * 14 example bots covering common patterns
147 lines
4.0 KiB
Go
147 lines
4.0 KiB
Go
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/go-telegram/client"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockDoer struct{ mock.Mock }
|
|
|
|
func (m *mockDoer) Do(r *http.Request) (*http.Response, error) {
|
|
args := m.Called(r)
|
|
if v := args.Get(0); v != nil {
|
|
return v.(*http.Response), args.Error(1)
|
|
}
|
|
return nil, args.Error(1)
|
|
}
|
|
|
|
func resp(body string) *http.Response {
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: io.NopCloser(bytes.NewBufferString(body)),
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
}
|
|
}
|
|
|
|
func TestLongPoller_DeliversUpdatesAndAdvancesOffset(t *testing.T) {
|
|
m := &mockDoer{}
|
|
m.On("Do", mock.Anything).Return(
|
|
resp(`{"ok":true,"result":[{"update_id":10,"message":{"message_id":1,"date":0,"chat":{"id":1,"type":"private"},"text":"hi"}}]}`),
|
|
nil,
|
|
).Once()
|
|
m.On("Do", mock.Anything).Return(
|
|
resp(`{"ok":true,"result":[{"update_id":11,"message":{"message_id":2,"date":0,"chat":{"id":1,"type":"private"},"text":"there"}}]}`),
|
|
nil,
|
|
).Once()
|
|
m.On("Do", mock.Anything).Return(
|
|
resp(`{"ok":true,"result":[]}`),
|
|
nil,
|
|
).Maybe()
|
|
|
|
b := client.New("t", client.WithHTTPClient(m))
|
|
p := NewLongPoller(b)
|
|
p.Timeout = 0
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
|
|
go func() { _ = p.Run(ctx) }()
|
|
|
|
u1 := <-p.Updates()
|
|
require.Equal(t, int64(10), u1.UpdateID)
|
|
u2 := <-p.Updates()
|
|
require.Equal(t, int64(11), u2.UpdateID)
|
|
}
|
|
|
|
func TestLongPoller_BackoffOnNetworkError(t *testing.T) {
|
|
m := &mockDoer{}
|
|
var attempts atomic.Int32
|
|
m.On("Do", mock.Anything).Run(func(args mock.Arguments) {
|
|
attempts.Add(1)
|
|
}).Return(nil, io.ErrUnexpectedEOF).Maybe()
|
|
|
|
b := client.New("t", client.WithHTTPClient(m))
|
|
p := NewLongPoller(b)
|
|
p.Timeout = 0
|
|
p.Backoff = &ExponentialBackoff{Base: 5 * time.Millisecond, Max: 5 * time.Millisecond}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_ = p.Run(ctx)
|
|
require.GreaterOrEqual(t, attempts.Load(), int32(2), "should retry at least once")
|
|
}
|
|
|
|
func TestLongPoller_StopCloses(t *testing.T) {
|
|
m := &mockDoer{}
|
|
m.On("Do", mock.Anything).Return(resp(`{"ok":true,"result":[]}`), nil).Maybe()
|
|
|
|
b := client.New("t", client.WithHTTPClient(m))
|
|
p := NewLongPoller(b)
|
|
p.Timeout = 0
|
|
|
|
ctx := context.Background()
|
|
done := make(chan struct{})
|
|
go func() { _ = p.Run(ctx); close(done) }()
|
|
|
|
require.NoError(t, p.Stop(ctx))
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Run did not exit after Stop")
|
|
}
|
|
|
|
// Channel must be closed.
|
|
_, ok := <-p.Updates()
|
|
require.False(t, ok, "expected closed channel after Stop")
|
|
}
|
|
func TestLongPoller_HonoursRetryAfterOn429(t *testing.T) {
|
|
m := &mockDoer{}
|
|
var requestTimes []time.Time
|
|
var mu sync.Mutex
|
|
|
|
record := func(args mock.Arguments) {
|
|
mu.Lock()
|
|
requestTimes = append(requestTimes, time.Now())
|
|
mu.Unlock()
|
|
}
|
|
|
|
// First call: 429 with retry_after=1.
|
|
m.On("Do", mock.Anything).
|
|
Run(record).
|
|
Return(resp(`{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil).
|
|
Once()
|
|
// Subsequent calls: empty success.
|
|
m.On("Do", mock.Anything).
|
|
Run(record).
|
|
Return(resp(`{"ok":true,"result":[]}`), nil).
|
|
Maybe()
|
|
|
|
b := client.New("t", client.WithHTTPClient(m))
|
|
p := NewLongPoller(b)
|
|
p.Timeout = 0
|
|
// Backoff base is huge so if it were used we'd see >>1s delay.
|
|
p.Backoff = &ExponentialBackoff{Base: 10 * time.Second, Max: 30 * time.Second}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2500*time.Millisecond)
|
|
defer cancel()
|
|
_ = p.Run(ctx)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
require.GreaterOrEqual(t, len(requestTimes), 2, "expected at least 2 requests")
|
|
gap := requestTimes[1].Sub(requestTimes[0])
|
|
require.GreaterOrEqual(t, gap, 900*time.Millisecond, "should have waited ~1s per retry_after, got %v", gap)
|
|
require.Less(t, gap, 3*time.Second, "should not have waited backoff base (10s), got %v", gap)
|
|
}
|