Files
go-telegram/client/extra_test.go
T
lukaszraczylo 9072e9eafb 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

227 lines
6.7 KiB
Go

package client
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// client.go option getters
// ---------------------------------------------------------------------------
func TestBot_Getters(t *testing.T) {
b := New("mytoken",
WithBaseURL("http://localhost:9999"),
WithCodec(DefaultCodec{}),
WithLogger(NoopLogger{}),
)
require.Equal(t, "mytoken", b.Token())
require.Equal(t, "http://localhost:9999", b.BaseURL())
require.NotNil(t, b.HTTP())
require.NotNil(t, b.Codec())
require.NotNil(t, b.Logger())
}
func TestWithLogger_NilBecomesNoop(t *testing.T) {
b := New("t", WithLogger(nil))
require.IsType(t, NoopLogger{}, b.Logger())
}
func TestNoopLogger_AllMethods(t *testing.T) {
l := NoopLogger{}
// None of these should panic.
l.Debug("msg")
l.Info("msg", "k", "v")
l.Warn("msg")
l.Error("msg", "err", "oops")
}
// ---------------------------------------------------------------------------
// RetryOption setters
// ---------------------------------------------------------------------------
func TestRetryOptions_Applied(t *testing.T) {
d := NewRetryDoer(nil,
WithMaxAttempts(7),
WithBaseBackoff(1*time.Second),
WithMaxBackoff(60*time.Second),
WithBackoffFactor(3.0),
WithJitter(0.5),
)
require.Equal(t, 7, d.maxAttempts)
require.Equal(t, 1*time.Second, d.base)
require.Equal(t, 60*time.Second, d.max)
require.Equal(t, 3.0, d.factor)
require.Equal(t, 0.5, d.jitter)
}
// ---------------------------------------------------------------------------
// RetryDoer.delay — override path
// ---------------------------------------------------------------------------
func TestRetryDoer_DelayOverride(t *testing.T) {
d := NewRetryDoer(nil)
got := d.delay(1, 5*time.Second)
require.Equal(t, 5*time.Second, got)
}
func TestRetryDoer_DelayExponential(t *testing.T) {
d := NewRetryDoer(nil,
WithBaseBackoff(100*time.Millisecond),
WithMaxBackoff(10*time.Second),
WithJitter(0), // no jitter for deterministic test
WithBackoffFactor(2.0),
)
d1 := d.delay(1, 0)
d2 := d.delay(2, 0)
require.Greater(t, int64(d2), int64(d1), "backoff should grow")
}
func TestRetryDoer_DelayMaxCap(t *testing.T) {
d := NewRetryDoer(nil,
WithBaseBackoff(1*time.Second),
WithMaxBackoff(2*time.Second),
WithJitter(0),
WithBackoffFactor(100.0),
)
delay := d.delay(10, 0)
require.LessOrEqual(t, delay, 2*time.Second)
}
// ---------------------------------------------------------------------------
// errors.go — RetryAfter nil parameters + ParseError.Unwrap
// ---------------------------------------------------------------------------
func TestAPIError_RetryAfterNilParams(t *testing.T) {
e := &APIError{Code: 429, Description: "Too Many Requests", Parameters: nil}
require.Equal(t, time.Duration(0), e.RetryAfter())
}
func TestParseError_Unwrap(t *testing.T) {
inner := errors.New("decode error")
pe := &ParseError{Err: inner, Body: []byte("body")}
require.ErrorIs(t, pe, inner)
}
func TestParseError_LongBodyTruncated(t *testing.T) {
body := bytes.Repeat([]byte("x"), 1000)
pe := &ParseError{Err: errors.New("e"), Body: body}
msg := pe.Error()
// Error() truncates body to 256 for display — should not include all 1000 chars
require.Less(t, len(msg), 800, "should truncate body in Error()")
}
func TestNetworkError_Unwrap(t *testing.T) {
inner := errors.New("tcp error")
ne := &NetworkError{Err: inner}
require.ErrorIs(t, ne, inner)
}
// ---------------------------------------------------------------------------
// mapAPIError — missing sentinel branches (generic 400, unmapped 500)
// ---------------------------------------------------------------------------
func TestMapAPIError_Generic400(t *testing.T) {
e := mapAPIError(400, "Bad Request: some unknown thing", nil)
require.True(t, errors.Is(e, ErrBadRequest))
}
func TestMapAPIError_Unmapped500(t *testing.T) {
e := mapAPIError(500, "Internal Server Error", nil)
require.Nil(t, e.sentinel)
require.Equal(t, 500, e.Code)
}
func TestMapAPIError_403(t *testing.T) {
e := mapAPIError(403, "Forbidden: bot was blocked", nil)
require.True(t, errors.Is(e, ErrForbidden))
}
// ---------------------------------------------------------------------------
// callMultipart — ctx cancelled
// ---------------------------------------------------------------------------
func TestCallMultipart_ContextCancelled(t *testing.T) {
// A doer that blocks then returns context error.
blocker := &extraBlockingDoer{done: make(chan struct{})}
b := New("t", WithHTTPClient(blocker))
ctx, cancel := context.WithCancel(context.Background())
mp := &extraFakeMultipartReq{
fields: map[string]string{"chat_id": "1"},
files: []MultipartFile{
{FieldName: "document", Filename: "f.txt", Reader: bytes.NewReader([]byte("data"))},
},
}
go func() {
time.Sleep(10 * time.Millisecond)
cancel()
close(blocker.done)
}()
_, err := callMultipart[*struct{}](ctx, b, "sendDocument", mp)
require.Error(t, err)
}
type extraBlockingDoer struct{ done chan struct{} }
func (b *extraBlockingDoer) Do(r *http.Request) (*http.Response, error) {
<-b.done
return nil, r.Context().Err()
}
type extraFakeMultipartReq struct {
fields map[string]string
files []MultipartFile
}
func (f *extraFakeMultipartReq) HasFile() bool { return len(f.files) > 0 }
func (f *extraFakeMultipartReq) MultipartFiles() []MultipartFile { return f.files }
func (f *extraFakeMultipartReq) MultipartFields() map[string]string { return f.fields }
// ---------------------------------------------------------------------------
// copyBody size cap
// ---------------------------------------------------------------------------
func TestCopyBody_LargeBodyCapped(t *testing.T) {
big := bytes.Repeat([]byte("a"), 8000)
out := copyBody(big)
require.Len(t, out, 4096)
}
func TestCopyBody_SmallBody(t *testing.T) {
small := []byte("hello")
out := copyBody(small)
require.Equal(t, small, out)
}
// ---------------------------------------------------------------------------
// Call — 5xx non-200 HTTP status (transport level)
// ---------------------------------------------------------------------------
func TestCall_5xxHTTPStatus(t *testing.T) {
m := &mockDoer{}
m.On("Do", mock.Anything).Return(&http.Response{
StatusCode: 500,
Body: io.NopCloser(bytes.NewBufferString(`{"ok":false,"error_code":500,"description":"Internal"}`)),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil)
b := New("t", WithHTTPClient(m))
_, err := Call[*echoReq, *echoResp](context.Background(), b, "x", &echoReq{})
require.Error(t, err)
var ae *APIError
require.ErrorAs(t, err, &ae)
require.Equal(t, 500, ae.Code)
}