Matches upstream owpengram/owpengram-server's version. v1.3.2 collapses the old per-kind KeyboardButtonClass sum type (KeyboardButton, KeyboardButtonURL, KeyboardButtonCallback, KeyboardButtonRequestPeer, ...) into two unified structs mirroring Telegram's actual current MTProto layer: KeyboardButton (reply keyboards) and KeyboardInlineButton (inline keyboards), each carrying a Text/Style pair plus a Type field (ButtonTypeClass / InlineButtonTypeClass) that now holds what used to be the concrete Go type. Migrated the two call sites (internal/rpc/convert_markup.go, internal/rpc/bots_longtail.go) and their tests to the new shape. Behavior is unchanged -- every button kind (callback, url, url_auth, web_view, switch_inline, copy, request_phone, request_geo_location, request_poll, request_peer, simple_web_view) still round-trips the same domain fields, just read from/written to Type instead of the button's own concrete type. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
332 lines
13 KiB
Go
332 lines
13 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/iamxvbaba/td/bin"
|
|
"github.com/iamxvbaba/td/tg"
|
|
"github.com/iamxvbaba/td/tgerr"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func TestMessagesSavePreparedInlineMessageStoresRegistry(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
|
|
req := &tg.MessagesSavePreparedInlineMessageRequest{
|
|
Result: preparedInlineArticleResult(),
|
|
UserID: inputUser(f.owner),
|
|
}
|
|
req.SetPeerTypes([]tg.InlineQueryPeerTypeClass{&tg.InlineQueryPeerTypePM{}})
|
|
got, err := f.router.onMessagesSavePreparedInlineMessage(botCtx, req)
|
|
if err != nil {
|
|
t.Fatalf("save prepared inline: %v", err)
|
|
}
|
|
if got.ID == "" || got.ExpireDate <= int(f.router.clock.Now().Unix()) {
|
|
t.Fatalf("save prepared inline = %+v, want id and future expire_date", got)
|
|
}
|
|
if _, err := f.router.onMessagesSavePreparedInlineMessage(userCtx, req); !tgerr.Is(err, "USER_BOT_REQUIRED") {
|
|
t.Fatalf("save prepared inline by user err = %v, want USER_BOT_REQUIRED", err)
|
|
}
|
|
if _, err := f.router.onMessagesSavePreparedInlineMessage(botCtx, &tg.MessagesSavePreparedInlineMessageRequest{
|
|
Result: preparedInlineArticleResult(),
|
|
UserID: &tg.InputUser{UserID: f.owner.ID + 999999, AccessHash: 1},
|
|
}); !tgerr.Is(err, "USER_ID_INVALID") {
|
|
t.Fatalf("save prepared inline bad user err = %v, want USER_ID_INVALID", err)
|
|
}
|
|
}
|
|
|
|
func TestMessagesEditInlineBotMessageRejectsWithoutInlineMessageState(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
req := &tg.MessagesEditInlineBotMessageRequest{
|
|
ID: validInputBotInlineMessageID(f.owner.ID),
|
|
Message: "edited",
|
|
}
|
|
req.SetFlags()
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(userCtx, req); ok || !tgerr.Is(err, "MESSAGE_ID_INVALID") {
|
|
t.Fatalf("edit inline by user = %v,%v, want false,MESSAGE_ID_INVALID", ok, err)
|
|
}
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, req); ok || !tgerr.Is(err, "MESSAGE_ID_INVALID") {
|
|
t.Fatalf("edit inline by bot = %v,%v, want false,MESSAGE_ID_INVALID", ok, err)
|
|
}
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, &tg.MessagesEditInlineBotMessageRequest{}); ok || !tgerr.Is(err, "MESSAGE_ID_INVALID") {
|
|
t.Fatalf("edit inline nil id = %v,%v, want false,MESSAGE_ID_INVALID", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestMessagesEditInlineBotMessageEditsPrivateInlineMessage(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
sessions := &captureSessions{}
|
|
f.router.deps.Sessions = sessions
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
queryID, _ := f.router.inlines.registerWithCacheKey(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID}, inlineCacheKey{query: "editable"})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResultWithCallbackMarkup("editable-1", "before edit", "open", []byte("v1")),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline results = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
if _, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 991001,
|
|
QueryID: queryID,
|
|
ID: "editable-1",
|
|
}); err != nil {
|
|
t.Fatalf("send inline bot result: %v", err)
|
|
}
|
|
feedback := inlineSendFeedbackFromSessions(t, sessions, f.bot.ID)
|
|
if feedback.UserID != f.owner.ID || feedback.Query != "editable" || feedback.ID != "editable-1" {
|
|
t.Fatalf("inline send feedback = user %d query %q id %q", feedback.UserID, feedback.Query, feedback.ID)
|
|
}
|
|
msgID, ok := feedback.GetMsgID()
|
|
if !ok {
|
|
t.Fatalf("inline send feedback missing msg_id")
|
|
}
|
|
|
|
editReq := &tg.MessagesEditInlineBotMessageRequest{ID: msgID}
|
|
editReq.SetMessage("after edit")
|
|
editReq.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{
|
|
Buttons: []tg.KeyboardInlineButton{{Text: "done", Type: &tg.InlineButtonTypeCallback{Data: []byte("v2")}}},
|
|
}}})
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, editReq); err != nil || !ok {
|
|
t.Fatalf("edit inline bot message = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
ownerHistory, err := f.router.deps.Messages.GetHistory(ctx, f.owner.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID},
|
|
Limit: 1,
|
|
})
|
|
if err != nil || len(ownerHistory.Messages) != 1 {
|
|
t.Fatalf("owner history len = %d err=%v, want 1,nil", len(ownerHistory.Messages), err)
|
|
}
|
|
assertInlineEditedMessage(t, ownerHistory.Messages[0], "after edit", f.bot.ID, "done", []byte("v2"))
|
|
|
|
peerHistory, err := f.router.deps.Messages.GetHistory(ctx, f.peer.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID},
|
|
Limit: 1,
|
|
})
|
|
if err != nil || len(peerHistory.Messages) != 1 {
|
|
t.Fatalf("peer history len = %d err=%v, want 1,nil", len(peerHistory.Messages), err)
|
|
}
|
|
assertInlineEditedMessage(t, peerHistory.Messages[0], "after edit", f.bot.ID, "done", []byte("v2"))
|
|
}
|
|
|
|
func TestMessagesEditInlineBotMessageRewritesPrivateMedia(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
sessions := &captureSessions{}
|
|
f.router.deps.Sessions = sessions
|
|
ownerCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
queryID, _ := f.router.inlines.registerWithCacheKey(f.router.clock.Now(), f.bot.ID, f.owner.ID, domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID}, inlineCacheKey{query: "editable-media"})
|
|
if ok, err := f.router.onMessagesSetInlineBotResults(botCtx, &tg.MessagesSetInlineBotResultsRequest{
|
|
QueryID: queryID,
|
|
Results: []tg.InputBotInlineResultClass{
|
|
inlineArticleResultWithCallbackMarkup("editable-media-1", "caption before media edit", "open", []byte("media-v1")),
|
|
},
|
|
}); err != nil || !ok {
|
|
t.Fatalf("set inline media result = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if _, err := f.router.onMessagesSendInlineBotResult(ownerCtx, &tg.MessagesSendInlineBotResultRequest{
|
|
Peer: inputPeerUser(f.peer),
|
|
RandomID: 991003,
|
|
QueryID: queryID,
|
|
ID: "editable-media-1",
|
|
}); err != nil {
|
|
t.Fatalf("send inline bot media result: %v", err)
|
|
}
|
|
msgID, ok := inlineSendFeedbackFromSessions(t, sessions, f.bot.ID).GetMsgID()
|
|
if !ok {
|
|
t.Fatalf("inline send feedback missing msg_id")
|
|
}
|
|
|
|
badEdit := &tg.MessagesEditInlineBotMessageRequest{ID: msgID}
|
|
badEdit.SetMedia(&tg.InputMediaDice{Emoticon: "🎲"})
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, badEdit); ok || !tgerr.Is(err, "MEDIA_INVALID") {
|
|
t.Fatalf("unsupported inline media edit = %v,%v, want false,MEDIA_INVALID", ok, err)
|
|
}
|
|
|
|
editReq := &tg.MessagesEditInlineBotMessageRequest{ID: msgID}
|
|
editReq.SetMedia(&tg.InputMediaContact{
|
|
PhoneNumber: f.peer.Phone,
|
|
FirstName: "Peer",
|
|
LastName: "Edited",
|
|
Vcard: "BEGIN:VCARD\nFN:Peer Edited\nEND:VCARD",
|
|
})
|
|
if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, editReq); err != nil || !ok {
|
|
t.Fatalf("edit inline bot media = %v,%v, want true,nil", ok, err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
userID int64
|
|
peer domain.Peer
|
|
}{
|
|
{name: "owner", userID: f.owner.ID, peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.peer.ID}},
|
|
{name: "peer", userID: f.peer.ID, peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.owner.ID}},
|
|
} {
|
|
history, err := f.router.deps.Messages.GetHistory(ctx, tc.userID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: tc.peer,
|
|
Limit: 1,
|
|
})
|
|
if err != nil || len(history.Messages) != 1 {
|
|
t.Fatalf("%s history len = %d err=%v, want 1,nil", tc.name, len(history.Messages), err)
|
|
}
|
|
msg := history.Messages[0]
|
|
if msg.Body != "caption before media edit" || msg.ViaBotID != f.bot.ID {
|
|
t.Fatalf("%s edited message = %+v, want original caption via bot", tc.name, msg)
|
|
}
|
|
if msg.Media == nil || msg.Media.Kind != domain.MessageMediaKindContact || msg.Media.Contact == nil {
|
|
t.Fatalf("%s edited media = %+v, want contact", tc.name, msg.Media)
|
|
}
|
|
if msg.Media.Contact.PhoneNumber != f.peer.Phone || msg.Media.Contact.UserID != f.peer.ID {
|
|
t.Fatalf("%s edited contact = %+v, want peer contact", tc.name, msg.Media.Contact)
|
|
}
|
|
assertDomainInlineReplyMarkup(t, msg.ReplyMarkup, "open", []byte("media-v1"))
|
|
}
|
|
}
|
|
|
|
func TestMessagesPaymentQueryAnswersRejectWithoutPaymentState(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
|
|
if ok, err := f.router.onMessagesSetBotShippingResults(botCtx, &tg.MessagesSetBotShippingResultsRequest{QueryID: 42}); ok || !tgerr.Is(err, "QUERY_ID_INVALID") {
|
|
t.Fatalf("set shipping = %v,%v, want false,QUERY_ID_INVALID", ok, err)
|
|
}
|
|
if ok, err := f.router.onMessagesSetBotShippingResults(userCtx, &tg.MessagesSetBotShippingResultsRequest{QueryID: 42}); ok || !tgerr.Is(err, "USER_BOT_REQUIRED") {
|
|
t.Fatalf("set shipping by user = %v,%v, want false,USER_BOT_REQUIRED", ok, err)
|
|
}
|
|
precheckout := &tg.MessagesSetBotPrecheckoutResultsRequest{QueryID: 43}
|
|
precheckout.SetSuccess(true)
|
|
if ok, err := f.router.onMessagesSetBotPrecheckoutResults(botCtx, precheckout); ok || !tgerr.Is(err, "QUERY_ID_INVALID") {
|
|
t.Fatalf("set precheckout = %v,%v, want false,QUERY_ID_INVALID", ok, err)
|
|
}
|
|
if ok, err := f.router.onMessagesSetBotPrecheckoutResults(userCtx, precheckout); ok || !tgerr.Is(err, "USER_BOT_REQUIRED") {
|
|
t.Fatalf("set precheckout by user = %v,%v, want false,USER_BOT_REQUIRED", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestMessagesBotNoStateExplicitStubsAreRegistered(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
tests := []struct {
|
|
name string
|
|
req bin.Encoder
|
|
want string
|
|
}{
|
|
{
|
|
name: "editInlineBotMessage",
|
|
req: &tg.MessagesEditInlineBotMessageRequest{
|
|
ID: validInputBotInlineMessageID(f.owner.ID),
|
|
Message: "edited",
|
|
},
|
|
want: "MESSAGE_ID_INVALID",
|
|
},
|
|
{
|
|
name: "setBotShippingResults",
|
|
req: &tg.MessagesSetBotShippingResultsRequest{QueryID: 42},
|
|
want: "QUERY_ID_INVALID",
|
|
},
|
|
{
|
|
name: "setBotPrecheckoutResults",
|
|
req: func() bin.Encoder {
|
|
req := &tg.MessagesSetBotPrecheckoutResultsRequest{QueryID: 43}
|
|
req.SetSuccess(true)
|
|
return req
|
|
}(),
|
|
want: "QUERY_ID_INVALID",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var in bin.Buffer
|
|
if err := tt.req.Encode(&in); err != nil {
|
|
t.Fatalf("encode request: %v", err)
|
|
}
|
|
if _, err := f.router.Dispatch(botCtx, [8]byte{}, 0, &in); !tgerr.Is(err, tt.want) {
|
|
t.Fatalf("dispatch err = %v, want %s", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func preparedInlineArticleResult() tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResult{
|
|
ID: "prepared-article",
|
|
Type: "article",
|
|
Title: "Prepared",
|
|
SendMessage: &tg.InputBotInlineMessageText{
|
|
Message: "prepared text",
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineArticleResultWithCallbackMarkup(id, message, button string, data []byte) tg.InputBotInlineResultClass {
|
|
return &tg.InputBotInlineResult{
|
|
ID: id,
|
|
Type: "article",
|
|
Title: id,
|
|
SendMessage: &tg.InputBotInlineMessageText{
|
|
Message: message,
|
|
ReplyMarkup: &tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{
|
|
Buttons: []tg.KeyboardInlineButton{{Text: button, Type: &tg.InlineButtonTypeCallback{Data: data}}},
|
|
}}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func inlineSendFeedbackFromSessions(t *testing.T, sessions *captureSessions, botID int64) *tg.UpdateBotInlineSend {
|
|
t.Helper()
|
|
snap := sessions.snapshot()
|
|
if snap.userID != botID {
|
|
t.Fatalf("feedback pushed to user %d, want bot %d", snap.userID, botID)
|
|
}
|
|
updates, ok := snap.message.(*tg.Updates)
|
|
if !ok {
|
|
t.Fatalf("feedback message = %T, want *tg.Updates", snap.message)
|
|
}
|
|
for _, update := range updates.Updates {
|
|
if feedback, ok := update.(*tg.UpdateBotInlineSend); ok {
|
|
return feedback
|
|
}
|
|
}
|
|
t.Fatalf("feedback updates missing UpdateBotInlineSend: %+v", updates.Updates)
|
|
return nil
|
|
}
|
|
|
|
func assertInlineEditedMessage(t *testing.T, msg domain.Message, wantBody string, wantBotID int64, wantButton string, wantData []byte) {
|
|
t.Helper()
|
|
if msg.Body != wantBody || msg.ViaBotID != wantBotID {
|
|
t.Fatalf("message body/via = %q/%d, want %q/%d", msg.Body, msg.ViaBotID, wantBody, wantBotID)
|
|
}
|
|
if msg.ReplyMarkup == nil || len(msg.ReplyMarkup.Inline) != 1 || len(msg.ReplyMarkup.Inline[0]) != 1 {
|
|
t.Fatalf("message markup = %+v, want one callback button", msg.ReplyMarkup)
|
|
}
|
|
btn := msg.ReplyMarkup.Inline[0][0]
|
|
if btn.Text != wantButton || string(btn.Data) != string(wantData) {
|
|
t.Fatalf("message button = %q/%q, want %q/%q", btn.Text, string(btn.Data), wantButton, string(wantData))
|
|
}
|
|
}
|