mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-06-11 23:19:31 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 140ea13bde |
@@ -158,6 +158,7 @@ type InputPollOptionMediaType string
|
||||
|
||||
const (
|
||||
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
|
||||
InputPollOptionMediaTypeLink InputPollOptionMediaType = "link"
|
||||
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
|
||||
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
|
||||
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
|
||||
@@ -357,6 +358,14 @@ const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type Result string
|
||||
|
||||
const (
|
||||
ResultApprove Result = "approve"
|
||||
ResultDecline Result = "decline"
|
||||
ResultQueue Result = "queue"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
@@ -365,6 +374,88 @@ const (
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type RichBlockListItemType string
|
||||
|
||||
const (
|
||||
RichBlockListItemTypeLowerA RichBlockListItemType = "a"
|
||||
RichBlockListItemTypeUpperA RichBlockListItemType = "A"
|
||||
RichBlockListItemTypeLowerI RichBlockListItemType = "i"
|
||||
RichBlockListItemTypeUpperI RichBlockListItemType = "I"
|
||||
RichBlockListItemType1 RichBlockListItemType = "1"
|
||||
)
|
||||
|
||||
type RichBlockTableCellAlign string
|
||||
|
||||
const (
|
||||
RichBlockTableCellAlignLeft RichBlockTableCellAlign = "left"
|
||||
RichBlockTableCellAlignCenter RichBlockTableCellAlign = "center"
|
||||
RichBlockTableCellAlignRight RichBlockTableCellAlign = "right"
|
||||
)
|
||||
|
||||
type RichBlockTableCellValign string
|
||||
|
||||
const (
|
||||
RichBlockTableCellValignTop RichBlockTableCellValign = "top"
|
||||
RichBlockTableCellValignMiddle RichBlockTableCellValign = "middle"
|
||||
RichBlockTableCellValignBottom RichBlockTableCellValign = "bottom"
|
||||
)
|
||||
|
||||
type RichBlockType string
|
||||
|
||||
const (
|
||||
RichBlockTypeParagraph RichBlockType = "paragraph"
|
||||
RichBlockTypeHeading RichBlockType = "heading"
|
||||
RichBlockTypePre RichBlockType = "pre"
|
||||
RichBlockTypeFooter RichBlockType = "footer"
|
||||
RichBlockTypeDivider RichBlockType = "divider"
|
||||
RichBlockTypeMathematicalExpression RichBlockType = "mathematical_expression"
|
||||
RichBlockTypeAnchor RichBlockType = "anchor"
|
||||
RichBlockTypeList RichBlockType = "list"
|
||||
RichBlockTypeBlockquote RichBlockType = "blockquote"
|
||||
RichBlockTypePullquote RichBlockType = "pullquote"
|
||||
RichBlockTypeCollage RichBlockType = "collage"
|
||||
RichBlockTypeSlideshow RichBlockType = "slideshow"
|
||||
RichBlockTypeTable RichBlockType = "table"
|
||||
RichBlockTypeDetails RichBlockType = "details"
|
||||
RichBlockTypeMap RichBlockType = "map"
|
||||
RichBlockTypeAnimation RichBlockType = "animation"
|
||||
RichBlockTypeAudio RichBlockType = "audio"
|
||||
RichBlockTypePhoto RichBlockType = "photo"
|
||||
RichBlockTypeVideo RichBlockType = "video"
|
||||
RichBlockTypeVoiceNote RichBlockType = "voice_note"
|
||||
RichBlockTypeThinking RichBlockType = "thinking"
|
||||
)
|
||||
|
||||
type RichTextType string
|
||||
|
||||
const (
|
||||
RichTextTypeBold RichTextType = "bold"
|
||||
RichTextTypeItalic RichTextType = "italic"
|
||||
RichTextTypeUnderline RichTextType = "underline"
|
||||
RichTextTypeStrikethrough RichTextType = "strikethrough"
|
||||
RichTextTypeSpoiler RichTextType = "spoiler"
|
||||
RichTextTypeDateTime RichTextType = "date_time"
|
||||
RichTextTypeTextMention RichTextType = "text_mention"
|
||||
RichTextTypeSubscript RichTextType = "subscript"
|
||||
RichTextTypeSuperscript RichTextType = "superscript"
|
||||
RichTextTypeMarked RichTextType = "marked"
|
||||
RichTextTypeCode RichTextType = "code"
|
||||
RichTextTypeCustomEmoji RichTextType = "custom_emoji"
|
||||
RichTextTypeMathematicalExpression RichTextType = "mathematical_expression"
|
||||
RichTextTypeURL RichTextType = "url"
|
||||
RichTextTypeEmailAddress RichTextType = "email_address"
|
||||
RichTextTypePhoneNumber RichTextType = "phone_number"
|
||||
RichTextTypeBankCardNumber RichTextType = "bank_card_number"
|
||||
RichTextTypeMention RichTextType = "mention"
|
||||
RichTextTypeHashtag RichTextType = "hashtag"
|
||||
RichTextTypeCashtag RichTextType = "cashtag"
|
||||
RichTextTypeBotCommand RichTextType = "bot_command"
|
||||
RichTextTypeAnchor RichTextType = "anchor"
|
||||
RichTextTypeAnchorLink RichTextType = "anchor_link"
|
||||
RichTextTypeReference RichTextType = "reference"
|
||||
RichTextTypeReferenceLink RichTextType = "reference_link"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
|
||||
const (
|
||||
|
||||
+100
-6
@@ -2483,6 +2483,40 @@ func DeclineChatJoinRequest(ctx context.Context, b *client.Bot, p *DeclineChatJo
|
||||
return client.Call[*DeclineChatJoinRequestParams, bool](ctx, b, "declineChatJoinRequest", p)
|
||||
}
|
||||
|
||||
// AnswerChatJoinRequestQueryParams is the parameter set for AnswerChatJoinRequestQuery.
|
||||
//
|
||||
// Use this method to process a received chat join request query. Returns True on success.
|
||||
type AnswerChatJoinRequestQueryParams struct {
|
||||
// Unique identifier of the join request query
|
||||
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
|
||||
// Result of the query. Must be either “approve” to allow the user to join the chat, “decline” to disallow the user to join the chat, or “queue” to leave the decision to other administrators.
|
||||
Result Result `json:"result"`
|
||||
}
|
||||
|
||||
// AnswerChatJoinRequestQuery calls the answerChatJoinRequestQuery Telegram Bot API method.
|
||||
//
|
||||
// Use this method to process a received chat join request query. Returns True on success.
|
||||
func AnswerChatJoinRequestQuery(ctx context.Context, b *client.Bot, p *AnswerChatJoinRequestQueryParams) (bool, error) {
|
||||
return client.Call[*AnswerChatJoinRequestQueryParams, bool](ctx, b, "answerChatJoinRequestQuery", p)
|
||||
}
|
||||
|
||||
// SendChatJoinRequestWebAppParams is the parameter set for SendChatJoinRequestWebApp.
|
||||
//
|
||||
// Use this method to process a received chat join request query by showing a Mini App to the user before deciding the outcome. Returns True on success.
|
||||
type SendChatJoinRequestWebAppParams struct {
|
||||
// Unique identifier of the join request query
|
||||
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
|
||||
// The URL of the Mini App to be opened
|
||||
WebAppURL string `json:"web_app_url"`
|
||||
}
|
||||
|
||||
// SendChatJoinRequestWebApp calls the sendChatJoinRequestWebApp Telegram Bot API method.
|
||||
//
|
||||
// Use this method to process a received chat join request query by showing a Mini App to the user before deciding the outcome. Returns True on success.
|
||||
func SendChatJoinRequestWebApp(ctx context.Context, b *client.Bot, p *SendChatJoinRequestWebAppParams) (bool, error) {
|
||||
return client.Call[*SendChatJoinRequestWebAppParams, bool](ctx, b, "sendChatJoinRequestWebApp", p)
|
||||
}
|
||||
|
||||
// SetChatPhotoParams is the parameter set for SetChatPhoto.
|
||||
//
|
||||
// Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate administrator rights. Returns True on success.
|
||||
@@ -4004,7 +4038,7 @@ func SavePreparedKeyboardButton(ctx context.Context, b *client.Bot, p *SavePrepa
|
||||
|
||||
// EditMessageTextParams is the parameter set for EditMessageText.
|
||||
//
|
||||
// Use this method to edit text and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit text, rich and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
type EditMessageTextParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message to be edited was sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
@@ -4014,21 +4048,23 @@ type EditMessageTextParams struct {
|
||||
MessageID *int64 `json:"message_id,omitempty"`
|
||||
// Required if chat_id and message_id are not specified. Identifier of the inline message.
|
||||
InlineMessageID string `json:"inline_message_id,omitempty"`
|
||||
// New text of the message, 1-4096 characters after entities parsing
|
||||
Text string `json:"text"`
|
||||
// New text of the message, 1-4096 characters after entity parsing; required if rich_message isn't specified
|
||||
Text string `json:"text,omitempty"`
|
||||
// Mode for parsing entities in the message text. See formatting options for more details.
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
// A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode
|
||||
Entities []MessageEntity `json:"entities,omitempty"`
|
||||
// Link preview generation options for the message
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
// New rich content of the message; required if text isn't specified
|
||||
RichMessage *InputRichMessage `json:"rich_message,omitempty"`
|
||||
// A JSON-serialized object for an inline keyboard
|
||||
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// EditMessageText calls the editMessageText Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit text and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit text, rich and game messages. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
func EditMessageText(ctx context.Context, b *client.Bot, p *EditMessageTextParams) (*MessageOrBool, error) {
|
||||
return client.Call[*EditMessageTextParams, *MessageOrBool](ctx, b, "editMessageText", p)
|
||||
}
|
||||
@@ -4066,7 +4102,7 @@ func EditMessageCaption(ctx context.Context, b *client.Bot, p *EditMessageCaptio
|
||||
|
||||
// EditMessageMediaParams is the parameter set for EditMessageMedia.
|
||||
//
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to add media to text messages. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
type EditMessageMediaParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message to be edited was sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
@@ -4121,7 +4157,7 @@ func (p *EditMessageMediaParams) MultipartFiles() []client.MultipartFile {
|
||||
|
||||
// EditMessageMedia calls the editMessageMedia Telegram Bot API method.
|
||||
//
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to add media to text messages. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
// Use this method to edit animation, audio, document, live photo, photo, or video messages, or to replace a text or a rich message with a media. If a message is part of a message album, then it can be edited only to an audio for audio albums, only to a document for document albums and to a photo, a live photo, or a video otherwise. When an inline message is edited, a new file can't be uploaded; use a previously uploaded file via its file_id or specify a URL. On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned. Note that business messages that were not sent by the bot and do not contain an inline keyboard can only be edited within 48 hours from the time they were sent.
|
||||
func EditMessageMedia(ctx context.Context, b *client.Bot, p *EditMessageMediaParams) (*MessageOrBool, error) {
|
||||
return client.Call[*EditMessageMediaParams, *MessageOrBool](ctx, b, "editMessageMedia", p)
|
||||
}
|
||||
@@ -4795,6 +4831,64 @@ func DeleteStickerSet(ctx context.Context, b *client.Bot, p *DeleteStickerSetPar
|
||||
return client.Call[*DeleteStickerSetParams, bool](ctx, b, "deleteStickerSet", p)
|
||||
}
|
||||
|
||||
// SendRichMessageParams is the parameter set for SendRichMessage.
|
||||
//
|
||||
// Use this method to send rich messages. If the message contains a block with a media element, then the bot must have the right to send the media to the chat. On success, the sent Message is returned.
|
||||
type SendRichMessageParams struct {
|
||||
// Unique identifier of the business connection on behalf of which the message will be sent
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
// Unique identifier for the target chat or username of the target bot, supergroup or channel in the format @username
|
||||
ChatID ChatID `json:"chat_id"`
|
||||
// Unique identifier for the target message thread (topic) of a forum; for forum supergroups and private chats of bots with forum topic mode enabled only
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a direct messages chat
|
||||
DirectMessagesTopicID *int64 `json:"direct_messages_topic_id,omitempty"`
|
||||
// The message to be sent
|
||||
RichMessage InputRichMessage `json:"rich_message"`
|
||||
// Sends the message silently. Users will receive a notification with no sound.
|
||||
DisableNotification *bool `json:"disable_notification,omitempty"`
|
||||
// Protects the contents of the sent message from forwarding and saving
|
||||
ProtectContent *bool `json:"protect_content,omitempty"`
|
||||
// Pass True to allow up to 1000 messages per second, ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.
|
||||
AllowPaidBroadcast *bool `json:"allow_paid_broadcast,omitempty"`
|
||||
// Unique identifier of the message effect to be added to the message; for private chats only
|
||||
MessageEffectID string `json:"message_effect_id,omitempty"`
|
||||
// A JSON-serialized object containing the parameters of the suggested post to send; for direct messages chats only. If the message is sent as a reply to another suggested post, then that suggested post is automatically declined.
|
||||
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
|
||||
// Description of the message to reply to
|
||||
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
|
||||
// Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user.
|
||||
ReplyMarkup any `json:"reply_markup,omitempty"`
|
||||
}
|
||||
|
||||
// SendRichMessage calls the sendRichMessage Telegram Bot API method.
|
||||
//
|
||||
// Use this method to send rich messages. If the message contains a block with a media element, then the bot must have the right to send the media to the chat. On success, the sent Message is returned.
|
||||
func SendRichMessage(ctx context.Context, b *client.Bot, p *SendRichMessageParams) (*Message, error) {
|
||||
return client.Call[*SendRichMessageParams, *Message](ctx, b, "sendRichMessage", p)
|
||||
}
|
||||
|
||||
// SendRichMessageDraftParams is the parameter set for SendRichMessageDraft.
|
||||
//
|
||||
// Use this method to stream a partial rich message to a user while the message is being generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second preview - once the output is finalized, you must call sendRichMessage with the complete message to persist it in the user's chat. Returns True on success.
|
||||
type SendRichMessageDraftParams struct {
|
||||
// Unique identifier for the target private chat
|
||||
ChatID int64 `json:"chat_id"`
|
||||
// Unique identifier for the target message thread
|
||||
MessageThreadID *int64 `json:"message_thread_id,omitempty"`
|
||||
// Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated.
|
||||
DraftID int64 `json:"draft_id"`
|
||||
// The partial message to be streamed
|
||||
RichMessage InputRichMessage `json:"rich_message"`
|
||||
}
|
||||
|
||||
// SendRichMessageDraft calls the sendRichMessageDraft Telegram Bot API method.
|
||||
//
|
||||
// Use this method to stream a partial rich message to a user while the message is being generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second preview - once the output is finalized, you must call sendRichMessage with the complete message to persist it in the user's chat. Returns True on success.
|
||||
func SendRichMessageDraft(ctx context.Context, b *client.Bot, p *SendRichMessageDraftParams) (bool, error) {
|
||||
return client.Call[*SendRichMessageDraftParams, bool](ctx, b, "sendRichMessageDraft", p)
|
||||
}
|
||||
|
||||
// AnswerInlineQueryParams is the parameter set for AnswerInlineQuery.
|
||||
//
|
||||
// Use this method to send answers to an inline query. On success, True is returned.No more than 50 results per query are allowed.
|
||||
|
||||
+590
-21
@@ -7427,6 +7427,294 @@ func Test_DeclineChatJoinRequest_ServerError(t *testing.T) {
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/answerChatJoinRequestQuery")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_AnswerChatJoinRequestQuery_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_AnswerChatJoinRequestQuery_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_AnswerChatJoinRequestQuery_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, &AnswerChatJoinRequestQueryParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_AnswerChatJoinRequestQuery_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_AnswerChatJoinRequestQuery_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_AnswerChatJoinRequestQuery_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_AnswerChatJoinRequestQuery_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &AnswerChatJoinRequestQueryParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
Result: ResultApprove,
|
||||
}
|
||||
_, err := AnswerChatJoinRequestQuery(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/sendChatJoinRequestWebApp")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_SendChatJoinRequestWebApp_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_SendChatJoinRequestWebApp_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_SendChatJoinRequestWebApp_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, &SendChatJoinRequestWebAppParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendChatJoinRequestWebApp_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_SendChatJoinRequestWebApp_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendChatJoinRequestWebApp_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_SendChatJoinRequestWebApp_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendChatJoinRequestWebAppParams{
|
||||
ChatJoinRequestQueryID: "test_value",
|
||||
WebAppURL: "test_value",
|
||||
}
|
||||
_, err := SendChatJoinRequestWebApp(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SetChatPhoto_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
@@ -18587,9 +18875,7 @@ func Test_EditMessageText_Success(t *testing.T) {
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -18600,9 +18886,7 @@ func Test_EditMessageText_APIError(t *testing.T) {
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
@@ -18616,9 +18900,7 @@ func Test_EditMessageText_NetworkError(t *testing.T) {
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
@@ -18630,9 +18912,7 @@ func Test_EditMessageText_ParseError(t *testing.T) {
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
@@ -18647,9 +18927,7 @@ func Test_EditMessageText_ContextCanceled(t *testing.T) {
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
@@ -18686,9 +18964,7 @@ func Test_EditMessageText_Forbidden(t *testing.T) {
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
@@ -18706,9 +18982,7 @@ func Test_EditMessageText_ServerError(t *testing.T) {
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &EditMessageTextParams{
|
||||
Text: "test_value",
|
||||
}
|
||||
params := &EditMessageTextParams{}
|
||||
_, err := EditMessageText(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
@@ -22830,6 +23104,301 @@ func Test_DeleteStickerSet_ServerError(t *testing.T) {
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/sendRichMessage")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":{}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_SendRichMessage_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_SendRichMessage_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_SendRichMessage_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := SendRichMessage(context.Background(), bot, &SendRichMessageParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessage_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_SendRichMessage_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessage_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_SendRichMessage_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageParams{
|
||||
ChatID: ChatIDFromInt(123),
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessage(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
return strings.HasSuffix(r.URL.Path, "/sendRichMessageDraft")
|
||||
})).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_APIError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 429, ae.Code)
|
||||
require.True(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_NetworkError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ne *client.NetworkError
|
||||
require.ErrorAs(t, err, &ne)
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_ParseError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var pe *client.ParseError
|
||||
require.ErrorAs(t, err, &pe)
|
||||
}
|
||||
|
||||
func Test_SendRichMessageDraft_ContextCanceled(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(nil, context.Canceled).Maybe()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(ctx, bot, params)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
|
||||
// Test_SendRichMessageDraft_MissingRequiredFields exercises Telegram's server-side
|
||||
// validation: when a required field is omitted, Telegram returns 400 with
|
||||
// a description like "Bad Request: <field> is empty". The library must
|
||||
// surface this as *APIError with the ErrBadRequest sentinel.
|
||||
func Test_SendRichMessageDraft_MissingRequiredFields(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
// Send a Params with all required fields zeroed — simulates a caller
|
||||
// that forgot to populate them. The bot library marshals as-is and
|
||||
// surfaces Telegram's 400 reply.
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, &SendRichMessageDraftParams{})
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 400, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrBadRequest))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessageDraft_Forbidden exercises the 403 path (bot blocked by user,
|
||||
// removed from chat, etc.). The library must surface the ErrForbidden
|
||||
// sentinel.
|
||||
func Test_SendRichMessageDraft_Forbidden(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 403, ae.Code)
|
||||
require.True(t, errors.Is(err, client.ErrForbidden))
|
||||
require.False(t, ae.IsRetryable())
|
||||
}
|
||||
|
||||
// Test_SendRichMessageDraft_ServerError exercises the 5xx path. The library must
|
||||
// classify these as retryable so RetryDoer / user retry logic kicks in.
|
||||
func Test_SendRichMessageDraft_ServerError(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.Anything).Return(
|
||||
genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
|
||||
|
||||
bot := client.New("test:token", client.WithHTTPClient(m))
|
||||
params := &SendRichMessageDraftParams{
|
||||
ChatID: 42,
|
||||
DraftID: 42,
|
||||
RichMessage: InputRichMessage{},
|
||||
}
|
||||
_, err := SendRichMessageDraft(context.Background(), bot, params)
|
||||
require.Error(t, err)
|
||||
var ae *client.APIError
|
||||
require.ErrorAs(t, err, &ae)
|
||||
require.Equal(t, 500, ae.Code)
|
||||
require.True(t, ae.IsRetryable(), "5xx must be retryable")
|
||||
}
|
||||
|
||||
func Test_AnswerInlineQuery_Success(t *testing.T) {
|
||||
m := &genTestMockDoer{}
|
||||
m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
|
||||
|
||||
+1491
-3
File diff suppressed because it is too large
Load Diff
@@ -420,7 +420,7 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return goField(plan, parent, f)
|
||||
},
|
||||
"goFieldP": func(methodName string, f spec.Field) string {
|
||||
return goFieldX(plan, "", title(methodName)+"Params", f)
|
||||
return goFieldX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
@@ -432,7 +432,7 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return multipartFieldEntry(plan, parent, f)
|
||||
},
|
||||
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
|
||||
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
|
||||
return multipartFieldEntryX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
@@ -915,15 +915,15 @@ func buildUnionTypeSet(api *spec.API) map[string]bool {
|
||||
// used in generated test param literals. plan supplies typed-enum names
|
||||
// so a method-param sentinel for a ParseMode field becomes a typed
|
||||
// constant rather than a magic string.
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(spec.Field) string {
|
||||
return func(f spec.Field) string {
|
||||
return sentinelForField(f, unionTypes, plan)
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(string, spec.Field) string {
|
||||
return func(methodName string, f spec.Field) string {
|
||||
return sentinelForField(methodName, f, unionTypes, plan)
|
||||
}
|
||||
}
|
||||
|
||||
func sentinelForField(f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum("", f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return constName(name, f.EnumValues[0])
|
||||
func sentinelForField(methodName string, f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum(methodEnumParent(methodName), f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return plan.ConstFor(name, f.EnumValues[0])
|
||||
}
|
||||
tr := f.Type
|
||||
switch tr.Kind {
|
||||
|
||||
+73
-10
@@ -26,15 +26,21 @@ type enumPlan struct {
|
||||
}
|
||||
|
||||
// enumKey identifies a single Field occurrence so the emitter can look
|
||||
// up the enum name later. Parent is "" for method params (the method
|
||||
// doesn't share a Go type with the field).
|
||||
// up the enum name later. Parent is the type name for struct fields and
|
||||
// methodEnumParent(name) for method params, so two methods sharing a
|
||||
// param name never alias each other's enum (or non-enum) fields.
|
||||
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }
|
||||
|
||||
// methodEnumParent is the enum-plan key parent for a method's params.
|
||||
// The "method:" prefix keeps it disjoint from type names.
|
||||
func methodEnumParent(methodName string) string { return "method:" + methodName }
|
||||
|
||||
// planEnums walks the IR, decides on enum names, deduplicates, and
|
||||
// returns an enumPlan. All scraper-marked enum fields are covered.
|
||||
func planEnums(api *spec.API) *enumPlan {
|
||||
type ref struct {
|
||||
parent string
|
||||
parent string // naming parent ("" for method params)
|
||||
keyParent string // byField key parent (methodEnumParent(...) for method params)
|
||||
fieldName string
|
||||
jsonName string
|
||||
values []string
|
||||
@@ -52,7 +58,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
|
||||
var refs []ref
|
||||
collect := func(parent string, fields []spec.Field) {
|
||||
collect := func(parent, keyParent string, fields []spec.Field) {
|
||||
for _, f := range fields {
|
||||
if len(f.EnumValues) == 0 {
|
||||
continue
|
||||
@@ -62,6 +68,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
refs = append(refs, ref{
|
||||
parent: parent,
|
||||
keyParent: keyParent,
|
||||
fieldName: f.Name,
|
||||
jsonName: f.JSONName,
|
||||
values: f.EnumValues,
|
||||
@@ -70,13 +77,15 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
collect(t.Name, t.Fields)
|
||||
collect(t.Name, t.Name, t.Fields)
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
// Method params have no shared Go parent type, so we pass "" as
|
||||
// the parent. The default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums.
|
||||
collect("", m.Params)
|
||||
// Method params have no shared Go parent type, so the naming
|
||||
// parent is "" (the default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums), but the byField key
|
||||
// is method-scoped so a same-named non-enum param on another
|
||||
// method can never pick up this enum.
|
||||
collect("", methodEnumParent(m.Name), m.Params)
|
||||
}
|
||||
|
||||
// candidate name per ref (before collision resolution)
|
||||
@@ -196,7 +205,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
for i, r := range refs {
|
||||
name := groups[r.valueKey].name
|
||||
plan.byField[enumKey(r.parent, r.fieldName)] = name
|
||||
plan.byField[enumKey(r.keyParent, r.fieldName)] = name
|
||||
_ = i
|
||||
}
|
||||
for vk, g := range groups {
|
||||
@@ -389,6 +398,18 @@ func (p *enumPlan) FieldEnum(parent, fieldName string) string {
|
||||
return p.byField[enumKey(parent, fieldName)]
|
||||
}
|
||||
|
||||
// ConstFor returns the collision-resolved constant identifier for value
|
||||
// within the named enum declaration. Falls back to the plain constName
|
||||
// when the declaration is unknown (unit tests with partial plans).
|
||||
func (p *enumPlan) ConstFor(enumName, value string) string {
|
||||
if p != nil {
|
||||
if d, ok := p.decls[enumName]; ok {
|
||||
return d.ConstName(value)
|
||||
}
|
||||
}
|
||||
return constName(enumName, value)
|
||||
}
|
||||
|
||||
// defaultEnumName picks an initial Go enum name for a field. parse_mode
|
||||
// fields collapse to the canonical "ParseMode"; otherwise the name is
|
||||
// parent + PascalCase(jsonName).
|
||||
@@ -406,6 +427,48 @@ func constName(enumName, value string) string {
|
||||
return enumName + valuePascal(value)
|
||||
}
|
||||
|
||||
// ConstName returns the constant identifier for value within this enum,
|
||||
// resolving case-collisions between values that Pascal-case to the same
|
||||
// identifier (e.g. RichBlockListItem.type values "a" and "A"). Colliding
|
||||
// values get a Lower/Upper prefix on the value part based on the case of
|
||||
// the value's first letter; any residual collision gets a numeric suffix.
|
||||
func (e enumDecl) ConstName(value string) string {
|
||||
counts := map[string]int{}
|
||||
for _, v := range e.Values {
|
||||
counts[constName(e.Name, v)]++
|
||||
}
|
||||
plain := constName(e.Name, value)
|
||||
if counts[plain] <= 1 {
|
||||
return plain
|
||||
}
|
||||
cased := e.Name + casePrefix(value) + valuePascal(value)
|
||||
// Residual collision (same value text repeated, or Lower/Upper still
|
||||
// ambiguous): append the value's 1-based position for determinism.
|
||||
casedCounts := map[string]int{}
|
||||
pos := 0
|
||||
for i, v := range e.Values {
|
||||
if counts[constName(e.Name, v)] > 1 {
|
||||
casedCounts[e.Name+casePrefix(v)+valuePascal(v)]++
|
||||
}
|
||||
if v == value {
|
||||
pos = i + 1
|
||||
}
|
||||
}
|
||||
if casedCounts[cased] > 1 {
|
||||
return cased + itoa(pos)
|
||||
}
|
||||
return cased
|
||||
}
|
||||
|
||||
// casePrefix distinguishes case-colliding enum values: "Lower" when the
|
||||
// value starts with a lowercase letter, "Upper" otherwise.
|
||||
func casePrefix(v string) string {
|
||||
if v != "" && v[0] >= 'a' && v[0] <= 'z' {
|
||||
return "Lower"
|
||||
}
|
||||
return "Upper"
|
||||
}
|
||||
|
||||
func valuePascal(v string) string {
|
||||
// "image/jpeg" → "ImageOfJpeg"
|
||||
parts := strings.Split(v, "/")
|
||||
|
||||
@@ -8,6 +8,6 @@ package api
|
||||
type {{$e.Name}} string
|
||||
|
||||
const (
|
||||
{{range $v := $e.Values}} {{enumConstName $e.Name $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{range $v := $e.Values}} {{$e.ConstName $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{end}})
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression: Bot API 9.x RichBlockListItem.type enumerates "a"/"A"/"i"/"I"/"1",
|
||||
// which Pascal-case to the same identifier. ConstName must disambiguate.
|
||||
func TestEnumDecl_ConstName_CaseCollision(t *testing.T) {
|
||||
d := enumDecl{Name: "RichBlockListItemType", Values: []string{"a", "A", "i", "I", "1"}}
|
||||
got := map[string]bool{}
|
||||
for _, v := range d.Values {
|
||||
n := d.ConstName(v)
|
||||
require.False(t, got[n], "duplicate const ident %q for value %q", n, v)
|
||||
got[n] = true
|
||||
}
|
||||
require.Equal(t, "RichBlockListItemTypeLowerA", d.ConstName("a"))
|
||||
require.Equal(t, "RichBlockListItemTypeUpperA", d.ConstName("A"))
|
||||
// Non-colliding values keep the plain name.
|
||||
require.Equal(t, "RichBlockListItemType1", d.ConstName("1"))
|
||||
}
|
||||
|
||||
// Regression: answerChatJoinRequestQuery has an enum param named "result";
|
||||
// answerGuestQuery has a NON-enum param also named "result". The enum plan
|
||||
// must scope method params per method so the enum never leaks onto the
|
||||
// other method's field.
|
||||
func TestPlanEnums_MethodParamsScopedPerMethod(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Methods: []spec.MethodDecl{
|
||||
{
|
||||
Name: "answerChatJoinRequestQuery",
|
||||
Params: []spec.Field{{
|
||||
Name: "Result", JSONName: "result", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"},
|
||||
EnumValues: []string{"approve", "decline", "queue"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Name: "answerGuestQuery",
|
||||
Params: []spec.Field{{
|
||||
Name: "Result", JSONName: "result", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindNamed, Name: "InlineQueryResult"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
plan := planEnums(api)
|
||||
require.Equal(t, "Result",
|
||||
plan.FieldEnum(methodEnumParent("answerChatJoinRequestQuery"), "Result"))
|
||||
require.Empty(t,
|
||||
plan.FieldEnum(methodEnumParent("answerGuestQuery"), "Result"),
|
||||
"non-enum param must not inherit another method's enum")
|
||||
}
|
||||
@@ -574,7 +574,7 @@ func TestSentinelForField(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := sentinelForField(c.field, unionTypes, nil)
|
||||
got := sentinelForField("testMethod", c.field, unionTypes, nil)
|
||||
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func Test_{{$mName}}_Success(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -67,7 +67,7 @@ func Test_{{$mName}}_APIError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -89,7 +89,7 @@ func Test_{{$mName}}_NetworkError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -109,7 +109,7 @@ func Test_{{$mName}}_ParseError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -132,7 +132,7 @@ func Test_{{$mName}}_ContextCanceled(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(ctx, bot, params)
|
||||
@@ -177,7 +177,7 @@ func Test_{{$mName}}_Forbidden(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -203,7 +203,7 @@ func Test_{{$mName}}_ServerError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
|
||||
+2497
-734
File diff suppressed because it is too large
Load Diff
+2373
-8
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
@@ -1 +1 @@
|
||||
snapshot_2026-06-01.html
|
||||
snapshot_2026-06-11.html
|
||||
+21862
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user