149 lines
4.8 KiB
Go
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"])
|
|
}
|
|
})
|
|
}
|
|
}
|