Files
3x-ui/internal/web/service/tgbot/tgbot_add_client_wizard_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

139 lines
4.6 KiB
Go

package tgbot
import (
"encoding/json"
"slices"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mymmrac/telego"
)
// seedPickerInbounds inserts one enabled inbound per protocol.
func seedPickerInbounds(t *testing.T, protocols ...model.Protocol) {
t.Helper()
for _, protocol := range protocols {
tag := "pick-" + string(protocol)
if err := database.GetDB().Create(&model.Inbound{
UserId: 1, Tag: tag, Remark: string(protocol), Port: 8443,
Protocol: protocol, Enable: true,
}).Error; err != nil {
t.Fatalf("seed %s inbound: %v", protocol, err)
}
}
}
// attachPickerRows builds the attach picker for a draft and flattens it.
func attachPickerRows(t *testing.T, tb *Tgbot, draft *clientDraft) []telego.InlineKeyboardButton {
t.Helper()
picker, err := tb.getInboundsAttachPicker(draft)
if err != nil {
t.Fatalf("getInboundsAttachPicker: %v", err)
}
var rows []telego.InlineKeyboardButton
for _, row := range picker.InlineKeyboard {
rows = append(rows, row...)
}
return rows
}
// WireGuard and AmneziaWG clients get a generated keypair and address on Create,
// so the attach picker offers them; Mixed authenticates per inbound and has none.
func TestAttachPickerOffersWireGuardAndAmneziaWG(t *testing.T) {
tb, _ := newScreenTgbot(t, false)
initReportDB(t)
seedPickerInbounds(t, model.VLESS, model.WireGuard, model.AmneziaWG, model.Mixed)
draft := &clientDraft{}
// The picker marks selection, so its labels carry the checkbox and protocol.
rows := attachPickerRows(t, tb, draft)
var labels []string
for _, row := range rows {
labels = append(labels, row.Text)
}
slices.Sort(labels)
for _, want := range []string{"amneziawg", "vless", "wireguard"} {
found := false
for _, label := range labels {
if strings.Contains(label, want) {
found = true
}
}
if !found {
t.Errorf("the attach picker does not offer %s: %q", want, labels)
}
}
for _, label := range labels {
if strings.Contains(label, "mixed") {
t.Errorf("the attach picker offers Mixed, which has no clients: %q", label)
}
}
}
// The wizard starts on a draft, not an inbound picker: the client is created first and attached after,
// the bug that made "Add client" ask for an inbound and then offer existing clients.
func TestAddClientStartsOnADraftWithNoInboundChosen(t *testing.T) {
localizeWithRealBundle(t)
tb, _ := newScreenTgbot(t, false)
tb.screenAddClientStart(777, 1)
sc, ok := tb.screens().get(777)
if !ok {
t.Fatal("the add-client flow drew no screen")
}
// The draft card is shown, and nothing is attached yet.
if !strings.Contains(sc.pages[0], "newclient") && !strings.Contains(sc.pages[0], "Email") {
t.Errorf("the first screen is not the draft card: %s", sc.pages[0])
}
if strings.Contains(sc.pages[0], "pickInbound") || strings.Contains(sc.pages[0], "Pick the inbound") {
t.Errorf("the flow still opens on an inbound picker: %s", sc.pages[0])
}
markup, _ := json.Marshal(sc.markup)
if !strings.Contains(string(markup), "add_client_attach_more") {
t.Errorf("the draft offers no way to attach an inbound: %s", markup)
}
if strings.Contains(string(markup), "add_client_ch_default_email") == false {
t.Errorf("the draft offers no way to set the client's fields: %s", markup)
}
}
// Inbounds are chosen from the draft's own button while filling the card, and submitting without one is
// refused instead of creating a client attached to nothing.
func TestDraftCanBeFilledBeforeAnyInboundIsAttached(t *testing.T) {
localizeWithRealBundle(t)
tb, _ := newScreenTgbot(t, false)
draft := &clientDraft{email: "draft-only"}
rows := tb.getCommonClientButtons(draft)
markup, _ := json.Marshal(rows)
for _, want := range []string{
"add_client_ch_default_email", "add_client_ch_default_comment",
"add_client_ch_default_traffic", "add_client_ch_default_exp",
"add_client_ch_default_ip_limit", "add_client_ch_default_tg_id",
"add_client_attach_more", "add_client_submit_enable", "add_client_cancel",
} {
if !strings.Contains(string(markup), want) {
t.Errorf("the draft card lacks %s", want)
}
}
// The attach button says how many are attached, and it is localized.
for _, row := range rows {
for _, button := range row {
if button.CallbackData != "add_client_attach_more" {
continue
}
if strings.TrimSpace(button.Text) == "" || strings.HasPrefix(button.Text, "tgbot.") {
t.Errorf("the attach button has no localized label: %q", button.Text)
}
}
}
if _, err := tb.SubmitAddClient(draft); err == nil {
t.Error("submitting with no inbound attached created a client anyway")
}
}