Files
3x-ui/internal/web/service/tgbot/tgbot_add_client_picker_test.go
MHSanaei 12d51d7195 perf(tests): copy a migrated template DB instead of migrating per test
Most tests opened a throwaway panel DB with database.InitDB, which runs the
full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under
-race because GORM's reflection-heavy migration is what the detector slows
most. internal/web/service does this in ~550 of its 830 tests, so the CI race
job spent ~10 of its ~14.6 minutes re-migrating empty databases.

internal/database/dbtest.InitDB migrates once per test process, then hands
each test its own copy of that file (~130ms under -race) and registers the
CloseDB cleanup. The copy then goes through InitDB like a panel restart, so
every test still starts from the state a fresh install has. Tests that reopen
an existing file, migrate a hand-built legacy DB or target Postgres keep
calling database.InitDB.

Locally under -race: internal/web/service 626s (last CI run) -> 114s,
internal/sub 246s -> 35s.
2026-09-27 03:04:50 +02:00

65 lines
2.1 KiB
Go

package tgbot
import (
"fmt"
"path/filepath"
"slices"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mymmrac/telego"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
func seedPickerInbounds(t *testing.T, protocols ...model.Protocol) {
t.Helper()
dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
for i, protocol := range protocols {
port := 20000 + i
ib := &model.Inbound{Remark: string(protocol), Enable: true, Port: port, Protocol: protocol, Tag: fmt.Sprintf("inbound-%d", port), Settings: `{}`}
if err := database.GetDB().Create(ib).Error; err != nil {
t.Fatalf("seed %s inbound: %v", protocol, err)
}
}
}
func pickerLabels(keyboard *telego.InlineKeyboardMarkup) []string {
var labels []string
for _, row := range keyboard.InlineKeyboard {
for _, button := range row {
labels = append(labels, button.Text)
}
}
slices.Sort(labels)
return labels
}
// WireGuard and AmneziaWG clients get a generated keypair and address on Create,
// so the wizard offers them; Mixed authenticates per inbound and has no clients.
func TestAddClientPickerOffersWireGuardAndAmneziaWG(t *testing.T) {
seedPickerInbounds(t, model.VLESS, model.WireGuard, model.AmneziaWG, model.Mixed)
keyboard, err := (&Tgbot{}).getInboundsAddClient()
if err != nil {
t.Fatalf("getInboundsAddClient: %v", err)
}
want := []string{"amneziawg - ✅", "vless - ✅", "wireguard - ✅"}
if got := pickerLabels(keyboard); !slices.Equal(got, want) {
t.Fatalf("picker buttons = %q, want %q", got, want)
}
}
// An empty keyboard sent the admin a "choose inbound" prompt with nothing to tap.
func TestAddClientPickerFailsWhenNoInboundTakesClients(t *testing.T) {
draftLocalizer(t, &i18n.Message{ID: "tgbot.answers.getInboundsFailed", Other: "Failed to get inbounds."})
seedPickerInbounds(t, model.Mixed, model.HTTP, model.Tunnel)
keyboard, err := (&Tgbot{}).getInboundsAddClient()
if err == nil || err.Error() != "Failed to get inbounds." {
t.Fatalf("getInboundsAddClient = (%v, %v), want the getInboundsFailed error", keyboard, err)
}
}