mirror of
https://github.com/go-gost/x.git
synced 2026-10-20 08:18:33 +00:00
IsReference no longer uses reflect.DeepEqual; ResolveConfig trims the name before registry lookup; cloneConfig deep-copies all nested fields so hop parsing never mutates shared globals. Also documents the intentional fail-fast vs warn-and-skip error semantics.
676 lines
18 KiB
Go
676 lines
18 KiB
Go
package node
|
|
|
|
import (
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/x/config"
|
|
"github.com/go-gost/x/config/parsing"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
"github.com/go-gost/x/registry"
|
|
|
|
// Register connector and dialer implementations needed for node parsing.
|
|
_ "github.com/go-gost/x/connector/http"
|
|
_ "github.com/go-gost/x/dialer/tcp"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
|
|
m.Run()
|
|
}
|
|
|
|
func testLogger() logger.Logger {
|
|
return xlogger.NewLogger(xlogger.OutputOption(io.Discard))
|
|
}
|
|
|
|
func TestParseNode_Nil(t *testing.T) {
|
|
n, err := ParseNode("test-hop", nil, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n != nil {
|
|
t.Fatal("expected nil for nil config")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_Defaults(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "node1",
|
|
Addr: "example.com:8080",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DefaultsEmptyName(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "",
|
|
Addr: "example.com:8080",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DefaultsEmptyAddr(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "no-addr",
|
|
Addr: "",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_ExplicitConnector(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "explicit-conn",
|
|
Addr: "example.com:8080",
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "http",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_UnregisteredConnector(t *testing.T) {
|
|
_, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "bad-conn",
|
|
Addr: "example.com:8080",
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "nonexistent-connector-type",
|
|
},
|
|
}, testLogger())
|
|
if err == nil {
|
|
t.Fatal("expected error for unregistered connector type")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_UnregisteredDialer(t *testing.T) {
|
|
_, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "bad-dial",
|
|
Addr: "example.com:8080",
|
|
Dialer: &config.DialerConfig{
|
|
Type: "nonexistent-dialer-type",
|
|
},
|
|
}, testLogger())
|
|
if err == nil {
|
|
t.Fatal("expected error for unregistered dialer type")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_ExplicitDialer(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "explicit-dial",
|
|
Addr: "example.com:8080",
|
|
Dialer: &config.DialerConfig{
|
|
Type: "tcp",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithBypass(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "bypass-node",
|
|
Addr: "example.com:8080",
|
|
Bypass: "bp1",
|
|
Bypasses: []string{"bp2", "bp3"},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithResolver(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "resolver-node",
|
|
Addr: "example.com:8080",
|
|
Resolver: "my-resolver",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHosts(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "hosts-node",
|
|
Addr: "example.com:8080",
|
|
Hosts: "my-hosts",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithNetwork(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "network-node",
|
|
Addr: "example.com:8080",
|
|
Network: "tcp",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMetadata(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "meta-node",
|
|
Addr: "example.com:8080",
|
|
Metadata: map[string]any{
|
|
parsing.MDKeySoMark: 1234,
|
|
parsing.MDKeyInterface: "eth0",
|
|
parsing.MDKeyNetns: "myns",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithSockOpts(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "sockopts-node",
|
|
Addr: "example.com:8080",
|
|
SockOpts: &config.SockOptsConfig{
|
|
Mark: 9999,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithFilter(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "filter-node",
|
|
Addr: "example.com:8080",
|
|
Filter: &config.NodeFilterConfig{
|
|
Protocol: "http",
|
|
Host: "*.example.com",
|
|
Path: "/api",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithFilterStarBare(t *testing.T) {
|
|
// "*example.com" should become ".example.com"
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "star-bare-node",
|
|
Addr: "example.com:8080",
|
|
Filter: &config.NodeFilterConfig{
|
|
Host: "*example.com",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMatcher(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "matcher-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: "dstport==80",
|
|
Priority: 10,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMatcherAutoPriority(t *testing.T) {
|
|
// Priority auto-set to len(rule) when zero
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "auto-prio-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: "dstport==443",
|
|
Priority: 0,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMatcherBodySize(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "body-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
// Escaped predicate form; in YAML users write the backtick
|
|
// equivalent: BodyRegexp(`"model":"gpt-4"`).
|
|
Rule: `BodyRegexp("\"model\":\"gpt-4\"")`,
|
|
BodySize: 65536,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
if got := n.Options().MatcherBodySize; got != 65536 {
|
|
t.Errorf("MatcherBodySize = %d, want 65536", got)
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMatcherBodySize_Clamped(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "body-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: `BodyRegexp("foo")`,
|
|
BodySize: 10 * 1024 * 1024, // 10MB, above the 1MB cap
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
if got := n.Options().MatcherBodySize; got != MaxMatcherBodySize {
|
|
t.Errorf("MatcherBodySize = %d, want %d (clamped)", got, MaxMatcherBodySize)
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHTTPNode(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "http-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Host: "custom.example.com",
|
|
RequestHeader: map[string]string{"X-Custom": "value"},
|
|
ResponseHeader: map[string]string{"X-Response": "value"},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHTTPAuth(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "http-auth-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Auth: &config.AuthConfig{
|
|
Username: "user",
|
|
Password: "pass",
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHTTPDeprecatedHeader(t *testing.T) {
|
|
// Deprecated Header field should populate RequestHeader
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "http-deprecated-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Header: map[string]string{"X-Old": "value"},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHeaderRewrite(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "hdr-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
RewriteRequestHeader: []config.HTTPHeaderRewriteConfig{
|
|
{Name: "(?i)^referer$", Match: ".*", Replacement: ""},
|
|
},
|
|
RewriteResponseHeader: []config.HTTPHeaderRewriteConfig{
|
|
{Name: "(?i)^location$", Match: "https://github.com", Replacement: "https://127.0.0.1:8080"},
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
settings := n.Options().HTTP
|
|
if settings == nil {
|
|
t.Fatal("expected HTTP settings")
|
|
}
|
|
if len(settings.RewriteRequestHeader) != 1 || len(settings.RewriteResponseHeader) != 1 {
|
|
t.Fatalf("rewrite header counts = %d/%d, want 1/1", len(settings.RewriteRequestHeader), len(settings.RewriteResponseHeader))
|
|
}
|
|
}
|
|
|
|
func TestParseHeaderRewrites_EmptyNameNoRewriter(t *testing.T) {
|
|
// Empty Name with no Rewriter must not be appended (silent no-op guard).
|
|
out := parseHeaderRewrites([]config.HTTPHeaderRewriteConfig{
|
|
{Name: "", Match: ".*", Replacement: ""},
|
|
}, testLogger())
|
|
if len(out) != 0 {
|
|
t.Fatalf("out = %d rules, want 0", len(out))
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithTLSNode(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "tls-node",
|
|
Addr: "example.com:8080",
|
|
TLS: &config.TLSNodeConfig{
|
|
ServerName: "secure.example.com",
|
|
Secure: true,
|
|
Options: &config.TLSOptions{
|
|
MinVersion: "1.2",
|
|
MaxVersion: "1.3",
|
|
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
|
|
ALPN: []string{"h2", "http/1.1"},
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_TLSServerNameFromAddr(t *testing.T) {
|
|
// When connector/dialer TLS ServerName is empty, it defaults from addr hostname.
|
|
// The server name is set on the connector's TLS config (internal), so the best
|
|
// verification is that ParseNode does not error and returns a node.
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "tls-servername-node",
|
|
Addr: "tls.example.com:443",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_ConnectorAuth(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "conn-auth-node",
|
|
Addr: "example.com:8080",
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "http",
|
|
Auth: &config.AuthConfig{
|
|
Username: "connuser",
|
|
Password: "connpass",
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DialerAuth(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "dial-auth-node",
|
|
Addr: "example.com:8080",
|
|
Dialer: &config.DialerConfig{
|
|
Type: "tcp",
|
|
Auth: &config.AuthConfig{
|
|
Username: "dialuser",
|
|
Password: "dialpass",
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_InvalidMatcherRule(t *testing.T) {
|
|
// An invalid matcher rule sets priority to -1
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "invalid-matcher-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: "",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node even with empty matcher rule")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DeprecatedFilterFields(t *testing.T) {
|
|
// Deprecated filter fields: Protocol/Host/Path via Filter
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "deprecated-node",
|
|
Addr: "example.com:8080",
|
|
Filter: &config.NodeFilterConfig{
|
|
Protocol: "socks5",
|
|
Host: "*.example.com",
|
|
Path: "/proxy",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestFilterToMatcherRule(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
filter *config.NodeFilterConfig
|
|
want string
|
|
}{
|
|
{"nil", nil, ""},
|
|
{"all empty", &config.NodeFilterConfig{}, ""},
|
|
{"host only", &config.NodeFilterConfig{Host: "example.com"}, "Host(`example.com`)"},
|
|
{"dot host", &config.NodeFilterConfig{Host: ".example.com"},
|
|
"Host(`example.com`) || Host(`.example.com`)"},
|
|
{"star host", &config.NodeFilterConfig{Host: "*.example.com"},
|
|
"Host(`example.com`) || Host(`.example.com`)"},
|
|
{"protocol only", &config.NodeFilterConfig{Protocol: "ssh"}, "Proto(`ssh`)"},
|
|
{"path only", &config.NodeFilterConfig{Path: "/api"}, "PathPrefix(`/api`)"},
|
|
{"combined", &config.NodeFilterConfig{
|
|
Host: ".example.com",
|
|
Protocol: "http",
|
|
Path: "/api",
|
|
}, "(Host(`example.com`) || Host(`.example.com`)) && Proto(`http`) && PathPrefix(`/api`)"},
|
|
{"star combined", &config.NodeFilterConfig{
|
|
Host: "*.example.com",
|
|
Protocol: "http",
|
|
Path: "/api",
|
|
}, "(Host(`example.com`) || Host(`.example.com`)) && Proto(`http`) && PathPrefix(`/api`)"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := filterToMatcherRule(tt.filter); got != tt.want {
|
|
t.Errorf("filterToMatcherRule(%v) = %q, want %q", tt.filter, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsReference(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg *config.NodeConfig
|
|
want bool
|
|
}{
|
|
{"nil", nil, false},
|
|
{"empty name", &config.NodeConfig{}, false},
|
|
{"blank name", &config.NodeConfig{Name: " "}, false},
|
|
{"name only", &config.NodeConfig{Name: "shared"}, true},
|
|
{"name and addr", &config.NodeConfig{Name: "shared", Addr: "example.com:80"}, false},
|
|
{"name and network", &config.NodeConfig{Name: "shared", Network: "tcp"}, false},
|
|
{"name and empty-but-non-nil connector", &config.NodeConfig{Name: "shared", Connector: &config.ConnectorConfig{}}, false},
|
|
{"name and empty bypasses slice", &config.NodeConfig{Name: "shared", Bypasses: []string{}}, true},
|
|
{"name and matcher", &config.NodeConfig{Name: "shared", Matcher: &config.NodeMatcherConfig{Rule: "Host(`x`)"}}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := IsReference(tt.cfg); got != tt.want {
|
|
t.Errorf("IsReference(%+v) = %v, want %v", tt.cfg, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveConfig_TrimsName(t *testing.T) {
|
|
const name = "spaced-global-node"
|
|
registry.NodeRegistry().Register(name, &config.NodeConfig{Name: name, Addr: "example.com:8080"})
|
|
t.Cleanup(func() { registry.NodeRegistry().Unregister(name) })
|
|
|
|
got, err := ResolveConfig(&config.NodeConfig{Name: " " + name + " "})
|
|
if err != nil {
|
|
t.Fatalf("ResolveConfig: %v", err)
|
|
}
|
|
if got == nil || got.Addr != "example.com:8080" {
|
|
t.Fatalf("resolved = %+v, want addr %q", got, "example.com:8080")
|
|
}
|
|
}
|
|
|
|
func TestResolveConfig_DeepCopyIsolation(t *testing.T) {
|
|
const name = "rich-global-node"
|
|
global := &config.NodeConfig{
|
|
Name: name,
|
|
Addr: "example.com:8080",
|
|
Metadata: map[string]any{"k": "v"},
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "http",
|
|
Auth: &config.AuthConfig{Username: "u"},
|
|
Metadata: map[string]any{"ck": "cv"},
|
|
TLS: &config.TLSConfig{ServerName: "sni.example", Options: &config.TLSOptions{ALPN: []string{"h2"}}},
|
|
},
|
|
Dialer: &config.DialerConfig{
|
|
Type: "tcp",
|
|
Metadata: map[string]any{"dk": "dv"},
|
|
},
|
|
Matcher: &config.NodeMatcherConfig{Rule: "Host(`x`)"},
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Host: "h",
|
|
RequestHeader: map[string]string{"A": "b"},
|
|
Auth: &config.AuthConfig{Username: "hu"},
|
|
},
|
|
TLS: &config.TLSNodeConfig{ServerName: "tls.example", Options: &config.TLSOptions{MinVersion: "1.2"}},
|
|
Probe: &config.ProbeConfig{Type: "tcp", Addr: "example.com:80", HTTPHeaders: map[string]string{"H": "v"}},
|
|
}
|
|
registry.NodeRegistry().Register(name, global)
|
|
t.Cleanup(func() { registry.NodeRegistry().Unregister(name) })
|
|
|
|
got, err := ResolveConfig(&config.NodeConfig{Name: name})
|
|
if err != nil {
|
|
t.Fatalf("ResolveConfig: %v", err)
|
|
}
|
|
|
|
// Mutate every nested value on the copy; the global must not change.
|
|
got.Metadata["k"] = "mut"
|
|
got.Connector.Auth.Username = "mut"
|
|
got.Connector.Metadata["ck"] = "mut"
|
|
got.Connector.TLS.Options.ALPN[0] = "mut"
|
|
got.Dialer.Metadata["dk"] = "mut"
|
|
got.Matcher.Rule = "mut"
|
|
got.HTTP.RequestHeader["A"] = "mut"
|
|
got.HTTP.Auth.Username = "mut"
|
|
got.TLS.Options.MinVersion = "mut"
|
|
got.Probe.HTTPHeaders["H"] = "mut"
|
|
|
|
if global.Metadata["k"] != "v" ||
|
|
global.Connector.Auth.Username != "u" ||
|
|
global.Connector.Metadata["ck"] != "cv" ||
|
|
global.Connector.TLS.Options.ALPN[0] != "h2" ||
|
|
global.Dialer.Metadata["dk"] != "dv" ||
|
|
global.Matcher.Rule != "Host(`x`)" ||
|
|
global.HTTP.RequestHeader["A"] != "b" ||
|
|
global.HTTP.Auth.Username != "hu" ||
|
|
global.TLS.Options.MinVersion != "1.2" ||
|
|
global.Probe.HTTPHeaders["H"] != "v" {
|
|
t.Fatalf("global definition was mutated via the resolved copy: %+v", global)
|
|
}
|
|
}
|