mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-30 19:22:10 +03:00
12d51d7195
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.
210 lines
6.1 KiB
Go
210 lines
6.1 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"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"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const visionTest = "xtls-rprx-vision"
|
|
|
|
func clientFlowsInSettings(t *testing.T, settings string) map[string]string {
|
|
t.Helper()
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
|
t.Fatalf("parse settings: %v", err)
|
|
}
|
|
out := map[string]string{}
|
|
clients, _ := parsed["clients"].([]any)
|
|
for _, c := range clients {
|
|
cm, ok := c.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
email, _ := cm["email"].(string)
|
|
flow, _ := cm["flow"].(string)
|
|
out[email] = flow
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestStripClientFlows(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
wantChanged bool
|
|
wantFlows map[string]string
|
|
}{
|
|
{
|
|
name: "clears vision on all clients",
|
|
in: `{"clients":[{"email":"a","flow":"` + visionTest + `"},{"email":"b","flow":"` + visionTest + `"}]}`,
|
|
wantChanged: true,
|
|
wantFlows: map[string]string{"a": "", "b": ""},
|
|
},
|
|
{
|
|
name: "mixed flows: clears only the non-empty",
|
|
in: `{"clients":[{"email":"a","flow":"` + visionTest + `"},{"email":"b","flow":""}]}`,
|
|
wantChanged: true,
|
|
wantFlows: map[string]string{"a": "", "b": ""},
|
|
},
|
|
{
|
|
name: "no flows: unchanged",
|
|
in: `{"clients":[{"email":"a","flow":""},{"email":"b"}]}`,
|
|
wantChanged: false,
|
|
wantFlows: map[string]string{"a": "", "b": ""},
|
|
},
|
|
{
|
|
name: "no clients: unchanged",
|
|
in: `{"decryption":"none"}`,
|
|
wantChanged: false,
|
|
},
|
|
{
|
|
name: "malformed json: unchanged",
|
|
in: `{not json`,
|
|
wantChanged: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
out, changed := stripClientFlows(tc.in)
|
|
if changed != tc.wantChanged {
|
|
t.Fatalf("changed = %v, want %v", changed, tc.wantChanged)
|
|
}
|
|
if !changed {
|
|
if out != tc.in {
|
|
t.Fatalf("unchanged input must be returned verbatim, got %q", out)
|
|
}
|
|
return
|
|
}
|
|
got := clientFlowsInSettings(t, out)
|
|
for email, want := range tc.wantFlows {
|
|
if got[email] != want {
|
|
t.Errorf("flow[%s] = %q, want %q", email, got[email], want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func initFlowTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
return database.GetDB()
|
|
}
|
|
|
|
func TestAddInbound_DisableFlowClampsClientFlow(t *testing.T) {
|
|
initFlowTestDB(t)
|
|
ibSvc := &InboundService{}
|
|
|
|
in := &model.Inbound{
|
|
Tag: "dis-add", Enable: true, Port: 52001, Protocol: model.VLESS,
|
|
StreamSettings: `{"network":"tcp","security":"reality"}`,
|
|
Settings: `{"clients":[{"id":"u1","email":"a@x","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
|
|
DisableFlow: true,
|
|
}
|
|
if _, _, err := ibSvc.AddInbound(in); err != nil {
|
|
t.Fatalf("AddInbound: %v", err)
|
|
}
|
|
|
|
got, err := ibSvc.GetInbound(in.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetInbound: %v", err)
|
|
}
|
|
if !got.DisableFlow {
|
|
t.Error("DisableFlow not persisted on created inbound")
|
|
}
|
|
if f := clientFlowsInSettings(t, got.Settings)["a@x"]; f != "" {
|
|
t.Errorf("settings flow = %q, want empty (clamped at creation)", f)
|
|
}
|
|
list, err := ibSvc.clientService.ListForInbound(nil, in.Id)
|
|
if err != nil {
|
|
t.Fatalf("ListForInbound: %v", err)
|
|
}
|
|
if len(list) != 1 || list[0].Flow != "" {
|
|
t.Errorf("flow_override = %#v, want empty (xray must not expect Vision)", list)
|
|
}
|
|
}
|
|
|
|
func TestUpdateInbound_DisableFlowPersistsStripsAndResistsRestore(t *testing.T) {
|
|
db := initFlowTestDB(t)
|
|
ibSvc := &InboundService{}
|
|
cs := &ClientService{}
|
|
|
|
const email = "shared@x"
|
|
const uid = "ce8d33df-3a64-4f10-8f9b-91c3a8e0d001"
|
|
|
|
sibling := &model.Inbound{
|
|
Tag: "sib", Enable: true, Port: 52101, Protocol: model.VLESS,
|
|
StreamSettings: `{"network":"tcp","security":"reality"}`,
|
|
Settings: `{"clients":[{"id":"` + uid + `","email":"` + email + `","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
|
|
}
|
|
if err := db.Create(sibling).Error; err != nil {
|
|
t.Fatalf("create sibling: %v", err)
|
|
}
|
|
sc, _ := ibSvc.GetClients(sibling)
|
|
if err := cs.SyncInbound(nil, sibling.Id, sc); err != nil {
|
|
t.Fatalf("sync sibling: %v", err)
|
|
}
|
|
|
|
target := &model.Inbound{
|
|
Tag: "tgt", Enable: true, Port: 52102, Protocol: model.VLESS,
|
|
StreamSettings: `{"network":"tcp","security":"reality"}`,
|
|
Settings: `{"clients":[{"id":"` + uid + `","email":"` + email + `","flow":"` + visionTest + `","subId":"s1","enable":true}]}`,
|
|
}
|
|
if err := db.Create(target).Error; err != nil {
|
|
t.Fatalf("create target: %v", err)
|
|
}
|
|
tc, _ := ibSvc.GetClients(target)
|
|
if err := cs.SyncInbound(nil, target.Id, tc); err != nil {
|
|
t.Fatalf("sync target: %v", err)
|
|
}
|
|
|
|
upd := *target
|
|
upd.DisableFlow = true
|
|
if _, _, err := ibSvc.UpdateInbound(&upd); err != nil {
|
|
t.Fatalf("UpdateInbound: %v", err)
|
|
}
|
|
|
|
reloaded, err := ibSvc.GetInbound(target.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetInbound: %v", err)
|
|
}
|
|
if !reloaded.DisableFlow {
|
|
t.Fatal("DisableFlow did not persist through UpdateInbound (blocking regression)")
|
|
}
|
|
if f := clientFlowsInSettings(t, reloaded.Settings)["shared@x"]; f != "" {
|
|
t.Errorf("target settings flow = %q, want empty after disable", f)
|
|
}
|
|
list, err := cs.ListForInbound(nil, target.Id)
|
|
if err != nil {
|
|
t.Fatalf("ListForInbound(target): %v", err)
|
|
}
|
|
if len(list) != 1 || list[0].Flow != "" {
|
|
t.Errorf("target flow_override = %#v, want empty", list)
|
|
}
|
|
|
|
ibSvc.MigrationRestoreVisionFlow()
|
|
reloaded2, err := ibSvc.GetInbound(target.Id)
|
|
if err != nil {
|
|
t.Fatalf("GetInbound after restore: %v", err)
|
|
}
|
|
if f := clientFlowsInSettings(t, reloaded2.Settings)["shared@x"]; f != "" {
|
|
t.Errorf("after MigrationRestoreVisionFlow target flow = %q, want empty (must not self-revert)", f)
|
|
}
|
|
sList, err := cs.ListForInbound(nil, sibling.Id)
|
|
if err != nil {
|
|
t.Fatalf("ListForInbound(sibling): %v", err)
|
|
}
|
|
if len(sList) != 1 || sList[0].Flow != visionTest {
|
|
t.Errorf("sibling flow_override = %#v, want Vision preserved", sList)
|
|
}
|
|
}
|