(new): support Bot API 10.3
Golang lint / lint (push) Failing after 1m37s

(fix): finalize v2 contracts
(tests): cover v2 migration
(doc): prepare release guidance
This commit is contained in:
2026-09-08 23:21:38 +03:00
parent 24040fe164
commit d78526242b
88 changed files with 2321 additions and 918 deletions
+162 -13
View File
@@ -38,13 +38,15 @@ func TestSendRichMessageDraftMarshal(t *testing.T) {
ChatID: 1,
DraftID: 7,
RichMessage: InputRichMessage{Markdown: "*hi*"},
CanStop: true,
KeepOnStop: true,
}
data, err := json.Marshal(params)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
got := string(data)
for _, want := range []string{`"chat_id":1`, `"draft_id":7`, `"rich_message":{"markdown":"*hi*"}`} {
for _, want := range []string{`"chat_id":1`, `"draft_id":7`, `"rich_message":{"markdown":"*hi*"}`, `"can_stop":true`, `"keep_on_stop":true`} {
if !strings.Contains(got, want) {
t.Fatalf("missing %s in sendRichMessageDraft JSON: %s", want, got)
}
@@ -131,18 +133,18 @@ func TestEphemeralSendParametersMarshal(t *testing.T) {
name string
params any
}{
{"message", SendMessage{ChatID: 1, Text: "text", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"animation", SendAnimation{ChatID: 1, Animation: "animation", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"audio", SendAudio{ChatID: 1, Audio: "audio", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"document", SendDocument{ChatID: 1, Document: "document", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"photo", SendPhoto{ChatID: 1, Photo: "photo", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"sticker", SendSticker{ChatID: 1, Sticker: "sticker", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"video", SendVideo{ChatID: 1, Video: "video", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"video note", SendVideoNote{ChatID: 1, VideoNote: "video-note", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"voice", SendVoice{ChatID: 1, Voice: "voice", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"contact", SendContact{ChatID: 1, PhoneNumber: "+10000000000", FirstName: "A", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"location", SendLocation{ChatID: 1, Latitude: 1, Longitude: 2, ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"venue", SendVenue{ChatID: 1, Latitude: 1, Longitude: 2, Title: "Venue", Address: "Address", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"message", SendMessage{ChatID: 1, Text: "text", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"animation", SendAnimation{ChatID: 1, Animation: "animation", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"audio", SendAudio{ChatID: 1, Audio: "audio", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"document", SendDocument{ChatID: 1, Document: "document", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"photo", SendPhoto{ChatID: 1, Photo: "photo", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"sticker", SendSticker{ChatID: 1, Sticker: "sticker", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"video", SendVideo{ChatID: 1, Video: "video", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"video note", SendVideoNote{ChatID: 1, VideoNote: "video-note", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"voice", SendVoice{ChatID: 1, Voice: "voice", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"contact", SendContact{ChatID: 1, PhoneNumber: "+10000000000", FirstName: "A", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"location", SendLocation{ChatID: 1, Latitude: 1, Longitude: 2, EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
{"venue", SendVenue{ChatID: 1, Latitude: 1, Longitude: 2, Title: "Venue", Address: "Address", EphemeralMessageParameters: &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}}},
}
for _, tt := range cases {
@@ -151,6 +153,19 @@ func TestEphemeralSendParametersMarshal(t *testing.T) {
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatal(err)
}
if _, old := fields["receiver_user_id"]; old {
t.Fatalf("legacy receiver field remains: %s", data)
}
if _, old := fields["callback_query_id"]; old {
t.Fatalf("legacy callback field remains: %s", data)
}
if len(fields["ephemeral_message_parameters"]) == 0 {
t.Fatalf("missing nested parameters: %s", data)
}
if !strings.Contains(string(data), `"receiver_user_id":2`) || !strings.Contains(string(data), `"callback_query_id":"callback"`) {
t.Fatalf("missing ephemeral parameters in %s", data)
}
@@ -158,6 +173,140 @@ func TestEphemeralSendParametersMarshal(t *testing.T) {
}
}
func TestEphemeralUploadParametersMarshal(t *testing.T) {
ephemeral := &EphemeralMessageParameters{ReceiverUserID: 2, CallbackQueryID: "callback"}
cases := []struct {
name string
params any
}{
{"photo", UploadPhoto{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"animation", UploadAnimation{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"audio", UploadAudio{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"document", UploadDocument{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"live photo", UploadLivePhoto{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"video", UploadVideo{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"video note", UploadVideoNote{ChatID: 1, EphemeralMessageParameters: ephemeral}},
{"voice", UploadVoice{ChatID: 1, EphemeralMessageParameters: ephemeral}},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.params)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatal(err)
}
if _, old := fields["receiver_user_id"]; old {
t.Fatalf("legacy receiver field remains: %s", data)
}
if _, old := fields["callback_query_id"]; old {
t.Fatalf("legacy callback field remains: %s", data)
}
if !strings.Contains(string(fields["ephemeral_message_parameters"]), `"receiver_user_id":2`) {
t.Fatalf("missing nested parameters: %s", data)
}
})
}
}
func TestBotAPI103KeyboardAndAdministratorFieldsMarshal(t *testing.T) {
trueValue := true
cases := []struct {
name string
value any
fields []string
}{
{"inline keyboard force reply", InlineKeyboardMarkup{ForceReply: true}, []string{`"force_reply":true`}},
{"reply keyboard force reply", ReplyKeyboardMarkup{Keyboard: [][]KeyboardButton{}, ForceReply: true}, []string{`"force_reply":true`}},
{"disabled inline button", InlineKeyboardButton{Text: "Wait", Disabled: &DisabledButton{}}, []string{`"disabled":{}`}},
{"administrator member", ChatMember{CanSendWelcomeMessages: &trueValue}, []string{`"can_send_welcome_messages":true`}},
{"administrator rights", ChatAdministratorRights{CanSendWelcomeMessages: &trueValue}, []string{`"can_send_welcome_messages":true`}},
{"promote administrator", PromoteChatMember{ChatID: 1, UserID: 2, CanSendWelcomeMessages: true}, []string{`"can_send_welcome_messages":true`}},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.value)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
for _, field := range tt.fields {
if !strings.Contains(string(data), field) {
t.Fatalf("missing %s in %s", field, data)
}
}
if strings.Contains(string(data), "melcome") {
t.Fatalf("misspelled welcome field in %s", data)
}
})
}
}
func TestMessageGenerationStoppedUpdateUnmarshal(t *testing.T) {
var update Update
err := json.Unmarshal([]byte(`{"update_id":1,"stopped_message_generation":{"chat":{"id":42,"type":"private"},"message_thread_id":3,"draft_id":7}}`), &update)
if err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if update.Type != UpdateTypeMessageGenerationStopped || update.StoppedMessageGeneration == nil {
t.Fatalf("unexpected update routing: type=%q payload=%+v", update.Type, update.StoppedMessageGeneration)
}
if update.StoppedMessageGeneration.Chat.ID != 42 || update.StoppedMessageGeneration.DraftID != 7 {
t.Fatalf("unexpected stopped-generation payload: %+v", update.StoppedMessageGeneration)
}
}
func TestBotAPI103AdditionalFieldsMarshal(t *testing.T) {
params := []struct {
name string
value any
want []string
}{
{
"ephemeral replacement",
EphemeralMessageParameters{ReceiverUserID: 2, ReplaceCallbackQueryMessage: true},
[]string{`"receiver_user_id":2`, `"replace_callback_query_message":true`},
},
{
"message draft controls",
SendMessageDraft{ChatID: 1, DraftID: 7, CanStop: true, KeepOnStop: true},
[]string{`"can_stop":true`, `"keep_on_stop":true`},
},
{
"ephemeral rich text",
EditEphemeralMessageText{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, RichMessage: &InputRichMessage{HTML: "<b>x</b>"}},
[]string{`"rich_message":{`, `"html":`},
},
{
"ephemeral caption position",
EditEphemeralMessageCaption{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, ShowCaptionAboveMedia: true},
[]string{`"show_caption_above_media":true`},
},
{
"unique gift privacy",
UniqueGiftInfo{Text: "gift", Entities: []MessageEntity{{Type: MessageEntityBold, Length: 4}}, IsPrivate: true},
[]string{`"text":"gift"`, `"entities":[`, `"is_private":true`},
},
}
for _, tt := range params {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.value)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
for _, want := range tt.want {
if !strings.Contains(string(data), want) {
t.Fatalf("missing %s in %s", want, data)
}
}
})
}
}
func TestInputPollOptionMediaLinkMarshal(t *testing.T) {
media := InputPollOptionMedia{Type: "link", URL: "https://example.com"}
data, err := json.Marshal(media)