mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 23:35:00 +03:00
15fee2a035
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.
139 lines
4.6 KiB
Go
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")
|
|
}
|
|
}
|