Files
3x-ui/internal/web/service/tgbot/tgbot_groups_test.go
T
DIMFLIX 15fee2a035 feat(tgbot): rebuild the Telegram bot around editable screens and inline lists (#6760)
The bot answered every action with a new message, so a chat filled with stale
menus whose buttons still acted, and long client or inbound lists were paged
through dozens of buttons. Each chat now has one live screen that is edited in
place: the menu is grouped into Server, Clients, Traffic and Maintenance, lists
open through inline search, the add-client flow starts from a draft card, QR
codes arrive as one labelled sheet, and repeated notices collapse into one with
a counter. A client's /start and /usage show only that client's own links and
usage, groups are refused, and the settings page warns when the bot's inline
mode is off. Admin topics, the indexed Telegram lookup and every locale,
Slovak included, carry over.
2026-10-24 12:39:56 +02:00

131 lines
4.2 KiB
Go

package tgbot
import (
"context"
"testing"
"github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler"
)
const testAdminID = 1
func privateMessage(id int, text string) telego.Update {
return telego.Update{UpdateID: id, Message: &telego.Message{
MessageID: id, Text: text,
Chat: telego.Chat{ID: testAdminID, Type: telego.ChatTypePrivate},
From: &telego.User{ID: testAdminID, FirstName: "a"},
}}
}
func groupMessage(id int, text string) telego.Update {
return telego.Update{UpdateID: id, Message: &telego.Message{
MessageID: id, Text: text,
Chat: telego.Chat{ID: -900, Type: telego.ChatTypeSupergroup},
From: &telego.User{ID: testAdminID, FirstName: "a"},
}}
}
func groupCallback(id int, data string) telego.Update {
return telego.Update{UpdateID: id, CallbackQuery: &telego.CallbackQuery{
ID: "cq", Data: data,
From: telego.User{ID: testAdminID, FirstName: "a"},
Message: &telego.Message{
MessageID: id,
Chat: telego.Chat{ID: -900, Type: telego.ChatTypeSupergroup},
},
}}
}
// A group shares one screen and keyboard among all members, so admins and clients overwrite each other;
// the guard must answer a group and stop it before any screen is drawn.
func TestGroupUpdatesAreRefused(t *testing.T) {
tb, calls := newScreenTgbot(t, false)
h, err := th.NewBotHandler(liveBot(), make(chan telego.Update, 1))
if err != nil {
t.Fatalf("NewBotHandler: %v", err)
}
refuseGroups(h, tb)
for _, update := range []telego.Update{
groupMessage(1, "/start"),
groupCallback(2, cbHome),
{UpdateID: 3, InlineQuery: &telego.InlineQuery{ID: "q", ChatType: telego.ChatTypeGroup}},
} {
if err := h.BaseGroup().HandleUpdate(context.Background(), liveBot(), update); err != nil {
t.Fatalf("HandleUpdate: %v", err)
}
}
got := calls()
if indexOf(got, "sendPhoto") >= 0 || indexOf(got, "editMessageText") >= 0 {
t.Fatalf("a group drew a screen: %v", methods(got))
}
want := tb.I18nBot("tgbot.messages.privateOnly")
sawNotice := false
for _, c := range got {
if c.Method == "sendMessage" && c.text() == want {
sawNotice = true
}
}
if !sawNotice {
t.Fatalf("the group was not told why nothing happened: %v", methods(got))
}
if indexOf(got, "answerCallbackQuery") < 0 {
t.Fatalf("the group callback was left unanswered: %v", methods(got))
}
if indexOf(got, "answerInlineQuery") < 0 {
t.Fatalf("the group inline query was left unanswered: %v", methods(got))
}
}
// A private update must fall through the guard untouched: the close-keyboard
// route runs synchronously, so its delete is visible without a worker goroutine.
func TestPrivateChatIsNotRefused(t *testing.T) {
tb, calls := newScreenTgbot(t, false)
h, err := th.NewBotHandler(liveBot(), make(chan telego.Update, 1))
if err != nil {
t.Fatalf("NewBotHandler: %v", err)
}
refuseGroups(h, tb)
h.HandleMessage(func(*th.Context, telego.Message) error { return nil },
th.TextEqual(tb.I18nBot("tgbot.buttons.closeKeyboard")))
if err := h.BaseGroup().HandleUpdate(context.Background(), liveBot(),
privateMessage(1, tb.I18nBot("tgbot.buttons.closeKeyboard"))); err != nil {
t.Fatalf("HandleUpdate: %v", err)
}
got := calls()
for _, c := range got {
if c.text() == tb.I18nBot("tgbot.messages.privateOnly") {
t.Fatalf("a private chat was refused: %v", methods(got))
}
}
}
// updateChatType is the guard's only input, so it is pinned per update shape.
func TestUpdateChatType(t *testing.T) {
cases := []struct {
name string
up telego.Update
want string
}{
{"private message", privateMessage(1, "x"), telego.ChatTypePrivate},
{"group message", groupMessage(1, "x"), telego.ChatTypeSupergroup},
{"inline from private chat (sender)", telego.Update{InlineQuery: &telego.InlineQuery{
ID: "q", ChatType: "sender",
}}, telego.ChatTypePrivate},
{"inline from a group", telego.Update{InlineQuery: &telego.InlineQuery{
ID: "q", ChatType: telego.ChatTypeGroup,
}}, telego.ChatTypeGroup},
{"empty inline chat type", telego.Update{InlineQuery: &telego.InlineQuery{ID: "q"}}, telego.ChatTypePrivate},
{"unknown update", telego.Update{}, ""},
}
for _, tc := range cases {
if got := updateChatType(tc.up); got != tc.want {
t.Errorf("%s: updateChatType = %q, want %q", tc.name, got, tc.want)
}
}
}