mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-26 04:43:07 +00:00
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
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user