package tgapi import ( "encoding/json" "strings" "testing" ) func TestEditMessageTextMarshalsInputRichMessage(t *testing.T) { params := EditMessageText{ ChatID: 1, MessageID: 2, RichMessage: &InputRichMessage{ HTML: "
hi
", SkipEntityDetection: true, }, } data, err := json.Marshal(params) if err != nil { t.Fatalf("Marshal returned error: %v", err) } got := string(data) for _, want := range []string{`"rich_message":{"html":`, `"skip_entity_detection":true`} { if !strings.Contains(got, want) { t.Fatalf("missing %s in editMessageText JSON: %s", want, got) } } if strings.Contains(got, `"blocks"`) { t.Fatalf("rich_message must be an InputRichMessage, not a block tree: %s", got) } if strings.Contains(got, `"text"`) { t.Fatalf("empty text must be omitted when editing rich content: %s", got) } } func TestSendRichMessageDraftMarshal(t *testing.T) { params := SendRichMessageDraft{ 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*"}`, `"can_stop":true`, `"keep_on_stop":true`} { if !strings.Contains(got, want) { t.Fatalf("missing %s in sendRichMessageDraft JSON: %s", want, got) } } } func TestInputRichMessageContentMarshal(t *testing.T) { content := InputRichMessageContent{ RichMessage: InputRichMessage{HTML: "hi
"}, } data, err := json.Marshal(content) if err != nil { t.Fatalf("Marshal returned error: %v", err) } if got := string(data); !strings.Contains(got, `"rich_message":{"html":`) { t.Fatalf("unexpected InputRichMessageContent JSON: %s", got) } } func TestInputRichMessageMediaMarshal(t *testing.T) { message := InputRichMessage{ HTML: ``, Media: []InputRichMessageMedia{{ ID: "intro", Media: InputMedia{Type: InputMediaTypeVideo, Media: "attach://intro"}, }}, } data, err := json.Marshal(message) if err != nil { t.Fatalf("Marshal returned error: %v", err) } var got struct { Media []struct { ID string `json:"id"` Media InputMedia `json:"media"` } `json:"media"` } if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("Unmarshal returned error: %v", err) } if len(got.Media) != 1 || got.Media[0].ID != "intro" { t.Fatalf("unexpected media: %+v", got.Media) } if got.Media[0].Media.Type != InputMediaTypeVideo || got.Media[0].Media.Media != "attach://intro" { t.Fatalf("unexpected embedded media: %+v", got.Media[0].Media) } } func TestEphemeralMethodsMarshalReceiverUserID(t *testing.T) { cases := []struct { name string params any }{ {"edit text", EditEphemeralMessageText{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, Text: "updated"}}, {"edit media", EditEphemeralMessageMedia{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, Media: InputMedia{Type: InputMediaTypePhoto, Media: "photo-id"}}}, {"edit caption", EditEphemeralMessageCaption{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, Caption: "updated"}}, {"edit markup", EditEphemeralMessageReplyMarkup{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3}}, {"delete", DeleteEphemeralMessage{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3}}, } 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.Fatalf("Unmarshal returned error: %v", err) } if _, ok := fields["receiver_user_id"]; !ok { t.Fatalf("receiver_user_id is missing from %s", data) } if _, ok := fields["reciever_user_id"]; ok { t.Fatalf("misspelled receiver_user_id is present in %s", data) } }) } } func TestEphemeralSendParametersMarshal(t *testing.T) { cases := []struct { name string params any }{ {"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 { 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 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) } }) } } 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: "x"}}, []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) if err != nil { t.Fatalf("Marshal returned error: %v", err) } got := string(data) if got != `{"type":"link","url":"https://example.com"}` { t.Fatalf("unexpected link media JSON: %s", got) } } func TestPollMediaUnmarshalLink(t *testing.T) { var media PollMedia if err := json.Unmarshal([]byte(`{"link":{"url":"https://example.com"}}`), &media); err != nil { t.Fatalf("Unmarshal returned error: %v", err) } if media.Link == nil || media.Link.URL != "https://example.com" { t.Fatalf("unexpected poll media link: %+v", media.Link) } } func TestChatJoinRequestUnmarshalQueryID(t *testing.T) { payload := `{"chat":{"id":1},"from":{"id":2,"first_name":"A"},"user_chat_id":2,"date":3,"query_id":"q42"}` var req ChatJoinRequest if err := json.Unmarshal([]byte(payload), &req); err != nil { t.Fatalf("Unmarshal returned error: %v", err) } if req.QueryID == nil || *req.QueryID != "q42" { t.Fatalf("unexpected query_id: %+v", req.QueryID) } } func TestAnswerChatJoinRequestQueryResultValues(t *testing.T) { if JoinRequestApprove != "approve" || JoinRequestDecline != "decline" || JoinRequestQueue != "queue" { t.Fatalf("unexpected join request query result values: %q %q %q", JoinRequestApprove, JoinRequestDecline, JoinRequestQueue) } } func TestUserUnmarshalSupportsJoinRequestQueries(t *testing.T) { var user User if err := json.Unmarshal([]byte(`{"id":1,"first_name":"A","supports_join_request_queries":true}`), &user); err != nil { t.Fatalf("Unmarshal returned error: %v", err) } if user.SupportsJoinRequestQueries == nil || !*user.SupportsJoinRequestQueries { t.Fatalf("unexpected supports_join_request_queries: %+v", user.SupportsJoinRequestQueries) } }