Compare commits

...

1 Commits

Author SHA1 Message Date
lukaszraczylo 140ea13bde fix(genapi): scope method-param enums per method, dedupe case-colliding enum consts
Bot API update added RichBlockListItem.type values (a/A/i/I/1) that
Pascal-case to identical const idents, and answerGuestQuery's non-enum
'result' param picked up answerChatJoinRequestQuery's Result enum via
the shared parent="" enum-plan key.

- key method params as method:<name> in the enum plan byField map
- enumDecl.ConstName resolves case collisions with Lower/Upper prefix
- regenerate api/ from 2026-06-11 snapshot
2026-06-12 00:03:45 +01:00
14 changed files with 29151 additions and 800 deletions
+91
View File
@@ -158,6 +158,7 @@ type InputPollOptionMediaType string
const ( const (
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation" InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
InputPollOptionMediaTypeLink InputPollOptionMediaType = "link"
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo" InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location" InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo" InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
@@ -357,6 +358,14 @@ const (
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR" RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
) )
type Result string
const (
ResultApprove Result = "approve"
ResultDecline Result = "decline"
ResultQueue Result = "queue"
)
type RevenueWithdrawalStateKind string type RevenueWithdrawalStateKind string
const ( const (
@@ -365,6 +374,88 @@ const (
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed" 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 type StickerType string
const ( const (
+100 -6
View File
@@ -2483,6 +2483,40 @@ func DeclineChatJoinRequest(ctx context.Context, b *client.Bot, p *DeclineChatJo
return client.Call[*DeclineChatJoinRequestParams, bool](ctx, b, "declineChatJoinRequest", p) 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. // 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. // 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. // 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 { type EditMessageTextParams struct {
// Unique identifier of the business connection on behalf of which the message to be edited was sent // Unique identifier of the business connection on behalf of which the message to be edited was sent
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
@@ -4014,21 +4048,23 @@ type EditMessageTextParams struct {
MessageID *int64 `json:"message_id,omitempty"` MessageID *int64 `json:"message_id,omitempty"`
// Required if chat_id and message_id are not specified. Identifier of the inline message. // Required if chat_id and message_id are not specified. Identifier of the inline message.
InlineMessageID string `json:"inline_message_id,omitempty"` InlineMessageID string `json:"inline_message_id,omitempty"`
// New text of the message, 1-4096 characters after entities parsing // New text of the message, 1-4096 characters after entity parsing; required if rich_message isn't specified
Text string `json:"text"` Text string `json:"text,omitempty"`
// Mode for parsing entities in the message text. See formatting options for more details. // Mode for parsing entities in the message text. See formatting options for more details.
ParseMode ParseMode `json:"parse_mode,omitempty"` 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 // 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"` Entities []MessageEntity `json:"entities,omitempty"`
// Link preview generation options for the message // Link preview generation options for the message
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` 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 // A JSON-serialized object for an inline keyboard
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }
// EditMessageText calls the editMessageText Telegram Bot API method. // 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) { func EditMessageText(ctx context.Context, b *client.Bot, p *EditMessageTextParams) (*MessageOrBool, error) {
return client.Call[*EditMessageTextParams, *MessageOrBool](ctx, b, "editMessageText", p) 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. // 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 { type EditMessageMediaParams struct {
// Unique identifier of the business connection on behalf of which the message to be edited was sent // Unique identifier of the business connection on behalf of which the message to be edited was sent
BusinessConnectionID string `json:"business_connection_id,omitempty"` 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. // 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) { func EditMessageMedia(ctx context.Context, b *client.Bot, p *EditMessageMediaParams) (*MessageOrBool, error) {
return client.Call[*EditMessageMediaParams, *MessageOrBool](ctx, b, "editMessageMedia", p) 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) 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. // 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. // 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
View File
@@ -7427,6 +7427,294 @@ func Test_DeclineChatJoinRequest_ServerError(t *testing.T) {
require.True(t, ae.IsRetryable(), "5xx must be retryable") 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) { func Test_SetChatPhoto_Success(t *testing.T) {
m := &genTestMockDoer{} m := &genTestMockDoer{}
m.On("Do", mock.MatchedBy(func(r *http.Request) bool { 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) })).Return(genTestResp(200, `{"ok":true,"result":true}`), nil)
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(context.Background(), bot, params) _, err := EditMessageText(context.Background(), bot, params)
require.NoError(t, err) 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) genTestResp(200, `{"ok":false,"error_code":429,"description":"Too Many Requests","parameters":{"retry_after":1}}`), nil)
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(context.Background(), bot, params) _, err := EditMessageText(context.Background(), bot, params)
require.Error(t, err) require.Error(t, err)
var ae *client.APIError 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")) m.On("Do", mock.Anything).Return(nil, errors.New("dial tcp: timeout"))
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(context.Background(), bot, params) _, err := EditMessageText(context.Background(), bot, params)
require.Error(t, err) require.Error(t, err)
var ne *client.NetworkError 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) m.On("Do", mock.Anything).Return(genTestResp(200, `not json`), nil)
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(context.Background(), bot, params) _, err := EditMessageText(context.Background(), bot, params)
require.Error(t, err) require.Error(t, err)
var pe *client.ParseError var pe *client.ParseError
@@ -18647,9 +18927,7 @@ func Test_EditMessageText_ContextCanceled(t *testing.T) {
cancel() cancel()
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(ctx, bot, params) _, err := EditMessageText(ctx, bot, params)
require.Error(t, err) require.Error(t, err)
require.ErrorIs(t, err, context.Canceled) 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) genTestResp(200, `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`), nil)
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(context.Background(), bot, params) _, err := EditMessageText(context.Background(), bot, params)
require.Error(t, err) require.Error(t, err)
var ae *client.APIError 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) genTestResp(200, `{"ok":false,"error_code":500,"description":"Internal server error"}`), nil)
bot := client.New("test:token", client.WithHTTPClient(m)) bot := client.New("test:token", client.WithHTTPClient(m))
params := &EditMessageTextParams{ params := &EditMessageTextParams{}
Text: "test_value",
}
_, err := EditMessageText(context.Background(), bot, params) _, err := EditMessageText(context.Background(), bot, params)
require.Error(t, err) require.Error(t, err)
var ae *client.APIError var ae *client.APIError
@@ -22830,6 +23104,301 @@ func Test_DeleteStickerSet_ServerError(t *testing.T) {
require.True(t, ae.IsRetryable(), "5xx must be retryable") 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) { func Test_AnswerInlineQuery_Success(t *testing.T) {
m := &genTestMockDoer{} m := &genTestMockDoer{}
m.On("Do", mock.MatchedBy(func(r *http.Request) bool { m.On("Do", mock.MatchedBy(func(r *http.Request) bool {
+1491 -3
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -420,7 +420,7 @@ func funcs(plan *enumPlan) template.FuncMap {
return goField(plan, parent, f) return goField(plan, parent, f)
}, },
"goFieldP": func(methodName string, f spec.Field) string { "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, "docComment": docComment,
"isOptional": func(f spec.Field) bool { return !f.Required }, "isOptional": func(f spec.Field) bool { return !f.Required },
@@ -432,7 +432,7 @@ func funcs(plan *enumPlan) template.FuncMap {
return multipartFieldEntry(plan, parent, f) return multipartFieldEntry(plan, parent, f)
}, },
"multipartFieldEntryP": func(methodName string, f spec.Field) string { "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, "multipartFileEntry": multipartFileEntry,
"returnGoType": returnGoType, "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 // used in generated test param literals. plan supplies typed-enum names
// so a method-param sentinel for a ParseMode field becomes a typed // so a method-param sentinel for a ParseMode field becomes a typed
// constant rather than a magic string. // constant rather than a magic string.
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(spec.Field) string { func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(string, spec.Field) string {
return func(f spec.Field) string { return func(methodName string, f spec.Field) string {
return sentinelForField(f, unionTypes, plan) return sentinelForField(methodName, f, unionTypes, plan)
} }
} }
func sentinelForField(f spec.Field, unionTypes map[string]bool, plan *enumPlan) string { func sentinelForField(methodName string, f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
if name := plan.FieldEnum("", f.Name); name != "" && len(f.EnumValues) > 0 { if name := plan.FieldEnum(methodEnumParent(methodName), f.Name); name != "" && len(f.EnumValues) > 0 {
return constName(name, f.EnumValues[0]) return plan.ConstFor(name, f.EnumValues[0])
} }
tr := f.Type tr := f.Type
switch tr.Kind { switch tr.Kind {
+73 -10
View File
@@ -26,15 +26,21 @@ type enumPlan struct {
} }
// enumKey identifies a single Field occurrence so the emitter can look // enumKey identifies a single Field occurrence so the emitter can look
// up the enum name later. Parent is "" for method params (the method // up the enum name later. Parent is the type name for struct fields and
// doesn't share a Go type with the field). // 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 } 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 // planEnums walks the IR, decides on enum names, deduplicates, and
// returns an enumPlan. All scraper-marked enum fields are covered. // returns an enumPlan. All scraper-marked enum fields are covered.
func planEnums(api *spec.API) *enumPlan { func planEnums(api *spec.API) *enumPlan {
type ref struct { type ref struct {
parent string parent string // naming parent ("" for method params)
keyParent string // byField key parent (methodEnumParent(...) for method params)
fieldName string fieldName string
jsonName string jsonName string
values []string values []string
@@ -52,7 +58,7 @@ func planEnums(api *spec.API) *enumPlan {
} }
var refs []ref var refs []ref
collect := func(parent string, fields []spec.Field) { collect := func(parent, keyParent string, fields []spec.Field) {
for _, f := range fields { for _, f := range fields {
if len(f.EnumValues) == 0 { if len(f.EnumValues) == 0 {
continue continue
@@ -62,6 +68,7 @@ func planEnums(api *spec.API) *enumPlan {
} }
refs = append(refs, ref{ refs = append(refs, ref{
parent: parent, parent: parent,
keyParent: keyParent,
fieldName: f.Name, fieldName: f.Name,
jsonName: f.JSONName, jsonName: f.JSONName,
values: f.EnumValues, values: f.EnumValues,
@@ -70,13 +77,15 @@ func planEnums(api *spec.API) *enumPlan {
} }
} }
for _, t := range api.Types { for _, t := range api.Types {
collect(t.Name, t.Fields) collect(t.Name, t.Name, t.Fields)
} }
for _, m := range api.Methods { for _, m := range api.Methods {
// Method params have no shared Go parent type, so we pass "" as // Method params have no shared Go parent type, so the naming
// the parent. The default-name heuristic still produces the // parent is "" (the default-name heuristic still produces the
// right answer for ParseMode-style enums. // right answer for ParseMode-style enums), but the byField key
collect("", m.Params) // 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) // candidate name per ref (before collision resolution)
@@ -196,7 +205,7 @@ func planEnums(api *spec.API) *enumPlan {
} }
for i, r := range refs { for i, r := range refs {
name := groups[r.valueKey].name name := groups[r.valueKey].name
plan.byField[enumKey(r.parent, r.fieldName)] = name plan.byField[enumKey(r.keyParent, r.fieldName)] = name
_ = i _ = i
} }
for vk, g := range groups { for vk, g := range groups {
@@ -389,6 +398,18 @@ func (p *enumPlan) FieldEnum(parent, fieldName string) string {
return p.byField[enumKey(parent, fieldName)] 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 // defaultEnumName picks an initial Go enum name for a field. parse_mode
// fields collapse to the canonical "ParseMode"; otherwise the name is // fields collapse to the canonical "ParseMode"; otherwise the name is
// parent + PascalCase(jsonName). // parent + PascalCase(jsonName).
@@ -406,6 +427,48 @@ func constName(enumName, value string) string {
return enumName + valuePascal(value) 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 { func valuePascal(v string) string {
// "image/jpeg" → "ImageOfJpeg" // "image/jpeg" → "ImageOfJpeg"
parts := strings.Split(v, "/") parts := strings.Split(v, "/")
+1 -1
View File
@@ -8,6 +8,6 @@ package api
type {{$e.Name}} string type {{$e.Name}} string
const ( 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}})
{{end}} {{end}}
+56
View File
@@ -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")
}
+1 -1
View File
@@ -574,7 +574,7 @@ func TestSentinelForField(t *testing.T) {
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { 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) require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
}) })
} }
+7 -7
View File
@@ -48,7 +48,7 @@ func Test_{{$mName}}_Success(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(context.Background(), bot, params) _, err := {{$mName}}(context.Background(), bot, params)
@@ -67,7 +67,7 @@ func Test_{{$mName}}_APIError(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(context.Background(), bot, params) _, err := {{$mName}}(context.Background(), bot, params)
@@ -89,7 +89,7 @@ func Test_{{$mName}}_NetworkError(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(context.Background(), bot, params) _, err := {{$mName}}(context.Background(), bot, params)
@@ -109,7 +109,7 @@ func Test_{{$mName}}_ParseError(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(context.Background(), bot, params) _, err := {{$mName}}(context.Background(), bot, params)
@@ -132,7 +132,7 @@ func Test_{{$mName}}_ContextCanceled(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(ctx, bot, params) _, err := {{$mName}}(ctx, bot, params)
@@ -177,7 +177,7 @@ func Test_{{$mName}}_Forbidden(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(context.Background(), bot, params) _, err := {{$mName}}(context.Background(), bot, params)
@@ -203,7 +203,7 @@ func Test_{{$mName}}_ServerError(t *testing.T) {
{{- if .Params}} {{- if .Params}}
params := &{{$mName}}Params{ params := &{{$mName}}Params{
{{- range .Params}}{{if .Required}} {{- range .Params}}{{if .Required}}
{{.Name}}: {{sentinelValue .}},{{end}} {{.Name}}: {{sentinelValue $m.Name .}},{{end}}
{{- end}} {{- end}}
} }
_, err := {{$mName}}(context.Background(), bot, params) _, err := {{$mName}}(context.Background(), bot, params)
+2497 -734
View File
File diff suppressed because it is too large Load Diff
+2373 -8
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
snapshot_2026-06-01.html snapshot_2026-06-11.html
File diff suppressed because one or more lines are too long