Files
go-telegram/transport/extra_test.go
T
lukaszraczylo ac7cae8fa7 Initial release of go-telegram
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
2026-05-09 13:09:27 +01:00

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 }