mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-05 22:43:59 +00:00
9072e9eafb
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
104 lines
2.4 KiB
Go
104 lines
2.4 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type fakeMultipartReq struct {
|
|
chatID int64
|
|
body string
|
|
}
|
|
|
|
func (f *fakeMultipartReq) HasFile() bool { return true }
|
|
func (f *fakeMultipartReq) MultipartFields() map[string]string {
|
|
return map[string]string{"chat_id": "42"}
|
|
}
|
|
func (f *fakeMultipartReq) MultipartFiles() []MultipartFile {
|
|
return []MultipartFile{{
|
|
FieldName: "document",
|
|
Filename: "hello.txt",
|
|
Reader: strings.NewReader(f.body),
|
|
}}
|
|
}
|
|
|
|
type fileResp struct {
|
|
MessageID int64 `json:"message_id"`
|
|
}
|
|
|
|
func TestCallMultipart_Success(t *testing.T) {
|
|
m := &mockDoer{}
|
|
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
|
ct := r.Header.Get("Content-Type")
|
|
if !strings.HasPrefix(ct, "multipart/form-data") {
|
|
return false
|
|
}
|
|
_, params, err := mime.ParseMediaType(ct)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
mr := multipart.NewReader(r.Body, params["boundary"])
|
|
seenChat := false
|
|
seenFile := false
|
|
for {
|
|
p, err := mr.NextPart()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return false
|
|
}
|
|
switch p.FormName() {
|
|
case "chat_id":
|
|
body, _ := io.ReadAll(p)
|
|
seenChat = string(body) == "42"
|
|
case "document":
|
|
body, _ := io.ReadAll(p)
|
|
seenFile = string(body) == "hello world"
|
|
}
|
|
}
|
|
return seenChat && seenFile
|
|
})).Return(newResp(200, `{"ok":true,"result":{"message_id":99}}`), nil)
|
|
|
|
b := New("t", WithHTTPClient(m))
|
|
out, err := Call[*fakeMultipartReq, *fileResp](context.Background(), b, "sendDocument", &fakeMultipartReq{chatID: 42, body: "hello world"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(99), out.MessageID)
|
|
}
|
|
|
|
func TestCallMultipart_NoGoroutineLeakOnError(t *testing.T) {
|
|
m := &mockDoer{}
|
|
m.On("Do", mock.Anything).Return(nil, errors.New("dial timeout"))
|
|
|
|
b := New("t", WithHTTPClient(m))
|
|
before := runtime.NumGoroutine()
|
|
|
|
for i := 0; i < 50; i++ {
|
|
_, _ = Call[*fakeMultipartReq, *fileResp](
|
|
context.Background(), b, "sendDocument",
|
|
&fakeMultipartReq{chatID: 42, body: strings.Repeat("x", 1<<14)},
|
|
)
|
|
}
|
|
|
|
// Allow goroutines to finish exiting after Close propagates.
|
|
time.Sleep(50 * time.Millisecond)
|
|
runtime.GC()
|
|
after := runtime.NumGoroutine()
|
|
|
|
// A small drift is normal (timers, finalizers); 5 is generous.
|
|
if after-before > 5 {
|
|
t.Fatalf("goroutine leak: before=%d after=%d", before, after)
|
|
}
|
|
}
|