package service import ( "encoding/json" "os" "path/filepath" "sort" "strings" "testing" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) // The frontend's golden fixtures are the panel's model of an xray config: the // Zod snapshots pin what the forms parse and emit. Until the lite peel, every // fixture was also built here through xray-core's config builders // (conf.InboundDetourConfig / conf.RouterConfig / conf.DNSConfig) to prove // the core would accept what the panel produces. That cross-check retired // with the library: this build has no in-process core builder, and config // acceptance is enforced by the core on the node when it starts. What stays // is the panel-side contract — every fixture still parses, and every full // inbound fixture still flows through GenXrayInboundConfig (the conversion // the full-config path uses) with its identity intact. // // mtproto fixtures are excluded from generation: it is served by the // bundled mtg-multi sidecar, not by xray. func goldenFixtureDir(t *testing.T, category string) string { t.Helper() dir, err := filepath.Abs(filepath.Join("..", "..", "..", "frontend", "src", "test", "golden", "fixtures", category)) if err != nil { t.Fatalf("resolve fixture dir: %v", err) } return dir } // goldenFixtures returns every fixture in a category as name -> decoded object. func goldenFixtures(t *testing.T, category string) map[string]map[string]any { t.Helper() dir := goldenFixtureDir(t, category) entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("read %s: %v", dir, err) } out := make(map[string]map[string]any) names := make([]string, 0, len(entries)) for _, entry := range entries { if filepath.Ext(entry.Name()) != ".json" { continue } names = append(names, entry.Name()) } sort.Strings(names) if len(names) == 0 { t.Fatalf("no fixtures under %s", dir) } for _, name := range names { raw, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { t.Fatalf("read %s: %v", name, err) } var obj map[string]any if err := json.Unmarshal(raw, &obj); err != nil { t.Fatalf("unmarshal %s: %v", name, err) } out[strings.TrimSuffix(name, ".json")] = obj } return out } // TestGoldenFixturesParse keeps the whole corpus exercised: a fixture that // stops being a valid JSON object fails here even in a category whose only // consumer is the frontend. func TestGoldenFixturesParse(t *testing.T) { for _, category := range []string{"inbound", "inbound-full", "stream", "security", "sockopt", "finalmask", "rule", "balancer", "dns", "dns-server"} { t.Run(category, func(t *testing.T) { if fixtures := goldenFixtures(t, category); len(fixtures) == 0 { t.Fatal("no fixtures parsed") } }) } } // TestGoldenInboundFullFixturesGenerate runs each complete panel Inbound // fixture through GenXrayInboundConfig and checks the generated config keeps // the fixture's identity: protocol, port, tag and a non-empty settings // object. (The full-output equivalence of this path against the previous // build is covered by the phase-2 golden diff at the binary level.) func TestGoldenInboundFullFixturesGenerate(t *testing.T) { for name, fixture := range goldenFixtures(t, "inbound-full") { protocol, _ := fixture["protocol"].(string) if protocol == string(model.MTProto) { continue } t.Run(name, func(t *testing.T) { section := func(key string) string { value, ok := fixture[key] if !ok || value == nil { return "" } raw, err := json.Marshal(value) if err != nil { t.Fatalf("marshal %s: %v", key, err) } return string(raw) } port := 0 if p, ok := fixture["port"].(float64); ok { port = int(p) } listen, _ := fixture["listen"].(string) tag, _ := fixture["tag"].(string) ib := &model.Inbound{ Protocol: model.Protocol(protocol), Port: port, Listen: listen, Tag: tag, Settings: section("settings"), StreamSettings: section("streamSettings"), Sniffing: section("sniffing"), } raw, err := json.Marshal(ib.GenXrayInboundConfig()) if err != nil { t.Fatalf("marshal generated inbound: %v", err) } var generated map[string]any if err := json.Unmarshal(raw, &generated); err != nil { t.Fatalf("decode generated inbound: %v", err) } if generated["protocol"] != protocol { t.Errorf("protocol = %v, want %q", generated["protocol"], protocol) } if port != 0 { if got, ok := generated["port"].(float64); !ok || int(got) != port { t.Errorf("port = %v, want %d", generated["port"], port) } } if tag != "" && generated["tag"] != tag { t.Errorf("tag = %v, want %q", generated["tag"], tag) } settings, ok := generated["settings"].(map[string]any) if !ok || len(settings) == 0 { t.Errorf("settings = %v, want a non-empty object", generated["settings"]) } }) } }