(new): rich message support
(fix): runtime reliability (tests): regression coverage (doc): v1.1 release notes
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package tgapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -105,6 +106,17 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploaderRejectsDirectRichMessageDraftUpload(t *testing.T) {
|
||||
uploader := &Uploader{}
|
||||
_, err := uploader.SendRichMessageDraft(
|
||||
SendRichMessageDraft{ChatID: 42, DraftID: 1},
|
||||
NewUploaderFile("photo.jpg", []byte("photo")),
|
||||
)
|
||||
if !errors.Is(err, ErrRichMessageDraftUploadUnsupported) {
|
||||
t.Fatalf("expected ErrRichMessageDraftUploadUnsupported, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploaderSurfacesResponseErrorForTelegramFailure(t *testing.T) {
|
||||
const responseBody = `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`
|
||||
|
||||
@@ -177,6 +189,141 @@ func TestNewUploaderFileDetectsFileTypeCaseInsensitively(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploaderSendLivePhotoUsesRequiredMultipartFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
send func(*Uploader) (Message, error)
|
||||
}{
|
||||
{
|
||||
name: "background context",
|
||||
send: func(uploader *Uploader) (Message, error) {
|
||||
return uploader.SendLivePhoto(
|
||||
UploadLivePhoto{ChatID: 42},
|
||||
NewUploaderFile("live.mp4", []byte("video")),
|
||||
NewUploaderFile("photo.jpg", []byte("image")),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "explicit context",
|
||||
send: func(uploader *Uploader) (Message, error) {
|
||||
return uploader.SendLivePhotoWithContext(
|
||||
context.Background(),
|
||||
UploadLivePhoto{ChatID: 42},
|
||||
NewUploaderFile("live.mp4", []byte("video")),
|
||||
NewUploaderFile("photo.jpg", []byte("image")),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var (
|
||||
gotPath string
|
||||
gotFiles map[string]multipartFile
|
||||
parseErr error
|
||||
)
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
gotPath = req.URL.Path
|
||||
gotFiles, parseErr = readMultipartFiles(req)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":{"message_id":5,"date":1}}`)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
api := NewAPI(
|
||||
NewAPIOpts("token").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
if err := api.Close(); err != nil {
|
||||
t.Errorf("Close API returned error: %v", err)
|
||||
}
|
||||
}()
|
||||
uploader := NewUploader(api)
|
||||
defer func() {
|
||||
if err := uploader.Close(); err != nil {
|
||||
t.Errorf("Close uploader returned error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tt.send(uploader); err != nil {
|
||||
t.Fatalf("SendLivePhoto returned error: %v", err)
|
||||
}
|
||||
if parseErr != nil {
|
||||
t.Fatalf("multipart parse failed: %v", parseErr)
|
||||
}
|
||||
if gotPath != "/bottoken/sendLivePhoto" {
|
||||
t.Fatalf("unexpected request path: %q", gotPath)
|
||||
}
|
||||
assertMultipartFile(t, gotFiles, "live_photo", "live.mp4", "video")
|
||||
assertMultipartFile(t, gotFiles, "photo", "photo.jpg", "image")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareMultipartRichMessageUsesAttachName(t *testing.T) {
|
||||
params := SendRichMessage{
|
||||
ChatID: 42,
|
||||
RichMessage: InputRichMessage{Blocks: []InputRichBlock{
|
||||
InputRichBlockAnimation{
|
||||
Type: InputRichTypeAnimation,
|
||||
Animation: InputMedia{Type: InputMediaTypeAnimation, Media: "attach://animation"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
buf, contentType, err := prepareMultipart(
|
||||
[]UploaderFile{NewUploaderFile("animation.mp4", []byte("animation")).SetAttachName("animation")},
|
||||
params,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("prepareMultipart returned error: %v", err)
|
||||
}
|
||||
|
||||
_, contentTypeParams, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMediaType returned error: %v", err)
|
||||
}
|
||||
reader := multipart.NewReader(buf, contentTypeParams["boundary"])
|
||||
|
||||
parts := make(map[string]string)
|
||||
var fileData []byte
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("NextPart returned error: %v", err)
|
||||
}
|
||||
data, err := io.ReadAll(part)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll returned error: %v", err)
|
||||
}
|
||||
if part.FileName() != "" {
|
||||
if part.FormName() != "animation" {
|
||||
t.Errorf("file form name = %q, want animation", part.FormName())
|
||||
}
|
||||
fileData = data
|
||||
continue
|
||||
}
|
||||
parts[part.FormName()] = string(data)
|
||||
}
|
||||
|
||||
if string(fileData) != "animation" {
|
||||
t.Errorf("file data = %q, want animation", fileData)
|
||||
}
|
||||
if got := parts["rich_message"]; !strings.Contains(got, `"media":"attach://animation"`) {
|
||||
t.Errorf("rich_message = %s, want attach reference", got)
|
||||
}
|
||||
}
|
||||
|
||||
func readMultipartRequest(req *http.Request) (map[string]string, string, []byte, error) {
|
||||
_, params, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
@@ -209,3 +356,48 @@ func readMultipartRequest(req *http.Request) (map[string]string, string, []byte,
|
||||
fields[part.FormName()] = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
type multipartFile struct {
|
||||
name string
|
||||
data string
|
||||
}
|
||||
|
||||
func readMultipartFiles(req *http.Request) (map[string]multipartFile, error) {
|
||||
_, params, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := multipart.NewReader(req.Body, params["boundary"])
|
||||
files := make(map[string]multipartFile)
|
||||
for {
|
||||
part, err := reader.NextPart()
|
||||
if err == io.EOF {
|
||||
return files, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if part.FileName() == "" {
|
||||
continue
|
||||
}
|
||||
data, err := io.ReadAll(part)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files[part.FormName()] = multipartFile{name: part.FileName(), data: string(data)}
|
||||
}
|
||||
}
|
||||
|
||||
func assertMultipartFile(t *testing.T, files map[string]multipartFile, field, name, data string) {
|
||||
t.Helper()
|
||||
file, ok := files[field]
|
||||
if !ok {
|
||||
t.Fatalf("multipart field %q is missing", field)
|
||||
}
|
||||
if file.name != name {
|
||||
t.Errorf("multipart field %q filename = %q, want %q", field, file.name, name)
|
||||
}
|
||||
if file.data != data {
|
||||
t.Errorf("multipart field %q data = %q, want %q", field, file.data, data)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user