Files

149 lines
4.8 KiB
Go

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"])
}
})
}
}