精简P2: 下线 infra/conf 交叉验证类测试: golden fixtures 改为面板侧契约(全语料可解析+inbound-full 经 GenXrayInboundConfig 生成后身份保持), 文件更名 golden_fixtures_test.go; XMC 完整性测试由与核心对拍改为面板谓词自身规则表(用户名正则/uuid 可解析/textures 非空); healed-config 测试改为断言面板 heal 结果(version 钉 2/空 packet 被清). 核心接受性今后由节点核心启动时把关
This commit is contained in:
@@ -107,9 +107,13 @@ func TestDropEmptyRandPacketsIgnoresMissingFinalMask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealedConfigsBuildInXray closes the loop on both heals: the rows xray
|
||||
// refuses outright must build once the panel has healed them.
|
||||
func TestHealedConfigsBuildInXray(t *testing.T) {
|
||||
// TestHealedConfigsCarryPanelHeals closes the loop on both heals at the
|
||||
// panel level: the generated config must carry the healed shape. Until the
|
||||
// lite peel this test also built the results in xray-core (and asserted the
|
||||
// unhealed rows were refused); with the library gone, the heals themselves
|
||||
// are what's pinned — the core still refuses the unhealed shapes when it
|
||||
// starts on the node, which is exactly why the heals exist.
|
||||
func TestHealedConfigsCarryPanelHeals(t *testing.T) {
|
||||
t.Run("hysteria v1 row", func(t *testing.T) {
|
||||
in := model.Inbound{
|
||||
Protocol: model.Hysteria,
|
||||
@@ -128,33 +132,36 @@ func TestHealedConfigsBuildInXray(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &healed); err != nil {
|
||||
t.Fatalf("decode generated inbound: %v", err)
|
||||
}
|
||||
assertXrayAccepts(t, "the healed hysteria inbound", buildGoldenInbound(t, healed))
|
||||
|
||||
var unhealed map[string]any
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"tag":"in-hysteria","listen":"127.0.0.1","port":36715,"protocol":"hysteria",
|
||||
"settings":`+in.Settings+`,"streamSettings":`+in.StreamSettings+`}`), &unhealed); err != nil {
|
||||
t.Fatalf("decode raw inbound: %v", err)
|
||||
settings, _ := healed["settings"].(map[string]any)
|
||||
if settings["version"] != float64(2) {
|
||||
t.Fatalf("generated settings version = %v, want the heal to pin 2", settings["version"])
|
||||
}
|
||||
if err := buildGoldenInbound(t, unhealed); err == nil {
|
||||
t.Fatal("the unhealed v1 row is expected to be refused; the heal is what makes it buildable")
|
||||
stream, _ := healed["streamSettings"].(map[string]any)
|
||||
hysteriaSettings, _ := stream["hysteriaSettings"].(map[string]any)
|
||||
if hysteriaSettings["version"] != float64(2) {
|
||||
t.Fatalf("generated hysteriaSettings version = %v, want the heal to pin 2", hysteriaSettings["version"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("noise item with an empty packet", func(t *testing.T) {
|
||||
item := map[string]any{"type": "array", "rand": "1-8192", "packet": []any{}, "delay": "5"}
|
||||
stream := streamWithNoiseItem(t, item)
|
||||
inbound := map[string]any{
|
||||
"tag": "in-vless", "listen": "127.0.0.1", "port": 8443, "protocol": "vless",
|
||||
"settings": map[string]any{"clients": []any{}, "decryption": "none"},
|
||||
"streamSettings": stream,
|
||||
}
|
||||
if err := buildGoldenInbound(t, inbound); err == nil {
|
||||
t.Fatal("xray-core is expected to refuse a packet and a rand on one item")
|
||||
}
|
||||
|
||||
dropEmptyRandPackets(stream["finalmask"])
|
||||
inbound["streamSettings"] = stream
|
||||
assertXrayAccepts(t, "the healed noise mask", buildGoldenInbound(t, inbound))
|
||||
// The unhealed item carries the rand+packet combination the core
|
||||
// refuses; the heal must remove the empty packet and keep the rest.
|
||||
before := noiseItem(t, stream)
|
||||
if _, present := before["packet"]; !present {
|
||||
t.Fatal("fixture lost its packet key before the heal")
|
||||
}
|
||||
if cleared := dropEmptyRandPackets(stream["finalmask"]); cleared != 1 {
|
||||
t.Fatalf("cleared = %d, want 1", cleared)
|
||||
}
|
||||
after := noiseItem(t, stream)
|
||||
if _, present := after["packet"]; present {
|
||||
t.Fatalf("packet survived the heal: %#v", after)
|
||||
}
|
||||
if after["rand"] != "1-8192" || after["delay"] != "5" {
|
||||
t.Fatalf("healing changed the mask: %#v", after)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
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"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"maps"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
)
|
||||
|
||||
// The frontend's golden fixtures are the panel's model of an xray config: the
|
||||
// Zod snapshots pin what the forms parse and emit. Parsing proves the panel
|
||||
// agrees with itself, not that xray-core would accept the result, so every
|
||||
// fixture is also built here through the very config builders the panel hands
|
||||
// its config to — conf.InboundDetourConfig for the full-config and AddInbound
|
||||
// paths, conf.RouterConfig for ApplyRoutingConfig, conf.DNSConfig for the dns
|
||||
// section. A fixture xray-core refuses is a config the panel would let an
|
||||
// admin save and then fail to start the core with, taking every inbound down.
|
||||
//
|
||||
// mtproto is excluded: it is served by the bundled mtg-multi sidecar, not by
|
||||
// xray, so xray-core has no config id for it.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// writeTestCertificate writes a self-signed certificate and key, returning both
|
||||
// paths. The TLS fixtures point certificateFile/keyFile at deployment paths
|
||||
// that do not exist here, and xray-core reads them while building.
|
||||
func writeTestCertificate(t *testing.T) (certPath, keyPath string) {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "golden-fixture.test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
DNSNames: []string{"golden-fixture.test"},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatalf("create certificate: %v", err)
|
||||
}
|
||||
keyDER, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal key: %v", err)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
certPath = filepath.Join(dir, "fixture.crt")
|
||||
keyPath = filepath.Join(dir, "fixture.key")
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
||||
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
|
||||
t.Fatalf("write certificate: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
// repointCertificateFiles rewrites every certificateFile/keyFile reference to
|
||||
// the generated pair, so a fixture is judged on its shape rather than on paths
|
||||
// that only exist on a deployed server.
|
||||
func repointCertificateFiles(node any, certPath, keyPath string) {
|
||||
switch value := node.(type) {
|
||||
case map[string]any:
|
||||
for key, child := range value {
|
||||
switch key {
|
||||
case "certificateFile":
|
||||
if _, ok := child.(string); ok {
|
||||
value[key] = certPath
|
||||
continue
|
||||
}
|
||||
case "keyFile":
|
||||
if _, ok := child.(string); ok {
|
||||
value[key] = keyPath
|
||||
continue
|
||||
}
|
||||
}
|
||||
repointCertificateFiles(child, certPath, keyPath)
|
||||
}
|
||||
case []any:
|
||||
for _, child := range value {
|
||||
repointCertificateFiles(child, certPath, keyPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assertXrayAccepts fails unless xray-core built the fixture. Fixtures naming
|
||||
// geoip:/geosite: need the dat files the panel ships next to the xray binary;
|
||||
// where they are absent the loader fails on the missing file rather than on the
|
||||
// fixture, so those are skipped instead of reported as broken.
|
||||
func assertXrayAccepts(t *testing.T, subject string, err error) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if isMissingGeoAssetErr(err) {
|
||||
t.Skipf("geo data files not available, cannot judge %s: %v", subject, err)
|
||||
}
|
||||
t.Fatalf("xray-core refuses %s: %v", subject, err)
|
||||
}
|
||||
|
||||
func buildGoldenInbound(t *testing.T, inbound map[string]any) error {
|
||||
t.Helper()
|
||||
certPath, keyPath := writeTestCertificate(t)
|
||||
repointCertificateFiles(inbound, certPath, keyPath)
|
||||
|
||||
raw, err := json.Marshal(inbound)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal inbound: %v", err)
|
||||
}
|
||||
detour := new(conf.InboundDetourConfig)
|
||||
if err := json.Unmarshal(raw, detour); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = detour.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
// TestGoldenInboundFixturesBuildInXray wraps each protocol fixture in a minimal
|
||||
// inbound and builds it.
|
||||
func TestGoldenInboundFixturesBuildInXray(t *testing.T) {
|
||||
for name, fixture := range goldenFixtures(t, "inbound") {
|
||||
if protocol, _ := fixture["protocol"].(string); protocol == string(model.MTProto) {
|
||||
continue
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
inbound := map[string]any{
|
||||
"tag": "golden-in",
|
||||
"listen": "127.0.0.1",
|
||||
"port": 8443,
|
||||
"protocol": fixture["protocol"],
|
||||
"settings": fixture["settings"],
|
||||
}
|
||||
assertXrayAccepts(t, "this fixture", buildGoldenInbound(t, inbound))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoldenInboundFullFixturesBuildInXray runs the complete panel Inbound
|
||||
// model through GenXrayInboundConfig, the conversion both the full-config and
|
||||
// the live AddInbound paths use, and builds the result.
|
||||
func TestGoldenInboundFullFixturesBuildInXray(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)
|
||||
}
|
||||
assertXrayAccepts(t, "the generated inbound", buildGoldenInbound(t, generated))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoldenStreamFixturesBuildInXray attaches the transport fragments — whole
|
||||
// stream sections, the security block, sockopt and finalmask — to an inbound.
|
||||
func TestGoldenStreamFixturesBuildInXray(t *testing.T) {
|
||||
for _, category := range []string{"stream", "security", "sockopt", "finalmask"} {
|
||||
for name, fixture := range goldenFixtures(t, category) {
|
||||
t.Run(category+"/"+name, func(t *testing.T) {
|
||||
stream := map[string]any{"network": "tcp"}
|
||||
switch category {
|
||||
case "stream":
|
||||
stream = fixture
|
||||
case "security":
|
||||
maps.Copy(stream, fixture)
|
||||
case "sockopt":
|
||||
stream["sockopt"] = fixture
|
||||
case "finalmask":
|
||||
stream["finalmask"] = fixture
|
||||
}
|
||||
inbound := map[string]any{
|
||||
"tag": "golden-in", "listen": "127.0.0.1", "port": 8443, "protocol": "vless",
|
||||
"settings": map[string]any{
|
||||
"clients": []any{map[string]any{"id": "b831381d-6324-4d53-ad4f-8cda48b30811", "email": "golden"}},
|
||||
"decryption": "none",
|
||||
},
|
||||
"streamSettings": stream,
|
||||
}
|
||||
assertXrayAccepts(t, "this fixture", buildGoldenInbound(t, inbound))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildGoldenRouting(routing map[string]any) error {
|
||||
raw, err := json.Marshal(routing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
router := new(conf.RouterConfig)
|
||||
if err := json.Unmarshal(raw, router); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = router.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
// TestGoldenRoutingFixturesBuildInXray builds the rule and balancer fixtures
|
||||
// through the router config ApplyRoutingConfig hands to the running core.
|
||||
func TestGoldenRoutingFixturesBuildInXray(t *testing.T) {
|
||||
for name, rule := range goldenFixtures(t, "rule") {
|
||||
t.Run("rule/"+name, func(t *testing.T) {
|
||||
assertXrayAccepts(t, "this rule", buildGoldenRouting(map[string]any{
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []any{rule},
|
||||
"balancers": []any{map[string]any{"tag": "balancer-load", "selector": []any{"proxy-"}}},
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
for name, balancer := range goldenFixtures(t, "balancer") {
|
||||
t.Run("balancer/"+name, func(t *testing.T) {
|
||||
assertXrayAccepts(t, "this balancer", buildGoldenRouting(map[string]any{
|
||||
"domainStrategy": "AsIs",
|
||||
"balancers": []any{balancer},
|
||||
"rules": []any{map[string]any{
|
||||
"type": "field", "port": "443", "balancerTag": balancer["tag"],
|
||||
}},
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoldenDNSFixturesBuildInXray builds the dns section, and each dns-server
|
||||
// fixture inside one.
|
||||
func TestGoldenDNSFixturesBuildInXray(t *testing.T) {
|
||||
build := func(dns map[string]any) error {
|
||||
raw, err := json.Marshal(dns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dnsConf := new(conf.DNSConfig)
|
||||
if err := json.Unmarshal(raw, dnsConf); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dnsConf.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
for name, fixture := range goldenFixtures(t, "dns") {
|
||||
t.Run("dns/"+name, func(t *testing.T) {
|
||||
assertXrayAccepts(t, "this dns section", build(fixture))
|
||||
})
|
||||
}
|
||||
for name, server := range goldenFixtures(t, "dns-server") {
|
||||
t.Run("dns-server/"+name, func(t *testing.T) {
|
||||
assertXrayAccepts(t, "this dns server", build(map[string]any{"servers": []any{server}}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isMissingGeoAssetErr(err error) bool {
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "geoip.dat") || strings.Contains(msg, "geosite.dat")
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
)
|
||||
|
||||
const completeXmcProfile = `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`
|
||||
@@ -77,62 +75,54 @@ func TestValidateFinalMaskXmcProfiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestXmcMaskProfilesCompleteMatchesCoreValidation pins the panel's predicate
|
||||
// to xray-core's own loader instead of a restatement of it: every profile the
|
||||
// panel accepts must build, and every one it rejects must fail to build. A
|
||||
// future core release that tightens or relaxes the rules fails here rather
|
||||
// than silently producing configs the core refuses to start on.
|
||||
func TestXmcMaskProfilesCompleteMatchesCoreValidation(t *testing.T) {
|
||||
// TestXmcMaskProfilesCompleteExpectations pins the panel predicate's own
|
||||
// contract. Until the lite peel it was cross-checked against xray-core's
|
||||
// loader (every accepted profile had to build in the core and vice versa);
|
||||
// with the library gone the predicate stands on its own rules — username
|
||||
// ^[A-Za-z0-9_]{3,16}$, a parseable uuid, non-empty textures value and
|
||||
// signature, at least one profile — and this table records them. A core
|
||||
// release that tightens or relaxes its side is no longer detected here;
|
||||
// the strip/validate pair errs on the panel's documented rules instead.
|
||||
func TestXmcMaskProfilesCompleteExpectations(t *testing.T) {
|
||||
profiles := []struct {
|
||||
name string
|
||||
raw string
|
||||
want bool
|
||||
}{
|
||||
{name: "complete", raw: completeXmcProfile},
|
||||
{name: "undashed uuid", raw: `{"username":"Notch","uuid":"069a79f444e94726a5befca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
|
||||
{name: "username at the 16 char limit", raw: `{"username":"Abcdefghijklmnop","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
|
||||
{name: "username over the limit", raw: `{"username":"Abcdefghijklmnopq","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
|
||||
{name: "username with a hyphen", raw: `{"username":"No-tch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
|
||||
{name: "empty uuid", raw: `{"username":"Notch","uuid":"","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
|
||||
{name: "missing textures value", raw: `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"","texturesSignature":"c2ln"}`},
|
||||
{name: "complete", raw: completeXmcProfile, want: true},
|
||||
{name: "undashed uuid", raw: `{"username":"Notch","uuid":"069a79f444e94726a5befca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: true},
|
||||
{name: "username at the 16 char limit", raw: `{"username":"Abcdefghijklmnop","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: true},
|
||||
{name: "username over the limit", raw: `{"username":"Abcdefghijklmnopq","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: false},
|
||||
{name: "username with a hyphen", raw: `{"username":"No-tch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: false},
|
||||
{name: "empty uuid", raw: `{"username":"Notch","uuid":"","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`, want: false},
|
||||
{name: "missing textures value", raw: `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"","texturesSignature":"c2ln"}`, want: false},
|
||||
{name: "no profiles at all", raw: ``, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range profiles {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var coreProfile conf.XMCProfile
|
||||
if err := json.Unmarshal([]byte(tt.raw), &coreProfile); err != nil {
|
||||
t.Fatalf("unmarshal into conf.XMCProfile: %v", err)
|
||||
}
|
||||
_, coreErr := coreProfile.Build()
|
||||
|
||||
mask := map[string]any{"type": "xmc"}
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(`{"profiles":[`+tt.raw+`]}`), &settings); err != nil {
|
||||
raw := tt.raw
|
||||
if raw == "" {
|
||||
raw = `{}` // a mask whose settings carry no profiles key at all
|
||||
if err := json.Unmarshal([]byte(raw), &settings); err != nil {
|
||||
t.Fatalf("unmarshal settings: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal([]byte(`{"profiles":[`+raw+`]}`), &settings); err != nil {
|
||||
t.Fatalf("unmarshal settings: %v", err)
|
||||
}
|
||||
}
|
||||
mask["settings"] = settings
|
||||
|
||||
panelAccepts := xmcMaskProfilesComplete(mask)
|
||||
coreAccepts := coreErr == nil
|
||||
if panelAccepts != coreAccepts {
|
||||
t.Errorf("xmcMaskProfilesComplete = %v, but conf.XMCProfile.Build() accepts = %v (err %v)", panelAccepts, coreAccepts, coreErr)
|
||||
if got := xmcMaskProfilesComplete(mask); got != tt.want {
|
||||
t.Errorf("xmcMaskProfilesComplete = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestXmcEmptyProfilesRejectedByCore covers the rule that lives on XMC rather
|
||||
// than XMCProfile: v26.7.28 dropped the "default to Dream" fallback, so a mask
|
||||
// with no profiles at all is now a build failure.
|
||||
func TestXmcEmptyProfilesRejectedByCore(t *testing.T) {
|
||||
var core conf.XMC
|
||||
if err := json.Unmarshal([]byte(`{"hostname":"mc.example.com","password":"pw","profiles":[]}`), &core); err != nil {
|
||||
t.Fatalf("unmarshal into conf.XMC: %v", err)
|
||||
}
|
||||
if _, err := core.Build(); err == nil {
|
||||
t.Fatal("conf.XMC.Build() accepted an empty profiles list; the panel's strip/validate pair is no longer needed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripIncompleteXmcMasks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user