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>
316 lines
13 KiB
Go
316 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 TestBotsLongtailReadStubsReturnEmptyFacts(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
|
|
can, err := f.router.onBotsCanSendMessage(userCtx, inputUser(f.bot))
|
|
if err != nil || can {
|
|
t.Fatalf("canSendMessage = %v,%v, want false,nil", can, err)
|
|
}
|
|
|
|
popular, err := f.router.onBotsGetPopularAppBots(userCtx, &tg.BotsGetPopularAppBotsRequest{Limit: 20})
|
|
if err != nil {
|
|
t.Fatalf("getPopularAppBots: %v", err)
|
|
}
|
|
if next, ok := popular.GetNextOffset(); ok || next != "" || len(popular.Users) != 0 {
|
|
t.Fatalf("popular app bots = %+v, want empty without next offset", popular)
|
|
}
|
|
|
|
info, err := f.router.onBotsGetPreviewInfo(userCtx, &tg.BotsGetPreviewInfoRequest{
|
|
Bot: inputUser(f.bot),
|
|
LangCode: "en",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("getPreviewInfo: %v", err)
|
|
}
|
|
if len(info.Media) != 0 || len(info.LangCodes) != 0 {
|
|
t.Fatalf("preview info = %+v, want empty", info)
|
|
}
|
|
|
|
medias, err := f.router.onBotsGetPreviewMedias(userCtx, inputUser(f.bot))
|
|
if err != nil {
|
|
t.Fatalf("getPreviewMedias: %v", err)
|
|
}
|
|
if len(medias) != 0 {
|
|
t.Fatalf("preview medias len = %d, want 0", len(medias))
|
|
}
|
|
|
|
access, err := f.router.onBotsGetAccessSettings(userCtx, inputUser(f.bot))
|
|
if err != nil {
|
|
t.Fatalf("getAccessSettings: %v", err)
|
|
}
|
|
if access.GetRestricted() {
|
|
t.Fatalf("access settings = %+v, want unrestricted empty", access)
|
|
}
|
|
|
|
recs, err := f.router.onBotsGetBotRecommendations(userCtx, inputUser(f.bot))
|
|
if err != nil {
|
|
t.Fatalf("getBotRecommendations: %v", err)
|
|
}
|
|
if got := len(recs.(*tg.UsersUsers).Users); got != 0 {
|
|
t.Fatalf("recommendations len = %d, want 0", got)
|
|
}
|
|
|
|
recs, err = f.router.onBotsGetBotRecommendations(userCtx, inputUser(f.owner))
|
|
if err != nil {
|
|
t.Fatalf("getBotRecommendations non-bot target: %v", err)
|
|
}
|
|
if got := len(recs.(*tg.UsersUsers).Users); got != 0 {
|
|
t.Fatalf("recommendations for non-bot target len = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestBotsWriteAccessAllowSendMessageRoundTrip(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
|
|
can, err := f.router.onBotsCanSendMessage(userCtx, inputUser(f.bot))
|
|
if err != nil || can {
|
|
t.Fatalf("can before allow = %v,%v, want false,nil", can, err)
|
|
}
|
|
updatesClass, err := f.router.onBotsAllowSendMessage(userCtx, inputUser(f.bot))
|
|
if err != nil {
|
|
t.Fatalf("allowSendMessage: %v", err)
|
|
}
|
|
updates := updatesClass.(*tg.Updates)
|
|
if len(updates.Updates) != 1 {
|
|
t.Fatalf("allow updates len = %d, want one service message", len(updates.Updates))
|
|
}
|
|
newMessage, ok := updates.Updates[0].(*tg.UpdateNewMessage)
|
|
if !ok {
|
|
t.Fatalf("allow update = %T, want UpdateNewMessage", updates.Updates[0])
|
|
}
|
|
service, ok := newMessage.Message.(*tg.MessageService)
|
|
if !ok {
|
|
t.Fatalf("allow message = %T, want MessageService", newMessage.Message)
|
|
}
|
|
action, ok := service.Action.(*tg.MessageActionBotAllowed)
|
|
if !ok || !action.FromRequest {
|
|
t.Fatalf("allow action = %+v (%T), want messageActionBotAllowed from_request", service.Action, service.Action)
|
|
}
|
|
if service.PeerID.(*tg.PeerUser).UserID != f.bot.ID || service.FromID.(*tg.PeerUser).UserID != f.owner.ID {
|
|
t.Fatalf("allow service peer/from = %+v/%+v, want bot/user", service.PeerID, service.FromID)
|
|
}
|
|
|
|
can, err = f.router.onBotsCanSendMessage(userCtx, inputUser(f.bot))
|
|
if err != nil || !can {
|
|
t.Fatalf("can after allow = %v,%v, want true,nil", can, err)
|
|
}
|
|
repeat, err := f.router.onBotsAllowSendMessage(userCtx, inputUser(f.bot))
|
|
if err != nil {
|
|
t.Fatalf("repeat allowSendMessage: %v", err)
|
|
}
|
|
if got := len(repeat.(*tg.Updates).Updates); got != 0 {
|
|
t.Fatalf("repeat allow updates len = %d, want empty", got)
|
|
}
|
|
history, err := f.router.deps.Messages.GetHistory(ctx, f.owner.ID, domain.MessageFilter{
|
|
HasPeer: true,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.bot.ID},
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("history: %v", err)
|
|
}
|
|
serviceMessages := 0
|
|
for _, msg := range history.Messages {
|
|
if msg.Media != nil && msg.Media.ServiceAction != nil && msg.Media.ServiceAction.Kind == domain.MessageServiceActionBotAllowed {
|
|
serviceMessages++
|
|
}
|
|
}
|
|
if serviceMessages != 1 {
|
|
t.Fatalf("bot allowed service messages = %d, want 1", serviceMessages)
|
|
}
|
|
}
|
|
|
|
func TestBotsLongtailRejectsMissingState(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
if _, err := f.router.onBotsInvokeWebViewCustomMethod(userCtx, &tg.BotsInvokeWebViewCustomMethodRequest{
|
|
Bot: inputUser(f.bot),
|
|
CustomMethod: "unsupported",
|
|
Params: tg.DataJSON{Data: "{}"},
|
|
}); !tgerr.Is(err, "METHOD_INVALID") {
|
|
t.Fatalf("invoke custom method err = %v, want METHOD_INVALID", err)
|
|
}
|
|
if _, err := f.router.onBotsAddPreviewMedia(userCtx, &tg.BotsAddPreviewMediaRequest{
|
|
Bot: inputUser(f.bot),
|
|
Media: &tg.InputMediaEmpty{},
|
|
}); !tgerr.Is(err, "BOT_APP_INVALID") {
|
|
t.Fatalf("add preview media err = %v, want BOT_APP_INVALID", err)
|
|
}
|
|
if ok, err := f.router.onBotsDeletePreviewMedia(userCtx, &tg.BotsDeletePreviewMediaRequest{
|
|
Bot: inputUser(f.bot),
|
|
Media: []tg.InputMediaClass{&tg.InputMediaEmpty{}},
|
|
}); ok || !tgerr.Is(err, "BOT_APP_INVALID") {
|
|
t.Fatalf("delete preview media = %v,%v, want false,BOT_APP_INVALID", ok, err)
|
|
}
|
|
if _, err := f.router.onBotsGetRequestedWebViewButton(userCtx, &tg.BotsGetRequestedWebViewButtonRequest{
|
|
Bot: inputUser(f.bot),
|
|
WebappReqID: "req",
|
|
}); !tgerr.Is(err, "BUTTON_DATA_INVALID") {
|
|
t.Fatalf("get requested button err = %v, want BUTTON_DATA_INVALID", err)
|
|
}
|
|
if ok, err := f.router.onBotsCheckDownloadFileParams(userCtx, &tg.BotsCheckDownloadFileParamsRequest{
|
|
Bot: inputUser(f.bot),
|
|
FileName: "file.txt",
|
|
URL: "https://127.0.0.1/file.txt",
|
|
}); ok || err != nil {
|
|
t.Fatalf("check download params = %v,%v, want false,nil", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsUpdateUserEmojiStatus(botCtx, &tg.BotsUpdateUserEmojiStatusRequest{
|
|
UserID: inputUser(f.owner),
|
|
EmojiStatus: &tg.EmojiStatusEmpty{},
|
|
}); ok || !tgerr.Is(err, "USER_PERMISSION_DENIED") {
|
|
t.Fatalf("update emoji status = %v,%v, want false,USER_PERMISSION_DENIED", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsToggleUserEmojiStatusPermission(userCtx, &tg.BotsToggleUserEmojiStatusPermissionRequest{
|
|
Bot: inputUser(f.bot),
|
|
Enabled: true,
|
|
}); !ok || err != nil {
|
|
t.Fatalf("toggle emoji permission = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsUpdateUserEmojiStatus(botCtx, &tg.BotsUpdateUserEmojiStatusRequest{
|
|
UserID: inputUser(f.owner),
|
|
EmojiStatus: &tg.EmojiStatusEmpty{},
|
|
}); !ok || err != nil {
|
|
t.Fatalf("update emoji status after permission = %v,%v, want true,nil", ok, err)
|
|
}
|
|
if _, err := f.router.onBotsSendCustomRequest(botCtx, &tg.BotsSendCustomRequestRequest{
|
|
CustomMethod: "unsupported",
|
|
Params: tg.DataJSON{Data: "{}"},
|
|
}); !tgerr.Is(err, "METHOD_INVALID") {
|
|
t.Fatalf("send custom request err = %v, want METHOD_INVALID", err)
|
|
}
|
|
if ok, err := f.router.onBotsAnswerWebhookJSONQuery(botCtx, &tg.BotsAnswerWebhookJSONQueryRequest{
|
|
QueryID: 1,
|
|
Data: tg.DataJSON{Data: "{}"},
|
|
}); ok || !tgerr.Is(err, "QUERY_ID_INVALID") {
|
|
t.Fatalf("answer webhook json query = %v,%v, want false,QUERY_ID_INVALID", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestBotsLongtailCommercialAndSettingsStubs(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
if ok, err := f.router.onBotsSetBotBroadcastDefaultAdminRights(botCtx, tg.ChatAdminRights{}); ok || !tgerr.Is(err, "RIGHTS_NOT_MODIFIED") {
|
|
t.Fatalf("broadcast default rights = %v,%v, want false,RIGHTS_NOT_MODIFIED", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsSetBotGroupDefaultAdminRights(botCtx, tg.ChatAdminRights{}); ok || !tgerr.Is(err, "RIGHTS_NOT_MODIFIED") {
|
|
t.Fatalf("group default rights = %v,%v, want false,RIGHTS_NOT_MODIFIED", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsReorderUsernames(userCtx, &tg.BotsReorderUsernamesRequest{
|
|
Bot: inputUser(f.bot),
|
|
Order: []string{f.bot.Username},
|
|
}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
|
|
t.Fatalf("reorder usernames = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsToggleUsername(userCtx, &tg.BotsToggleUsernameRequest{
|
|
Bot: inputUser(f.bot),
|
|
Username: f.bot.Username,
|
|
Active: true,
|
|
}); ok || !tgerr.Is(err, "USERNAME_NOT_MODIFIED") {
|
|
t.Fatalf("toggle username = %v,%v, want false,USERNAME_NOT_MODIFIED", ok, err)
|
|
}
|
|
if _, err := f.router.onBotsUpdateStarRefProgram(userCtx, &tg.BotsUpdateStarRefProgramRequest{
|
|
Bot: inputUser(f.bot),
|
|
CommissionPermille: 100,
|
|
}); !tgerr.Is(err, "BOT_INVALID") {
|
|
t.Fatalf("update star ref program err = %v, want BOT_INVALID", err)
|
|
}
|
|
if ok, err := f.router.onBotsSetCustomVerification(userCtx, func() *tg.BotsSetCustomVerificationRequest {
|
|
req := &tg.BotsSetCustomVerificationRequest{Peer: inputPeerUser(f.peer)}
|
|
req.SetBot(inputUser(f.bot))
|
|
req.SetEnabled(true)
|
|
return req
|
|
}()); ok || !tgerr.Is(err, "BOT_VERIFIER_FORBIDDEN") {
|
|
t.Fatalf("set custom verification = %v,%v, want false,BOT_VERIFIER_FORBIDDEN", ok, err)
|
|
}
|
|
if ok, err := f.router.onBotsEditAccessSettings(userCtx, &tg.BotsEditAccessSettingsRequest{
|
|
Bot: inputUser(f.bot),
|
|
}); ok || !tgerr.Is(err, "BOT_INVALID") {
|
|
t.Fatalf("edit access settings = %v,%v, want false,BOT_INVALID", ok, err)
|
|
}
|
|
if _, err := f.router.onBotsRequestWebViewButton(botCtx, &tg.BotsRequestWebViewButtonRequest{
|
|
UserID: inputUser(f.owner),
|
|
Button: tg.KeyboardButton{
|
|
Text: "Open",
|
|
Type: &tg.ButtonTypeSimpleWebView{URL: "https://example.com/app"},
|
|
},
|
|
}); !tgerr.Is(err, "BUTTON_DATA_INVALID") {
|
|
t.Fatalf("request webview button err = %v, want BUTTON_DATA_INVALID", err)
|
|
}
|
|
}
|
|
|
|
func TestBotsLongtailExplicitStubsAreRegistered(t *testing.T) {
|
|
ctx := context.Background()
|
|
f := newInlineBotRPCTestFixture(t)
|
|
userCtx := WithUserID(ctx, f.owner.ID)
|
|
botCtx := WithUserID(ctx, f.bot.ID)
|
|
|
|
userRequests := []struct {
|
|
name string
|
|
req bin.Encoder
|
|
want string
|
|
}{
|
|
{name: "invokeWebViewCustomMethod", req: &tg.BotsInvokeWebViewCustomMethodRequest{Bot: inputUser(f.bot), CustomMethod: "x", Params: tg.DataJSON{Data: "{}"}}, want: "METHOD_INVALID"},
|
|
{name: "addPreviewMedia", req: &tg.BotsAddPreviewMediaRequest{Bot: inputUser(f.bot), Media: &tg.InputMediaEmpty{}}, want: "BOT_APP_INVALID"},
|
|
{name: "getRequestedWebViewButton", req: &tg.BotsGetRequestedWebViewButtonRequest{Bot: inputUser(f.bot), WebappReqID: "req"}, want: "BUTTON_DATA_INVALID"},
|
|
{name: "reorderUsernames", req: &tg.BotsReorderUsernamesRequest{Bot: inputUser(f.bot), Order: []string{f.bot.Username}}, want: "USERNAME_NOT_MODIFIED"},
|
|
{name: "toggleUsername", req: &tg.BotsToggleUsernameRequest{Bot: inputUser(f.bot), Username: f.bot.Username, Active: true}, want: "USERNAME_NOT_MODIFIED"},
|
|
{name: "editAccessSettings", req: &tg.BotsEditAccessSettingsRequest{Bot: inputUser(f.bot)}, want: "BOT_INVALID"},
|
|
}
|
|
for _, tt := range userRequests {
|
|
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(userCtx, [8]byte{}, 0, &in); !tgerr.Is(err, tt.want) {
|
|
t.Fatalf("dispatch err = %v, want %s", err, tt.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
botRequests := []struct {
|
|
name string
|
|
req bin.Encoder
|
|
want string
|
|
}{
|
|
{name: "sendCustomRequest", req: &tg.BotsSendCustomRequestRequest{CustomMethod: "x", Params: tg.DataJSON{Data: "{}"}}, want: "METHOD_INVALID"},
|
|
{name: "answerWebhookJSONQuery", req: &tg.BotsAnswerWebhookJSONQueryRequest{QueryID: 1, Data: tg.DataJSON{Data: "{}"}}, want: "QUERY_ID_INVALID"},
|
|
{name: "setBotBroadcastDefaultAdminRights", req: &tg.BotsSetBotBroadcastDefaultAdminRightsRequest{AdminRights: tg.ChatAdminRights{}}, want: "RIGHTS_NOT_MODIFIED"},
|
|
{name: "setBotGroupDefaultAdminRights", req: &tg.BotsSetBotGroupDefaultAdminRightsRequest{AdminRights: tg.ChatAdminRights{}}, want: "RIGHTS_NOT_MODIFIED"},
|
|
{name: "updateUserEmojiStatus", req: &tg.BotsUpdateUserEmojiStatusRequest{UserID: inputUser(f.owner), EmojiStatus: &tg.EmojiStatusEmpty{}}, want: "USER_PERMISSION_DENIED"},
|
|
}
|
|
for _, tt := range botRequests {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|