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
189 lines
6.1 KiB
Go
189 lines
6.1 KiB
Go
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/go-telegram/api"
|
|
"github.com/lukaszraczylo/go-telegram/client"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LongPoller — unauthorized error causes immediate return
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestLongPoller_UnauthorizedExits(t *testing.T) {
|
|
m := &mockDoer{}
|
|
m.On("Do", mock.Anything).Return(
|
|
resp(`{"ok":false,"error_code":401,"description":"Unauthorized"}`), nil,
|
|
).Once()
|
|
|
|
b := client.New("bad-token", client.WithHTTPClient(m))
|
|
p := NewLongPoller(b)
|
|
p.Timeout = 0
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
err := p.Run(ctx)
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, client.ErrUnauthorized), "expected unauthorized: %v", err)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LongPoller — ctx cancelled while waiting for retry backoff
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestLongPoller_CtxCancelledDuringBackoff(t *testing.T) {
|
|
m := &mockDoer{}
|
|
var callCount int
|
|
m.On("Do", mock.Anything).Run(func(args mock.Arguments) {
|
|
callCount++
|
|
}).Return(nil, errors.New("network error")).Maybe()
|
|
|
|
b := client.New("t", client.WithHTTPClient(m))
|
|
p := NewLongPoller(b)
|
|
p.Timeout = 0
|
|
// Long backoff ensures ctx cancels before retry fires.
|
|
p.Backoff = &ExponentialBackoff{Base: 5 * time.Second, Max: 5 * time.Second, Factor: 1}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
err := p.Run(ctx)
|
|
require.Error(t, err)
|
|
// Should fail fast, not wait the full 5s backoff.
|
|
require.LessOrEqual(t, callCount, 3)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LongPoller — AllowedTypes field
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestLongPoller_AllowedTypes(t *testing.T) {
|
|
m := &mockDoer{}
|
|
var seenBody string
|
|
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
|
b, _ := io.ReadAll(r.Body)
|
|
seenBody = string(b)
|
|
return true
|
|
})).Return(resp(`{"ok":true,"result":[]}`), 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
|
|
p.AllowedTypes = []api.UpdateType{"message", "callback_query"}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
_ = p.Run(ctx)
|
|
|
|
require.Contains(t, seenBody, "allowed_updates")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WebhookServer — ListenAndServe error (bind on in-use port)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestWebhookServer_ListenAndServeError(t *testing.T) {
|
|
// Bind a port to block the webhook server.
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = l.Close() })
|
|
addr := l.Addr().String()
|
|
|
|
b := client.New("t")
|
|
w := NewWebhookServer(b)
|
|
|
|
ctx := context.Background()
|
|
err = w.ListenAndServe(ctx, addr)
|
|
require.Error(t, err, "should error when port is in use")
|
|
require.False(t, errors.Is(err, http.ErrServerClosed))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WebhookServer — body too large (> 1 MiB)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestWebhookServer_BodyTooLarge(t *testing.T) {
|
|
b := client.New("t")
|
|
w := NewWebhookServer(b)
|
|
|
|
// Construct a body slightly over 1 MiB.
|
|
bigBody := bytes.Repeat([]byte("x"), 1<<20+1)
|
|
req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewReader(bigBody))
|
|
rw := newTestResponseWriter()
|
|
w.ServeHTTP(rw, req)
|
|
require.Equal(t, http.StatusRequestEntityTooLarge, rw.code)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WebhookServer — Stop when srv is nil (no ListenAndServe called)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestWebhookServer_StopNoServer(t *testing.T) {
|
|
b := client.New("t")
|
|
w := NewWebhookServer(b)
|
|
require.NoError(t, w.Stop(context.Background()))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WebhookServer — no secret token, any POST accepted
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestWebhookServer_NoSecretAllowsAnyPost(t *testing.T) {
|
|
b := client.New("t")
|
|
w := NewWebhookServer(b)
|
|
|
|
body := `{"update_id":99}`
|
|
req, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(body))
|
|
// No secret header set.
|
|
rw := newTestResponseWriter()
|
|
// ServeHTTP would block on w.out <- u unless we drain it.
|
|
go func() {
|
|
for range w.Updates() {
|
|
}
|
|
}()
|
|
w.ServeHTTP(rw, req)
|
|
// Bad JSON would return 400; update_id-only body is valid enough for Update.
|
|
require.NotEqual(t, http.StatusUnauthorized, rw.code)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ExponentialBackoff — negative attempt clamped to 1
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestExponentialBackoff_NegativeAttempt(t *testing.T) {
|
|
b := DefaultBackoff()
|
|
d := b.NextDelay(-5)
|
|
require.GreaterOrEqual(t, d, time.Duration(0))
|
|
require.LessOrEqual(t, d, b.Max)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type testResponseWriter struct {
|
|
code int
|
|
header http.Header
|
|
}
|
|
|
|
func newTestResponseWriter() *testResponseWriter {
|
|
return &testResponseWriter{code: http.StatusOK, header: http.Header{}}
|
|
}
|
|
|
|
func (r *testResponseWriter) Header() http.Header { return r.header }
|
|
func (r *testResponseWriter) Write(b []byte) (int, error) { return len(b), nil }
|
|
func (r *testResponseWriter) WriteHeader(statusCode int) { r.code = statusCode }
|