FILE / ScuroNeko/Laniakea

handler_test.go

Исходный файл и его история в репозитории.
FILE e2444752c22c5b5edb1a191fc333c2455f44b251
Files
Laniakea/handler_test.go
T
ScuroNeko e2444752c2 rename app data API
Align docs, examples, and tests with AppData and NoData

Update handler, scene, runner, and plugin generics to the new naming

Ignore .codex and record the cleanup in the changelog
2026-03-30 16:43:59 +03:00

803 lines
21 KiB
Go

package laniakea
import (
"context"
"testing"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
func ptr[T any](v T) *T {
return &v
}
func TestCheckPrefixesSkipsEmptyPrefixes(t *testing.T) {
bot := &Bot[NoData]{prefixes: []string{"", "/"}}
if prefix, ok := bot.checkPrefixes("hello"); ok {
t.Fatalf("unexpected prefix match for plain text: %q", prefix)
}
if prefix, ok := bot.checkPrefixes("/start"); !ok || prefix != "/" {
t.Fatalf("unexpected prefix result: prefix=%q ok=%v", prefix, ok)
}
}
func TestBotMiddlewareReceivesLogger(t *testing.T) {
logger := slog.CreateLogger()
called := false
bot := &Bot[NoData]{
logger: logger,
middlewares: []Middleware[NoData]{
NewMiddleware("logger-check", func(ctx *MsgContext, db NoData) bool {
called = true
if ctx.Logger != logger {
t.Fatalf("expected bot logger in middleware context, got %#v", ctx.Logger)
}
return true
}),
},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 1,
Type: tgapi.UpdateTypePoll,
Poll: &tgapi.Poll{
ID: "poll",
Question: "question",
},
})
if !called {
t.Fatal("expected bot middleware to be called")
}
}
func TestAddUpdateHandlerRejectsReservedUpdateTypes(t *testing.T) {
plugin := NewPlugin[NoData]("test")
handler := func(ctx *MsgContext, db NoData) error { return nil }
for _, updateType := range []tgapi.UpdateType{
tgapi.UpdateTypeMessage,
tgapi.UpdateTypeChannelPost,
tgapi.UpdateTypeCallbackQuery,
} {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("AddUpdateHandler(%q) panicked: %v", updateType, r)
}
}()
plugin.AddUpdateHandler(updateType, handler)
}()
if _, ok := plugin.handlers[updateType]; ok {
t.Fatalf("reserved update type %q must not be registered", updateType)
}
}
}
func TestPrepareUpdateCtxContract(t *testing.T) {
tests := []struct {
name string
update *tgapi.Update
wantMsg bool
wantFrom bool
wantFromID int64
wantCallbackID string
wantCallbackMsgID int
wantInlineMsgID string
}{
{
name: "message",
update: &tgapi.Update{
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 11,
From: &tgapi.User{ID: 101},
Chat: &tgapi.Chat{ID: 1001},
},
},
wantMsg: true,
wantFrom: true,
wantFromID: 101,
},
{
name: "edited message",
update: &tgapi.Update{
Type: tgapi.UpdateTypeEditedMessage,
EditedMessage: &tgapi.Message{
MessageID: 12,
From: &tgapi.User{ID: 102},
Chat: &tgapi.Chat{ID: 1002},
},
},
wantMsg: true,
wantFrom: true,
wantFromID: 102,
},
{
name: "channel post sender chat",
update: &tgapi.Update{
Type: tgapi.UpdateTypeChannelPost,
ChannelPost: &tgapi.Message{
MessageID: 13,
Chat: &tgapi.Chat{ID: -1003},
},
},
wantMsg: true,
},
{
name: "business message",
update: &tgapi.Update{
Type: tgapi.UpdateTypeBusinessMessage,
BusinessMessage: &tgapi.Message{
MessageID: 14,
From: &tgapi.User{ID: 103},
Chat: &tgapi.Chat{ID: 1004},
},
},
wantMsg: true,
wantFrom: true,
wantFromID: 103,
},
{
name: "inline query",
update: &tgapi.Update{
Type: tgapi.UpdateTypeInlineQuery,
InlineQuery: &tgapi.InlineQuery{ID: "iq", From: tgapi.User{ID: 104}},
},
wantFrom: true,
wantFromID: 104,
},
{
name: "chosen inline result",
update: &tgapi.Update{
Type: tgapi.UpdateTypeChosenInlineResult,
ChosenInlineResult: &tgapi.ChosenInlineResult{ResultID: "res", From: tgapi.User{ID: 105}},
},
wantFrom: true,
wantFromID: 105,
},
{
name: "callback query with message",
update: &tgapi.Update{
Type: tgapi.UpdateTypeCallbackQuery,
CallbackQuery: &tgapi.CallbackQuery{
ID: "cb-1",
From: tgapi.User{ID: 106},
Message: &tgapi.Message{
MessageID: 77,
Chat: &tgapi.Chat{ID: 1005},
},
},
},
wantMsg: true,
wantFrom: true,
wantFromID: 106,
wantCallbackID: "cb-1",
wantCallbackMsgID: 77,
},
{
name: "callback query with inline message",
update: &tgapi.Update{
Type: tgapi.UpdateTypeCallbackQuery,
CallbackQuery: &tgapi.CallbackQuery{
ID: "cb-2",
From: tgapi.User{ID: 107},
InlineMessageID: ptr("inline-42"),
},
},
wantFrom: true,
wantFromID: 107,
wantCallbackID: "cb-2",
wantInlineMsgID: "inline-42",
},
{
name: "shipping query",
update: &tgapi.Update{
Type: tgapi.UpdateTypeShippingQuery,
ShippingQuery: &tgapi.ShippingQuery{ID: "ship", From: tgapi.User{ID: 108}},
},
wantFrom: true,
wantFromID: 108,
},
{
name: "pre checkout query",
update: &tgapi.Update{
Type: tgapi.UpdateTypePreCheckoutQuery,
PreCheckoutQuery: &tgapi.PreCheckoutQuery{ID: "pre", From: tgapi.User{ID: 109}},
},
wantFrom: true,
wantFromID: 109,
},
{
name: "purchased paid media",
update: &tgapi.Update{
Type: tgapi.UpdateTypePurchasedPaidMedia,
PurchasedPaidMedia: &tgapi.PaidMediaPurchased{From: tgapi.User{ID: 110}},
},
wantFrom: true,
wantFromID: 110,
},
{
name: "my chat member",
update: &tgapi.Update{
Type: tgapi.UpdateTypeMyChatMember,
MyChatMember: &tgapi.ChatMemberUpdated{From: tgapi.User{ID: 111}},
},
wantFrom: true,
wantFromID: 111,
},
{
name: "chat member",
update: &tgapi.Update{
Type: tgapi.UpdateTypeChatMember,
ChatMember: &tgapi.ChatMemberUpdated{From: tgapi.User{ID: 112}},
},
wantFrom: true,
wantFromID: 112,
},
{
name: "chat join request",
update: &tgapi.Update{
Type: tgapi.UpdateTypeChatJoinRequest,
ChatJoinRequest: &tgapi.ChatJoinRequest{From: tgapi.User{ID: 113}},
},
wantFrom: true,
wantFromID: 113,
},
{
name: "business connection",
update: &tgapi.Update{
Type: tgapi.UpdateTypeBusinessConnection,
BusinessConnection: &tgapi.BusinessConnection{User: tgapi.User{ID: 114}},
},
wantFrom: true,
wantFromID: 114,
},
{
name: "poll answer",
update: &tgapi.Update{
Type: tgapi.UpdateTypePollAnswer,
PollAnswer: &tgapi.PollAnswer{User: tgapi.User{ID: 115}},
},
wantFrom: true,
wantFromID: 115,
},
{
name: "message reaction",
update: &tgapi.Update{
Type: tgapi.UpdateTypeMessageReaction,
MessageReaction: &tgapi.MessageReactionUpdated{User: &tgapi.User{ID: 116}},
},
wantFrom: true,
wantFromID: 116,
},
{
name: "chat boost",
update: &tgapi.Update{
Type: tgapi.UpdateTypeChatBoost,
ChatBoost: &tgapi.ChatBoostUpdated{
Boost: tgapi.ChatBoost{Source: tgapi.ChatBoostSource{User: tgapi.User{ID: 117}}},
},
},
wantFrom: true,
wantFromID: 117,
},
{
name: "removed chat boost",
update: &tgapi.Update{
Type: tgapi.UpdateTypeRemovedChatBoost,
RemovedChatBoost: &tgapi.ChatBoostRemoved{
Source: tgapi.ChatBoostSource{User: tgapi.User{ID: 118}},
},
},
wantFrom: true,
wantFromID: 118,
},
{
name: "poll",
update: &tgapi.Update{
Type: tgapi.UpdateTypePoll,
Poll: &tgapi.Poll{ID: "poll"},
},
},
{
name: "message reaction count",
update: &tgapi.Update{
Type: tgapi.UpdateTypeMessageReactionCount,
MessageReactionCount: &tgapi.MessageReactionCountUpdated{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bot := &Bot[NoData]{}
ctx := &MsgContext{}
bot.prepareUpdateCtx(tt.update, ctx)
if got := ctx.Msg != nil; got != tt.wantMsg {
t.Fatalf("unexpected Msg presence: got %v want %v", got, tt.wantMsg)
}
if got := ctx.From != nil; got != tt.wantFrom {
t.Fatalf("unexpected From presence: got %v want %v", got, tt.wantFrom)
}
if ctx.FromID != tt.wantFromID {
t.Fatalf("unexpected FromID: got %d want %d", ctx.FromID, tt.wantFromID)
}
if ctx.CallbackQueryId != tt.wantCallbackID {
t.Fatalf("unexpected CallbackQueryId: got %q want %q", ctx.CallbackQueryId, tt.wantCallbackID)
}
if ctx.CallbackMsgId != tt.wantCallbackMsgID {
t.Fatalf("unexpected CallbackMsgId: got %d want %d", ctx.CallbackMsgId, tt.wantCallbackMsgID)
}
if ctx.InlineMsgId != tt.wantInlineMsgID {
t.Fatalf("unexpected InlineMsgId: got %q want %q", ctx.InlineMsgId, tt.wantInlineMsgID)
}
if ctx.Text != "" {
t.Fatalf("prepareUpdateCtx must not populate Text, got %q", ctx.Text)
}
if len(ctx.Args) != 0 {
t.Fatalf("prepareUpdateCtx must not populate Args, got %v", ctx.Args)
}
})
}
}
func TestHandleUpdateHandlersPopulateFromContext(t *testing.T) {
tests := []struct {
name string
update *tgapi.Update
wantID int64
}{
{
name: "inline query",
update: &tgapi.Update{
UpdateID: 1,
Type: tgapi.UpdateTypeInlineQuery,
InlineQuery: &tgapi.InlineQuery{
ID: "iq",
From: tgapi.User{ID: 41},
Query: "ping",
},
},
wantID: 41,
},
{
name: "chosen inline result",
update: &tgapi.Update{
UpdateID: 2,
Type: tgapi.UpdateTypeChosenInlineResult,
ChosenInlineResult: &tgapi.ChosenInlineResult{
ResultID: "res",
From: tgapi.User{ID: 77},
Query: "pong",
},
},
wantID: 77,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test").AddUpdateHandler(tt.update.Type, func(ctx *MsgContext, db NoData) error {
called = true
if ctx.Update.UpdateID != tt.update.UpdateID {
t.Fatalf("unexpected update in context: got %d want %d", ctx.Update.UpdateID, tt.update.UpdateID)
}
if ctx.From == nil {
t.Fatal("expected ctx.From to be populated")
}
if ctx.FromID != tt.wantID {
t.Fatalf("unexpected FromID: got %d want %d", ctx.FromID, tt.wantID)
}
if ctx.From.ID != tt.wantID {
t.Fatalf("unexpected ctx.From.ID: got %d want %d", ctx.From.ID, tt.wantID)
}
if ctx.Msg != nil {
t.Fatalf("did not expect message context for %s", tt.name)
}
return nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
bot.handle(context.Background(), tt.update)
if !called {
t.Fatalf("expected update handler for %s to be called", tt.name)
}
})
}
}
func TestHandleUpdateHandlersReceiveIsolatedContexts(t *testing.T) {
firstCalled := false
secondCalled := false
first := NewPlugin[NoData]("first").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
firstCalled = true
if ctx.FromID != 41 {
t.Fatalf("unexpected FromID in first handler: got %d want 41", ctx.FromID)
}
ctx.From = nil
ctx.FromID = 999
ctx.Text = "mutated"
ctx.Args = []string{"mutated"}
return nil
})
second := NewPlugin[NoData]("second").AddUpdateHandler(tgapi.UpdateTypeInlineQuery, func(ctx *MsgContext, db NoData) error {
secondCalled = true
if ctx.From == nil {
t.Fatal("expected ctx.From to remain populated for second handler")
}
if ctx.FromID != 41 {
t.Fatalf("unexpected FromID in second handler: got %d want 41", ctx.FromID)
}
if ctx.Text != "" {
t.Fatalf("unexpected leaked Text in second handler: %q", ctx.Text)
}
if len(ctx.Args) != 0 {
t.Fatalf("unexpected leaked Args in second handler: %v", ctx.Args)
}
return nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
plugins: []Plugin[NoData]{
clonePlugin(first),
clonePlugin(second),
},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 3,
Type: tgapi.UpdateTypeInlineQuery,
InlineQuery: &tgapi.InlineQuery{
ID: "iq",
From: tgapi.User{ID: 41},
Query: "ping",
},
})
if !firstCalled || !secondCalled {
t.Fatalf("expected both handlers to be called, got first=%v second=%v", firstCalled, secondCalled)
}
}
func TestHandleChannelPostCommandWithSenderChat(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
called = true
if ctx.Msg == nil {
t.Fatal("expected message context")
}
if ctx.Msg.Chat == nil || ctx.Msg.Chat.ID != -1001 {
t.Fatalf("unexpected chat context: %#v", ctx.Msg.Chat)
}
if ctx.From != nil {
t.Fatalf("expected ctx.From to stay nil for sender_chat updates, got %#v", ctx.From)
}
if ctx.FromID != 0 {
t.Fatalf("expected zero FromID for sender_chat updates, got %d", ctx.FromID)
}
return nil
}, "ping")
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 10,
Type: tgapi.UpdateTypeChannelPost,
ChannelPost: &tgapi.Message{
MessageID: 55,
Text: "/ping",
SenderChat: &tgapi.Chat{ID: -1001, Type: string(tgapi.ChatTypeChannel)},
Chat: &tgapi.Chat{ID: -1001, Type: string(tgapi.ChatTypeChannel)},
},
})
if !called {
t.Fatal("expected channel post command handler to be called")
}
}
func TestCommandHandlerBindArgsEndToEnd(t *testing.T) {
type banInput struct {
UserID int
Reason string
}
var got banInput
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
return ctx.BindArgs(&got)
}, "ban",
NewCommandArg("user_id").SetValueType(CommandValueIntType).SetRequired(),
NewCommandArg("reason").SetRequired(),
)
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 11,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 1,
Text: "/ban 42 too loud",
Chat: &tgapi.Chat{ID: 99, Type: string(tgapi.ChatTypePrivate)},
},
})
want := banInput{UserID: 42, Reason: "too loud"}
if got != want {
t.Fatalf("unexpected bound input: got %#v want %#v", got, want)
}
}
func TestPayloadHandlerBindArgsEndToEnd(t *testing.T) {
type payloadInput struct {
ID int
Note string
}
var got payloadInput
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
return ctx.BindArgs(&got)
}, "approve",
NewCommandArg("id").SetValueType(CommandValueIntType).SetRequired(),
NewCommandArg("note").SetRequired(),
)
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
payloadType: BotPayloadJson,
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
data, err := encodeJsonPayload(CallbackData{
Command: "approve",
Args: []string{"7", "looks", "good"},
})
if err != nil {
t.Fatalf("encodeJsonPayload returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 12,
Type: tgapi.UpdateTypeCallbackQuery,
CallbackQuery: &tgapi.CallbackQuery{
ID: "cb-1",
Data: data,
From: tgapi.User{ID: 1},
},
})
want := payloadInput{ID: 7, Note: "looks good"}
if got != want {
t.Fatalf("unexpected bound payload input: got %#v want %#v", got, want)
}
}
func TestHandleEditedMessageStaysOutOfCommandFlow(t *testing.T) {
commandCalled := false
updateCalled := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedMessage, func(ctx *MsgContext, db NoData) error {
updateCalled = true
if ctx.Msg == nil {
t.Fatal("expected ctx.Msg in edited message handler")
}
if ctx.Text != "" {
t.Fatalf("expected empty Text in edited_message update handler, got %q", ctx.Text)
}
if len(ctx.Args) != 0 {
t.Fatalf("expected empty Args in edited_message update handler, got %v", ctx.Args)
}
return nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 20,
Type: tgapi.UpdateTypeEditedMessage,
EditedMessage: &tgapi.Message{
MessageID: 1,
Text: "/ping",
From: &tgapi.User{ID: 1},
Chat: &tgapi.Chat{ID: 42},
},
})
if commandCalled {
t.Fatal("edited_message must not enter command flow")
}
if !updateCalled {
t.Fatal("expected edited_message update handler to be called")
}
}
func TestHandleEditedChannelPostStaysOutOfCommandFlow(t *testing.T) {
commandCalled := false
updateCalled := false
plugin := NewPlugin[NoData]("test")
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
commandCalled = true
return nil
}, "ping")
plugin.AddUpdateHandler(tgapi.UpdateTypeEditedChannelPost, func(ctx *MsgContext, db NoData) error {
updateCalled = true
if ctx.Msg == nil {
t.Fatal("expected ctx.Msg in edited channel post handler")
}
return nil
})
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 21,
Type: tgapi.UpdateTypeEditedChannelPost,
EditedChannelPost: &tgapi.Message{
MessageID: 1,
Text: "/ping",
Chat: &tgapi.Chat{ID: -10042},
},
})
if commandCalled {
t.Fatal("edited_channel_post must not enter command flow")
}
if !updateCalled {
t.Fatal("expected edited_channel_post update handler to be called")
}
}
func TestHandleCallbackPopulatesMessageTargets(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
called = true
if ctx.CallbackQueryId != "cb-msg" {
t.Fatalf("unexpected CallbackQueryId: %q", ctx.CallbackQueryId)
}
if ctx.CallbackMsgId != 55 {
t.Fatalf("unexpected CallbackMsgId: %d", ctx.CallbackMsgId)
}
if ctx.InlineMsgId != "" {
t.Fatalf("did not expect InlineMsgId, got %q", ctx.InlineMsgId)
}
if ctx.Msg == nil {
t.Fatal("expected callback message context")
}
if ctx.From == nil || ctx.FromID != 7 {
t.Fatalf("unexpected callback sender: %#v / %d", ctx.From, ctx.FromID)
}
if ctx.Text != "" {
t.Fatalf("callback flow must not populate Text, got %q", ctx.Text)
}
if got, want := ctx.Args, []string{"7", "ok"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("unexpected callback args: got %v want %v", got, want)
}
return nil
}, "approve")
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
payloadType: BotPayloadJson,
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
data, err := encodeJsonPayload(CallbackData{Command: "approve", Args: []string{"7", "ok"}})
if err != nil {
t.Fatalf("encodeJsonPayload returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 30,
Type: tgapi.UpdateTypeCallbackQuery,
CallbackQuery: &tgapi.CallbackQuery{
ID: "cb-msg",
Data: data,
From: tgapi.User{ID: 7},
Message: &tgapi.Message{
MessageID: 55,
Chat: &tgapi.Chat{ID: 77},
},
},
})
if !called {
t.Fatal("expected payload handler to be called")
}
}
func TestHandleCallbackPopulatesInlineTargets(t *testing.T) {
called := false
plugin := NewPlugin[NoData]("test")
plugin.NewPayload(func(ctx *MsgContext, db NoData) error {
called = true
if ctx.CallbackQueryId != "cb-inline" {
t.Fatalf("unexpected CallbackQueryId: %q", ctx.CallbackQueryId)
}
if ctx.CallbackMsgId != 0 {
t.Fatalf("did not expect CallbackMsgId, got %d", ctx.CallbackMsgId)
}
if ctx.InlineMsgId != "inline-55" {
t.Fatalf("unexpected InlineMsgId: %q", ctx.InlineMsgId)
}
if ctx.Msg != nil {
t.Fatalf("did not expect callback chat message context, got %#v", ctx.Msg)
}
if ctx.From == nil || ctx.FromID != 8 {
t.Fatalf("unexpected callback sender: %#v / %d", ctx.From, ctx.FromID)
}
if ctx.Text != "" {
t.Fatalf("callback flow must not populate Text, got %q", ctx.Text)
}
if got, want := ctx.Args, []string{"9"}; len(got) != len(want) || got[0] != want[0] {
t.Fatalf("unexpected callback args: got %v want %v", got, want)
}
return nil
}, "inline.approve")
bot := &Bot[NoData]{
logger: slog.CreateLogger(),
payloadType: BotPayloadJson,
plugins: []Plugin[NoData]{clonePlugin(plugin)},
}
data, err := encodeJsonPayload(CallbackData{Command: "inline.approve", Args: []string{"9"}})
if err != nil {
t.Fatalf("encodeJsonPayload returned error: %v", err)
}
bot.handle(context.Background(), &tgapi.Update{
UpdateID: 31,
Type: tgapi.UpdateTypeCallbackQuery,
CallbackQuery: &tgapi.CallbackQuery{
ID: "cb-inline",
Data: data,
From: tgapi.User{ID: 8},
InlineMessageID: ptr("inline-55"),
},
})
if !called {
t.Fatal("expected inline payload handler to be called")
}
}