package service import ( "encoding/json" "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) // seedSocksInbound homes a socks-family inbound (fixed landing account in // its stored settings) on the shared clients-test agent node and attaches // the given clients through the DB link tables, the same source the // running config is built from. func seedSocksInbound(t *testing.T, tag string, port int, protocol model.Protocol, settings string, clients []model.Client) { t.Helper() setupSettingTestDB(t) db := database.GetDB() nodeID := clientsTestNodeID(t, db) in := &model.Inbound{ Tag: tag, Enable: true, Port: port, Protocol: protocol, Settings: settings, NodeID: &nodeID, } if err := db.Create(in).Error; err != nil { t.Fatalf("create socks inbound: %v", err) } svc := ClientService{} if err := svc.SyncInbound(nil, in.Id, clients); err != nil { t.Fatalf("SyncInbound: %v", err) } } func emittedSettings(t *testing.T, tag string) map[string]any { t.Helper() svc := &XrayService{} cfg, err := svc.GetXrayConfigForAgent(clientsTestNodeID(t, database.GetDB()), "", "") if err != nil { t.Fatalf("GetXrayConfigForAgent: %v", err) } for i := range cfg.InboundConfigs { if cfg.InboundConfigs[i].Tag != tag { continue } var s map[string]any if err := json.Unmarshal([]byte(cfg.InboundConfigs[i].Settings), &s); err != nil { t.Fatalf("unmarshal emitted settings: %v", err) } return s } t.Fatalf("inbound %q not found in generated config", tag) return nil } func accountUsers(t *testing.T, settings map[string]any) []map[string]any { t.Helper() raw, ok := settings["accounts"].([]any) if !ok { t.Fatalf("settings.accounts must be an array, got %T", settings["accounts"]) } out := make([]map[string]any, 0, len(raw)) for _, a := range raw { m, ok := a.(map[string]any) if !ok { t.Fatalf("account entry must be an object, got %T", a) } out = append(out, m) } return out } func TestGetXrayConfig_SocksClientsEmittedAsAccounts(t *testing.T) { seedSocksInbound(t, "mixed-clients", 43201, model.Mixed, `{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true}`, []model.Client{ {Email: "user1@x", Password: "pass-1", Enable: true}, {Email: "user2@x", Password: "pass-2", Enable: true}, {Email: "off@x", Password: "pass-3", Enable: true}, }) disableClients(t, "off@x") settings := emittedSettings(t, "mixed-clients") if _, ok := settings["clients"]; ok { t.Fatal("settings.clients must not be emitted for a mixed inbound") } accounts := accountUsers(t, settings) if len(accounts) != 3 { t.Fatalf("expected fixed account + both enabled clients (disabled one excluded), got %d: %#v", len(accounts), accounts) } if accounts[0]["user"] != "fixed@landing" || accounts[0]["pass"] != "fixed-pass" { t.Errorf("the inbound's own account must stay first and untouched: %#v", accounts[0]) } if accounts[1]["user"] != "user1@x" || accounts[1]["pass"] != "pass-1" { t.Errorf("client account must be keyed by email with the client password: %#v", accounts[1]) } if accounts[2]["user"] != "user2@x" || accounts[2]["pass"] != "pass-2" { t.Errorf("second client account must follow in order: %#v", accounts[2]) } if settings["auth"] != "password" { t.Errorf("auth must be password when clients are attached, got %v", settings["auth"]) } } func TestGetXrayConfig_SocksClientsForcePasswordAuth(t *testing.T) { seedSocksInbound(t, "mixed-noauth", 43202, model.Mixed, `{"auth":"noauth","udp":true}`, []model.Client{ {Email: "user1@x", Password: "pass-1", Enable: true}, }) settings := emittedSettings(t, "mixed-noauth") accounts := accountUsers(t, settings) if len(accounts) != 1 || accounts[0]["user"] != "user1@x" { t.Fatalf("expected the single client account, got %#v", accounts) } if settings["auth"] != "password" { t.Errorf("attaching clients must switch a noauth inbound to password auth, got %v", settings["auth"]) } } func TestGetXrayConfig_LegacySocksClientsEmittedAsAccounts(t *testing.T) { seedSocksInbound(t, "socks-legacy", 43203, model.Protocol("socks"), `{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true}`, []model.Client{ {Email: "user1@x", Password: "pass-1", Enable: true}, }) settings := emittedSettings(t, "socks-legacy") accounts := accountUsers(t, settings) if len(accounts) != 2 || accounts[1]["user"] != "user1@x" { t.Fatalf("legacy socks rows must emit client accounts too, got %#v", accounts) } } func TestGetXrayConfig_SocksWithoutClientsUntouched(t *testing.T) { stored := `{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true}` seedSocksInbound(t, "mixed-bare", 43204, model.Mixed, stored, nil) svc := &XrayService{} cfg, err := svc.GetXrayConfigForAgent(clientsTestNodeID(t, database.GetDB()), "", "") if err != nil { t.Fatalf("GetXrayConfigForAgent: %v", err) } for i := range cfg.InboundConfigs { if cfg.InboundConfigs[i].Tag != "mixed-bare" { continue } if string(cfg.InboundConfigs[i].Settings) != stored { t.Fatalf("a clientless mixed inbound's settings must pass through verbatim:\nstored: %s\nemitted: %s", stored, cfg.InboundConfigs[i].Settings) } return } t.Fatal("inbound mixed-bare not found in generated config") }