From 2b1e4ca3a7b5b5c6750418909866841df81ede7b Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 24 May 2026 13:34:33 +0800 Subject: [PATCH] test(parsing): add 229 unit tests across 18 config/parsing packages Covering nil-input safety, plugin backends (HTTP/gRPC/default), loaders (File/HTTP/Redis), selector strategies, edge cases (empty/invalid inputs), and hop-to-node metadata inheritance. All tests passing. --- config/parsing/admission/parse_test.go | 144 ++++++++ config/parsing/auth/parse_test.go | 242 +++++++++++++ config/parsing/bypass/parse_test.go | 144 ++++++++ config/parsing/chain/parse_test.go | 121 +++++++ config/parsing/hop/parse_test.go | 429 +++++++++++++++++++++++ config/parsing/hosts/parse_test.go | 154 +++++++++ config/parsing/ingress/parse_test.go | 141 ++++++++ config/parsing/limiter/parse_test.go | 234 +++++++++++++ config/parsing/logger/parse_test.go | 142 ++++++++ config/parsing/node/parse_test.go | 461 +++++++++++++++++++++++++ config/parsing/observer/parse_test.go | 75 ++++ config/parsing/parser/parse_test.go | 227 ++++++++++++ config/parsing/recorder/parse_test.go | 199 +++++++++++ config/parsing/resolver/parse_test.go | 146 ++++++++ config/parsing/router/parse_test.go | 237 +++++++++++++ config/parsing/sd/parse_test.go | 75 ++++ config/parsing/selector/parse_test.go | 144 ++++++++ config/parsing/tls_test.go | 103 ++++++ 18 files changed, 3418 insertions(+) create mode 100644 config/parsing/admission/parse_test.go create mode 100644 config/parsing/auth/parse_test.go create mode 100644 config/parsing/bypass/parse_test.go create mode 100644 config/parsing/chain/parse_test.go create mode 100644 config/parsing/hop/parse_test.go create mode 100644 config/parsing/hosts/parse_test.go create mode 100644 config/parsing/ingress/parse_test.go create mode 100644 config/parsing/limiter/parse_test.go create mode 100644 config/parsing/logger/parse_test.go create mode 100644 config/parsing/node/parse_test.go create mode 100644 config/parsing/observer/parse_test.go create mode 100644 config/parsing/parser/parse_test.go create mode 100644 config/parsing/recorder/parse_test.go create mode 100644 config/parsing/resolver/parse_test.go create mode 100644 config/parsing/router/parse_test.go create mode 100644 config/parsing/sd/parse_test.go create mode 100644 config/parsing/selector/parse_test.go create mode 100644 config/parsing/tls_test.go diff --git a/config/parsing/admission/parse_test.go b/config/parsing/admission/parse_test.go new file mode 100644 index 00000000..7ac6d86a --- /dev/null +++ b/config/parsing/admission/parse_test.go @@ -0,0 +1,144 @@ +package admission + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseAdmission_Nil(t *testing.T) { + adm := ParseAdmission(nil) + if adm != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseAdmission_PluginHTTP(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "http-adm", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_PluginGRPC(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "grpc-adm", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "admission.local", + }, + }, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_PluginDefaultType(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "default-adm", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_WithMatchers(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "matcher-adm", + Matchers: []string{"192.168.0.0/16", "10.0.0.1"}, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_Whitelist(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "whitelist-adm", + Whitelist: true, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_Reverse(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "reverse-adm", + Reverse: true, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_FileLoader(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "file-adm", + File: &config.FileLoader{Path: "/tmp/admission.txt"}, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_HTTPLoader(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "http-adm", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/list"}, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestParseAdmission_RedisLoader(t *testing.T) { + adm := ParseAdmission(&config.AdmissionConfig{ + Name: "redis-adm", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "admission-set", + }, + }) + if adm == nil { + t.Fatal("expected non-nil admission") + } +} + +func TestList_ReturnsEntries(t *testing.T) { + // The registry returns a lazy wrapper for any non-empty name. + got := List("test-admission", "test-admission-2") + if len(got) != 2 { + t.Fatalf("expected 2 entries, got %d", len(got)) + } +} + +func TestList_EmptyNameReturnsNothing(t *testing.T) { + // Empty names are filtered by the registry Get. + got := List("") + if len(got) != 0 { + t.Fatal("expected empty list for empty name") + } +} diff --git a/config/parsing/auth/parse_test.go b/config/parsing/auth/parse_test.go new file mode 100644 index 00000000..53d985cf --- /dev/null +++ b/config/parsing/auth/parse_test.go @@ -0,0 +1,242 @@ +package auth + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestInfo_Nil(t *testing.T) { + if u := Info(nil); u != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestInfo_EmptyUsername(t *testing.T) { + u := Info(&config.AuthConfig{Username: ""}) + if u != nil { + t.Fatal("expected nil for empty username") + } +} + +func TestInfo_UsernameOnly(t *testing.T) { + u := Info(&config.AuthConfig{Username: "user"}) + if u == nil { + t.Fatal("expected non-nil userinfo") + } + if u.Username() != "user" { + t.Fatalf("username = %q, want %q", u.Username(), "user") + } + if _, hasPassword := u.Password(); hasPassword { + t.Fatal("expected no password") + } +} + +func TestInfo_UsernameAndPassword(t *testing.T) { + u := Info(&config.AuthConfig{Username: "user", Password: "pass"}) + if u == nil { + t.Fatal("expected non-nil userinfo") + } + if u.Username() != "user" { + t.Fatalf("username = %q, want %q", u.Username(), "user") + } + if p, ok := u.Password(); !ok || p != "pass" { + t.Fatalf("password = %q, want %q", p, "pass") + } +} + +func TestInfo_WithFile(t *testing.T) { + // File that doesn't exist - should fall back to username + u := Info(&config.AuthConfig{ + Username: "fallback", + Password: "pass", + File: "/nonexistent/auth_file", + }) + if u == nil { + t.Fatal("expected non-nil userinfo (fallback)") + } + if u.Username() != "fallback" { + t.Fatalf("username = %q, want %q", u.Username(), "fallback") + } +} + +func TestParseAutherFromAuth_Nil(t *testing.T) { + a := ParseAutherFromAuth(nil) + if a != nil { + t.Fatal("expected nil for nil auth config") + } +} + +func TestParseAutherFromAuth_EmptyUsername(t *testing.T) { + a := ParseAutherFromAuth(&config.AuthConfig{Username: ""}) + if a != nil { + t.Fatal("expected nil for empty username") + } +} + +func TestParseAutherFromAuth_Valid(t *testing.T) { + a := ParseAutherFromAuth(&config.AuthConfig{ + Username: "user", + Password: "pass", + }) + if a == nil { + t.Fatal("expected non-nil authenticator") + } +} + +func TestParseAuther_Nil(t *testing.T) { + a := ParseAuther(nil) + if a != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseAuther_WithUsers(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "test-auth", + Auths: []*config.AuthConfig{ + {Username: "u1", Password: "p1"}, + {Username: "u2", Password: "p2"}, + {Username: "", Password: "nope"}, // skipped + }, + }) + if a == nil { + t.Fatal("expected non-nil authenticator") + } +} + +func TestParseAuther_WithEmptyAuths(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "empty-auth", + Auths: []*config.AuthConfig{}, + }) + if a == nil { + t.Fatal("expected non-nil authenticator") + } +} + +func TestParseAuther_PluginHTTP(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "http-plugin", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if a == nil { + t.Fatal("expected non-nil plugin authenticator") + } +} + +func TestParseAuther_PluginGRPC(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "grpc-plugin", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "auth.local", + }, + }, + }) + if a == nil { + t.Fatal("expected non-nil plugin authenticator") + } +} + +func TestParseAuther_PluginDefaultType(t *testing.T) { + // Default plugin type should be treated as gRPC + a := ParseAuther(&config.AutherConfig{ + Name: "default-plugin", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if a == nil { + t.Fatal("expected non-nil plugin authenticator") + } +} + +func TestInfo_ParseFile(t *testing.T) { + // parseInfo with empty reader should return nil + u := Info(&config.AuthConfig{ + File: "/nonexistent/file", + }) + if u != nil { + t.Fatal("expected nil for nonexistent file (should not crash)") + } +} + +func TestList_ReturnsEntries(t *testing.T) { + // The registry returns a lazy wrapper for any non-empty name. + got := List("test-auther", "test-auther-2") + if len(got) != 2 { + t.Fatalf("expected 2 entries, got %d", len(got)) + } +} + +func TestList_EmptyNameReturnsNothing(t *testing.T) { + // Empty names are filtered by the registry Get. + got := List("") + if len(got) != 0 { + t.Fatal("expected empty list for empty name") + } +} + +func TestParseAuther_HTTPLoader(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "http-loader-auth", + HTTP: &config.HTTPLoader{ + URL: "http://localhost:8080/auth", + }, + }) + if a == nil { + t.Fatal("expected non-nil authenticator") + } +} + +func TestParseAuther_FileLoader(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "file-loader-auth", + File: &config.FileLoader{ + Path: "/tmp/auth.txt", + }, + }) + if a == nil { + t.Fatal("expected non-nil authenticator") + } +} + +func TestParseAuther_RedisLoader(t *testing.T) { + a := ParseAuther(&config.AutherConfig{ + Name: "redis-loader-auth", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + }, + }) + if a == nil { + t.Fatal("expected non-nil authenticator") + } +} + +func TestInfo_Integration(t *testing.T) { + // Verify Info returns *url.Userinfo whose String() matches the standard library. + u := Info(&config.AuthConfig{Username: "alice", Password: "secret"}) + if u == nil { + t.Fatal("expected non-nil") + } + want := "alice:secret" + if u.String() != want { + t.Fatalf("String() = %q, want %q", u.String(), want) + } +} diff --git a/config/parsing/bypass/parse_test.go b/config/parsing/bypass/parse_test.go new file mode 100644 index 00000000..c2e101f3 --- /dev/null +++ b/config/parsing/bypass/parse_test.go @@ -0,0 +1,144 @@ +package bypass + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseBypass_Nil(t *testing.T) { + bp := ParseBypass(nil) + if bp != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseBypass_PluginHTTP(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "http-bp", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_PluginGRPC(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "grpc-bp", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "bypass.local", + }, + }, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_PluginDefaultType(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "default-bp", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_WithMatchers(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "matcher-bp", + Matchers: []string{"127.0.0.1", "10.0.0.0/8", "*.example.com"}, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_Whitelist(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "whitelist-bp", + Whitelist: true, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_Reverse(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "reverse-bp", + Reverse: true, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_FileLoader(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "file-bp", + File: &config.FileLoader{Path: "/tmp/bypass.txt"}, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_HTTPLoader(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "http-bp", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/list"}, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestParseBypass_RedisLoader(t *testing.T) { + bp := ParseBypass(&config.BypassConfig{ + Name: "redis-bp", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "bypass-set", + }, + }) + if bp == nil { + t.Fatal("expected non-nil bypass") + } +} + +func TestList_ReturnsEntries(t *testing.T) { + // The registry returns a lazy wrapper for any non-empty name. + got := List("test-bypass", "test-bypass-2") + if len(got) != 2 { + t.Fatalf("expected 2 entries, got %d", len(got)) + } +} + +func TestList_EmptyNameReturnsNothing(t *testing.T) { + // Empty names are filtered by the registry Get. + got := List("") + if len(got) != 0 { + t.Fatal("expected empty list for empty name") + } +} diff --git a/config/parsing/chain/parse_test.go b/config/parsing/chain/parse_test.go new file mode 100644 index 00000000..f02305b6 --- /dev/null +++ b/config/parsing/chain/parse_test.go @@ -0,0 +1,121 @@ +package chain + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +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 TestParseChain_Nil(t *testing.T) { + c, err := ParseChain(nil, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseChain_EmptyHops(t *testing.T) { + c, err := ParseChain(&config.ChainConfig{ + Name: "empty-hops", + Hops: []*config.HopConfig{}, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil { + t.Fatal("expected non-nil chain") + } +} + +func TestParseChain_NilHops(t *testing.T) { + c, err := ParseChain(&config.ChainConfig{ + Name: "nil-hops", + Hops: nil, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil { + t.Fatal("expected non-nil chain") + } +} + +func TestParseChain_NilHopEntry(t *testing.T) { + c, err := ParseChain(&config.ChainConfig{ + Name: "nil-entry", + Hops: []*config.HopConfig{nil}, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil { + t.Fatal("expected non-nil chain when hop entry is nil") + } +} + +func TestParseChain_WithMetadata(t *testing.T) { + c, err := ParseChain(&config.ChainConfig{ + Name: "meta-chain", + Metadata: map[string]any{ + "key": "value", + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil { + t.Fatal("expected non-nil chain") + } +} + +func TestParseChain_NamedHop(t *testing.T) { + // Named hop (Nodes==nil, Plugin==nil) uses registry lookup + c, err := ParseChain(&config.ChainConfig{ + Name: "named-hop", + Hops: []*config.HopConfig{ + {Name: "nonexistent-hop"}, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil { + t.Fatal("expected non-nil chain even with unregistered hop") + } +} + +func TestParseChain_InlineHopWithPlugin(t *testing.T) { + // Inline hop (Nodes!=nil or Plugin!=nil) calls hop_parser.ParseHop + c, err := ParseChain(&config.ChainConfig{ + Name: "inline-hop", + Hops: []*config.HopConfig{ + { + Name: "plugin-hop", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c == nil { + t.Fatal("expected non-nil chain") + } +} diff --git a/config/parsing/hop/parse_test.go b/config/parsing/hop/parse_test.go new file mode 100644 index 00000000..0eaffbdf --- /dev/null +++ b/config/parsing/hop/parse_test.go @@ -0,0 +1,429 @@ +package hop + +import ( + "io" + "testing" + + "github.com/go-gost/core/hop" + + "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" + mdutil "github.com/go-gost/x/metadata/util" + + // 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 TestParseHop_Nil(t *testing.T) { + h, err := ParseHop(nil, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseHop_PluginHTTP(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "http-hop", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_PluginGRPC(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "grpc-hop", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "hop.local", + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_PluginDefaultType(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "default-hop", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_PluginHTTPWithTLS(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "http-tls-hop", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9003", + TLS: &config.TLSConfig{ + Secure: false, + ServerName: "hop.example.com", + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_PluginGRPCWithTLSInsecure(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "grpc-insecure-hop", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9004", + TLS: &config.TLSConfig{ + Secure: false, // InsecureSkipVerify = true + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_NoNodesNoPlugin(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "empty-hop", + Nodes: nil, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop even with no nodes") + } +} + +func TestParseHop_WithBypass(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "bypass-hop", + Bypass: "bypass1", + Bypasses: []string{"bypass2", "bypass3"}, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_WithSelector(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "selector-hop", + Selector: &config.SelectorConfig{ + Strategy: "round", + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_WithNodeNilEntries(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "nil-nodes-hop", + Nodes: []*config.NodeConfig{ + nil, + nil, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop when nodes contain nil entries") + } +} + +func TestParseHop_SockOptsMark(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "sockopts-hop", + SockOpts: &config.SockOptsConfig{ + Mark: 1234, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_MetadataSoMark(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "md-somark-hop", + Metadata: map[string]any{ + parsing.MDKeySoMark: 5678, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_MetadataInterface(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "md-iface-hop", + Metadata: map[string]any{ + parsing.MDKeyInterface: "eth0", + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_MetadataProxyProtocol(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "md-ppv-hop", + Metadata: map[string]any{ + parsing.MDKeyProxyProtocol: 2, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_MetadataNetns(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "md-netns-hop", + Metadata: map[string]any{ + parsing.MDKeyNetns: "mynetns", + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_InterfaceDeprecated(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "iface-deprecated-hop", + Interface: "eth1", + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_FileLoader(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "file-hop", + File: &config.FileLoader{Path: "/tmp/hop.txt"}, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_HTTPLoader(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "http-hop", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/hop"}, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_RedisLoader(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "redis-hop", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "hop-key", + Username: "user", + Password: "pass", + DB: 1, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } +} + +func TestParseHop_NodeInheritsInterface(t *testing.T) { + // When hop-level interface is set and node-level is not, node inherits. + h, err := ParseHop(&config.HopConfig{ + Name: "inherit-iface-hop", + Interface: "eth0", + Nodes: []*config.NodeConfig{ + { + Name: "node1", + Addr: "example.com:8080", + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } + nodes := h.(hop.NodeList).Nodes() + if len(nodes) != 1 { + t.Fatalf("expected 1 node, got %d", len(nodes)) + } + if got := mdutil.GetString(nodes[0].Metadata(), parsing.MDKeyInterface); got != "eth0" { + t.Fatalf("inherited interface = %q, want %q", got, "eth0") + } +} + +func TestParseHop_NodeInheritsSoMark(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "inherit-somark-hop", + SockOpts: &config.SockOptsConfig{ + Mark: 9999, + }, + Nodes: []*config.NodeConfig{ + { + Name: "node1", + Addr: "example.com:8080", + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } + nodes := h.(hop.NodeList).Nodes() + if len(nodes) != 1 { + t.Fatalf("expected 1 node, got %d", len(nodes)) + } + if got := mdutil.GetInt(nodes[0].Metadata(), parsing.MDKeySoMark); got != 9999 { + t.Fatalf("inherited so_mark = %d, want %d", got, 9999) + } +} + +func TestParseHop_NodeInheritsNetns(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "inherit-netns-hop", + Metadata: map[string]any{ + parsing.MDKeyNetns: "mynetns", + }, + Nodes: []*config.NodeConfig{ + { + Name: "node1", + Addr: "example.com:8080", + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } + nodes := h.(hop.NodeList).Nodes() + if len(nodes) != 1 { + t.Fatalf("expected 1 node, got %d", len(nodes)) + } + if got := mdutil.GetString(nodes[0].Metadata(), parsing.MDKeyNetns); got != "mynetns" { + t.Fatalf("inherited netns = %q, want %q", got, "mynetns") + } +} + +func TestParseHop_NodeResolverHostsInheritance(t *testing.T) { + h, err := ParseHop(&config.HopConfig{ + Name: "inherit-resolver-hop", + Resolver: "my-resolver", + Hosts: "my-hosts", + Nodes: []*config.NodeConfig{ + { + Name: "node1", + Addr: "example.com:8080", + }, + }, + }, testLogger()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h == nil { + t.Fatal("expected non-nil hop") + } + // Verify the node was created (no panic from nil resolver/hosts). + // The resolver/hosts names are inherited, but registry lookup returns + // nil since these names are not registered in this test. + nodes := h.(hop.NodeList).Nodes() + if len(nodes) != 1 { + t.Fatalf("expected 1 node, got %d", len(nodes)) + } +} diff --git a/config/parsing/hosts/parse_test.go b/config/parsing/hosts/parse_test.go new file mode 100644 index 00000000..db258662 --- /dev/null +++ b/config/parsing/hosts/parse_test.go @@ -0,0 +1,154 @@ +package hosts + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseHostMapper_Nil(t *testing.T) { + hm := ParseHostMapper(nil) + if hm != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseHostMapper_PluginHTTP(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "http-hosts", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_PluginGRPC(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "grpc-hosts", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "hosts.local", + }, + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_PluginDefaultType(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "default-hosts", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_WithMappings(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "mapping-hosts", + Mappings: []*config.HostMappingConfig{ + {IP: "127.0.0.1", Hostname: "localhost"}, + {IP: "::1", Hostname: "ipv6-localhost"}, + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_EmptyMapping(t *testing.T) { + // Mappings with empty IP or Hostname should be skipped + hm := ParseHostMapper(&config.HostsConfig{ + Name: "partial-hosts", + Mappings: []*config.HostMappingConfig{ + {IP: "", Hostname: "nope"}, // skipped + {IP: "127.0.0.1", Hostname: ""}, // skipped + {IP: "127.0.0.1", Hostname: "valid"}, // kept + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper even with partial mappings") + } +} + +func TestParseHostMapper_InvalidIP(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "invalid-ip", + Mappings: []*config.HostMappingConfig{ + {IP: "not-an-ip", Hostname: "example.com"}, // skipped + {IP: "127.0.0.1", Hostname: "valid"}, // kept + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper even with invalid IP") + } +} + +func TestParseHostMapper_FileLoader(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "file-hosts", + File: &config.FileLoader{Path: "/etc/hosts"}, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_HTTPLoader(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "http-hosts", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/hosts"}, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_RedisLoader(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "redis-hosts", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "hosts-key", + Type: "set", + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} + +func TestParseHostMapper_RedisListLoader(t *testing.T) { + hm := ParseHostMapper(&config.HostsConfig{ + Name: "redis-list-hosts", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "hosts-list", + Type: "list", + }, + }) + if hm == nil { + t.Fatal("expected non-nil host mapper") + } +} diff --git a/config/parsing/ingress/parse_test.go b/config/parsing/ingress/parse_test.go new file mode 100644 index 00000000..d845890e --- /dev/null +++ b/config/parsing/ingress/parse_test.go @@ -0,0 +1,141 @@ +package ingress + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseIngress_Nil(t *testing.T) { + ing := ParseIngress(nil) + if ing != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseIngress_PluginHTTP(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "http-ing", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_PluginGRPC(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "grpc-ing", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "ingress.local", + }, + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_PluginDefaultType(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "default-ing", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_WithRules(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "rule-ing", + Rules: []*config.IngressRuleConfig{ + {Hostname: "example.com", Endpoint: "10.0.0.1:8080"}, + {Hostname: "test.com", Endpoint: "10.0.0.2:9090"}, + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_EmptyRules(t *testing.T) { + // Rules with empty hostname or endpoint should be skipped + ing := ParseIngress(&config.IngressConfig{ + Name: "partial-ing", + Rules: []*config.IngressRuleConfig{ + {Hostname: "", Endpoint: "nope:8080"}, // skipped + {Hostname: "nope.com", Endpoint: ""}, // skipped + {Hostname: "valid.com", Endpoint: "10.0.0.1:8080"}, // kept + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress even with partial rules") + } +} + +func TestParseIngress_FileLoader(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "file-ing", + File: &config.FileLoader{Path: "/tmp/ingress.txt"}, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_HTTPLoader(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "http-ing", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/ingress"}, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_RedisLoader(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "redis-ing", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "ingress-key", + Type: "hash", + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} + +func TestParseIngress_RedisSetLoader(t *testing.T) { + ing := ParseIngress(&config.IngressConfig{ + Name: "redis-set-ing", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "ingress-set", + Type: "set", + }, + }) + if ing == nil { + t.Fatal("expected non-nil ingress") + } +} diff --git a/config/parsing/limiter/parse_test.go b/config/parsing/limiter/parse_test.go new file mode 100644 index 00000000..5ac91911 --- /dev/null +++ b/config/parsing/limiter/parse_test.go @@ -0,0 +1,234 @@ +package limiter + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseTrafficLimiter_Nil(t *testing.T) { + lim := ParseTrafficLimiter(nil) + if lim != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseTrafficLimiter_WithLimits(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "traffic-limiter", + Limits: []string{"100KB", "200KB"}, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_PluginHTTP(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "http-lim", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_PluginGRPC(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "grpc-lim", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "limiter.local", + }, + }, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_PluginDefaultType(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "default-lim", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_FileLoader(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "file-lim", + File: &config.FileLoader{Path: "/tmp/limiter.txt"}, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_HTTPLoader(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "http-lim", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/limits"}, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_RedisLoader(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "redis-lim", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "limiter-key", + }, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +func TestParseTrafficLimiter_RedisListLoader(t *testing.T) { + lim := ParseTrafficLimiter(&config.LimiterConfig{ + Name: "redis-list-lim", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "limiter-list", + Type: "list", + }, + }) + if lim == nil { + t.Fatal("expected non-nil traffic limiter") + } +} + +// --- ConnLimiter --- + +func TestParseConnLimiter_Nil(t *testing.T) { + lim := ParseConnLimiter(nil) + if lim != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseConnLimiter_WithLimits(t *testing.T) { + lim := ParseConnLimiter(&config.LimiterConfig{ + Name: "conn-limiter", + Limits: []string{"10", "50"}, + }) + if lim == nil { + t.Fatal("expected non-nil conn limiter") + } +} + +func TestParseConnLimiter_RedisLoader(t *testing.T) { + lim := ParseConnLimiter(&config.LimiterConfig{ + Name: "redis-conn-lim", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "conn-key", + }, + }) + if lim == nil { + t.Fatal("expected non-nil conn limiter") + } +} + +func TestParseConnLimiter_RedisListLoader(t *testing.T) { + lim := ParseConnLimiter(&config.LimiterConfig{ + Name: "redis-list-conn-lim", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "conn-list", + Type: "list", + }, + }) + if lim == nil { + t.Fatal("expected non-nil conn limiter") + } +} + +func TestParseConnLimiter_FileLoader(t *testing.T) { + lim := ParseConnLimiter(&config.LimiterConfig{ + Name: "file-conn-lim", + File: &config.FileLoader{Path: "/tmp/conn_limiter.txt"}, + }) + if lim == nil { + t.Fatal("expected non-nil conn limiter") + } +} + +// --- RateLimiter --- + +func TestParseRateLimiter_Nil(t *testing.T) { + lim := ParseRateLimiter(nil) + if lim != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseRateLimiter_WithLimits(t *testing.T) { + lim := ParseRateLimiter(&config.LimiterConfig{ + Name: "rate-limiter", + Limits: []string{"100", "200"}, + }) + if lim == nil { + t.Fatal("expected non-nil rate limiter") + } +} + +func TestParseRateLimiter_RedisLoader(t *testing.T) { + lim := ParseRateLimiter(&config.LimiterConfig{ + Name: "redis-rate-lim", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "rate-key", + }, + }) + if lim == nil { + t.Fatal("expected non-nil rate limiter") + } +} + +func TestParseRateLimiter_RedisListLoader(t *testing.T) { + lim := ParseRateLimiter(&config.LimiterConfig{ + Name: "redis-list-rate-lim", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "rate-list", + Type: "list", + }, + }) + if lim == nil { + t.Fatal("expected non-nil rate limiter") + } +} + +func TestParseRateLimiter_FileLoader(t *testing.T) { + lim := ParseRateLimiter(&config.LimiterConfig{ + Name: "file-rate-lim", + File: &config.FileLoader{Path: "/tmp/rate_limiter.txt"}, + }) + if lim == nil { + t.Fatal("expected non-nil rate limiter") + } +} diff --git a/config/parsing/logger/parse_test.go b/config/parsing/logger/parse_test.go new file mode 100644 index 00000000..2f76fadc --- /dev/null +++ b/config/parsing/logger/parse_test.go @@ -0,0 +1,142 @@ +package logger + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseLogger_Nil(t *testing.T) { + lg := ParseLogger(nil) + if lg != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseLogger_NilLog(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "test", + Log: nil, + }) + if lg != nil { + t.Fatal("expected nil when Log is nil") + } +} + +func TestParseLogger_Levels(t *testing.T) { + for _, level := range []string{"trace", "debug", "info", "warn", "error", "fatal"} { + t.Run(level, func(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "test-" + level, + Log: &config.LogConfig{ + Level: level, + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } + }) + } +} + +func TestParseLogger_OutputNone(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "null-logger", + Log: &config.LogConfig{ + Output: "none", + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } +} + +func TestParseLogger_OutputStdout(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "stdout-logger", + Log: &config.LogConfig{ + Output: "stdout", + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } +} + +func TestParseLogger_OutputStderr(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "stderr-logger", + Log: &config.LogConfig{ + Output: "stderr", + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } +} + +func TestParseLogger_OutputDefault(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "default-logger", + Log: &config.LogConfig{ + Output: "", + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } +} + +func TestParseLogger_WithFormat(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "format-logger", + Log: &config.LogConfig{ + Format: "json", + Level: "info", + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } +} + +func TestParseLogger_FileOutput(t *testing.T) { + lg := ParseLogger(&config.LoggerConfig{ + Name: "file-logger", + Log: &config.LogConfig{ + Output: "/tmp/gost-test.log", + Rotation: &config.LogRotationConfig{ + MaxSize: 10, + MaxAge: 7, + MaxBackups: 5, + LocalTime: true, + Compress: true, + }, + }, + }) + if lg == nil { + t.Fatal("expected non-nil logger") + } +} + +func TestList_EmptyLogger(t *testing.T) { + got := List("nonexistent") + if len(got) != 0 { + t.Fatal("expected empty list for unregistered name") + } +} + +func TestList_WithNames(t *testing.T) { + got := List("nonexistent", "also_nonexistent") + if len(got) != 0 { + t.Fatal("expected empty list for unregistered names") + } +} diff --git a/config/parsing/node/parse_test.go b/config/parsing/node/parse_test.go new file mode 100644 index 00000000..8d548f00 --- /dev/null +++ b/config/parsing/node/parse_test.go @@ -0,0 +1,461 @@ +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" + + // 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_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_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") + } +} diff --git a/config/parsing/observer/parse_test.go b/config/parsing/observer/parse_test.go new file mode 100644 index 00000000..b1e04809 --- /dev/null +++ b/config/parsing/observer/parse_test.go @@ -0,0 +1,75 @@ +package observer + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseObserver_Nil(t *testing.T) { + obs := ParseObserver(nil) + if obs != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseObserver_NilPlugin(t *testing.T) { + obs := ParseObserver(&config.ObserverConfig{ + Name: "no-plugin", + Plugin: nil, + }) + if obs != nil { + t.Fatal("expected nil when plugin is nil") + } +} + +func TestParseObserver_PluginHTTP(t *testing.T) { + obs := ParseObserver(&config.ObserverConfig{ + Name: "http-obs", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if obs == nil { + t.Fatal("expected non-nil observer") + } +} + +func TestParseObserver_PluginGRPC(t *testing.T) { + obs := ParseObserver(&config.ObserverConfig{ + Name: "grpc-obs", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "observer.local", + }, + }, + }) + if obs == nil { + t.Fatal("expected non-nil observer") + } +} + +func TestParseObserver_PluginDefaultType(t *testing.T) { + obs := ParseObserver(&config.ObserverConfig{ + Name: "default-obs", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if obs == nil { + t.Fatal("expected non-nil observer") + } +} diff --git a/config/parsing/parser/parse_test.go b/config/parsing/parser/parse_test.go new file mode 100644 index 00000000..8920bc1f --- /dev/null +++ b/config/parsing/parser/parse_test.go @@ -0,0 +1,227 @@ +package parser + +import ( + "testing" + + "github.com/go-gost/x/config" +) + +func TestMergeConfig_NilBoth(t *testing.T) { + got := mergeConfig(nil, nil) + if got != nil { + t.Fatal("expected nil for two nil inputs") + } +} + +func TestMergeConfig_NilFirst(t *testing.T) { + cfg2 := &config.Config{ + Services: []*config.ServiceConfig{{Name: "svc"}}, + } + got := mergeConfig(nil, cfg2) + if len(got.Services) != 1 || got.Services[0].Name != "svc" { + t.Fatal("expected cfg2 to be returned when cfg1 is nil") + } +} + +func TestMergeConfig_NilSecond(t *testing.T) { + cfg1 := &config.Config{ + Services: []*config.ServiceConfig{{Name: "svc"}}, + } + got := mergeConfig(cfg1, nil) + if len(got.Services) != 1 || got.Services[0].Name != "svc" { + t.Fatal("expected cfg1 to be returned when cfg2 is nil") + } +} + +func TestMergeConfig_AppendsAllSlices(t *testing.T) { + cfg1 := &config.Config{ + Services: []*config.ServiceConfig{{Name: "s1"}}, + Chains: []*config.ChainConfig{{Name: "c1"}}, + Hops: []*config.HopConfig{{Name: "h1"}}, + Authers: []*config.AutherConfig{{Name: "a1"}}, + Admissions: []*config.AdmissionConfig{{Name: "ad1"}}, + Bypasses: []*config.BypassConfig{{Name: "b1"}}, + Resolvers: []*config.ResolverConfig{{Name: "r1"}}, + Hosts: []*config.HostsConfig{{Name: "hosts1"}}, + Ingresses: []*config.IngressConfig{{Name: "i1"}}, + SDs: []*config.SDConfig{{Name: "sd1"}}, + Recorders: []*config.RecorderConfig{{Name: "rec1"}}, + Limiters: []*config.LimiterConfig{{Name: "l1"}}, + CLimiters: []*config.LimiterConfig{{Name: "cl1"}}, + RLimiters: []*config.LimiterConfig{{Name: "rl1"}}, + Loggers: []*config.LoggerConfig{{Name: "log1"}}, + Routers: []*config.RouterConfig{{Name: "rt1"}}, + Observers: []*config.ObserverConfig{{Name: "obs1"}}, + } + cfg2 := &config.Config{ + Services: []*config.ServiceConfig{{Name: "s2"}}, + Chains: []*config.ChainConfig{{Name: "c2"}}, + Hops: []*config.HopConfig{{Name: "h2"}}, + Authers: []*config.AutherConfig{{Name: "a2"}}, + Admissions: []*config.AdmissionConfig{{Name: "ad2"}}, + Bypasses: []*config.BypassConfig{{Name: "b2"}}, + Resolvers: []*config.ResolverConfig{{Name: "r2"}}, + Hosts: []*config.HostsConfig{{Name: "hosts2"}}, + Ingresses: []*config.IngressConfig{{Name: "i2"}}, + SDs: []*config.SDConfig{{Name: "sd2"}}, + Recorders: []*config.RecorderConfig{{Name: "rec2"}}, + Limiters: []*config.LimiterConfig{{Name: "l2"}}, + CLimiters: []*config.LimiterConfig{{Name: "cl2"}}, + RLimiters: []*config.LimiterConfig{{Name: "rl2"}}, + Loggers: []*config.LoggerConfig{{Name: "log2"}}, + Routers: []*config.RouterConfig{{Name: "rt2"}}, + Observers: []*config.ObserverConfig{{Name: "obs2"}}, + } + + got := mergeConfig(cfg1, cfg2) + + if len(got.Services) != 2 || got.Services[0].Name != "s1" || got.Services[1].Name != "s2" { + t.Fatal("services not properly appended") + } + if len(got.Chains) != 2 { + t.Fatal("chains not properly appended") + } + if len(got.Hops) != 2 { + t.Fatal("hops not properly appended") + } + if len(got.Authers) != 2 { + t.Fatal("authers not properly appended") + } + if len(got.Admissions) != 2 { + t.Fatal("admissions not properly appended") + } + if len(got.Bypasses) != 2 { + t.Fatal("bypasses not properly appended") + } + if len(got.Resolvers) != 2 { + t.Fatal("resolvers not properly appended") + } + if len(got.Hosts) != 2 { + t.Fatal("hosts not properly appended") + } + if len(got.Ingresses) != 2 { + t.Fatal("ingresses not properly appended") + } + if len(got.SDs) != 2 { + t.Fatal("SDs not properly appended") + } + if len(got.Recorders) != 2 { + t.Fatal("recorders not properly appended") + } + if len(got.Limiters) != 2 { + t.Fatal("limiters not properly appended") + } + if len(got.CLimiters) != 2 { + t.Fatal("climiters not properly appended") + } + if len(got.RLimiters) != 2 { + t.Fatal("rlimiters not properly appended") + } + if len(got.Loggers) != 2 { + t.Fatal("loggers not properly appended") + } + if len(got.Routers) != 2 { + t.Fatal("routers not properly appended") + } + if len(got.Observers) != 2 { + t.Fatal("observers not properly appended") + } +} + +func TestMergeConfig_ScalarOverrides(t *testing.T) { + cfg1 := &config.Config{ + TLS: &config.TLSConfig{ServerName: "old"}, + Log: &config.LogConfig{Level: "info"}, + API: &config.APIConfig{Addr: ":8080"}, + Metrics: &config.MetricsConfig{Addr: ":9090"}, + Profiling: &config.ProfilingConfig{Addr: ":6060"}, + } + cfg2 := &config.Config{ + TLS: &config.TLSConfig{ServerName: "new"}, + Log: &config.LogConfig{Level: "debug"}, + API: &config.APIConfig{Addr: ":8081"}, + Metrics: &config.MetricsConfig{Addr: ":9091"}, + Profiling: &config.ProfilingConfig{Addr: ":6061"}, + } + + got := mergeConfig(cfg1, cfg2) + + if got.TLS.ServerName != "new" { + t.Fatalf("TLS not overridden: got %q, want %q", got.TLS.ServerName, "new") + } + if got.Log.Level != "debug" { + t.Fatalf("Log not overridden: got %q, want %q", got.Log.Level, "debug") + } + if got.API.Addr != ":8081" { + t.Fatalf("API not overridden: got %q, want %q", got.API.Addr, ":8081") + } + if got.Metrics.Addr != ":9091" { + t.Fatalf("Metrics not overridden: got %q, want %q", got.Metrics.Addr, ":9091") + } + if got.Profiling.Addr != ":6061" { + t.Fatalf("Profiling not overridden: got %q, want %q", got.Profiling.Addr, ":6061") + } +} + +func TestMergeConfig_ScalarKeepsCfg1WhenCfg2Nil(t *testing.T) { + cfg1 := &config.Config{ + TLS: &config.TLSConfig{ServerName: "keep"}, + Log: &config.LogConfig{Level: "warn"}, + API: &config.APIConfig{Addr: ":8080"}, + Metrics: &config.MetricsConfig{Addr: ":9090"}, + Profiling: &config.ProfilingConfig{Addr: ":6060"}, + } + cfg2 := &config.Config{} // all scalars nil + + got := mergeConfig(cfg1, cfg2) + + if got.TLS.ServerName != "keep" { + t.Fatal("TLS should be kept from cfg1") + } + if got.Log.Level != "warn" { + t.Fatal("Log should be kept from cfg1") + } + if got.API.Addr != ":8080" { + t.Fatal("API should be kept from cfg1") + } + if got.Metrics.Addr != ":9090" { + t.Fatal("Metrics should be kept from cfg1") + } + if got.Profiling.Addr != ":6060" { + t.Fatal("Profiling should be kept from cfg1") + } +} + +func TestInit(t *testing.T) { + Init(Args{ + CfgFile: "test.yml", + Services: []string{"s1"}, + Nodes: []string{"n1"}, + Debug: true, + Trace: false, + ApiAddr: ":8080", + MetricsAddr: ":9090", + }) + + if defaultParser.args.CfgFile != "test.yml" { + t.Fatalf("CfgFile not set: got %q", defaultParser.args.CfgFile) + } + if len(defaultParser.args.Services) != 1 || defaultParser.args.Services[0] != "s1" { + t.Fatal("Services not set") + } + if len(defaultParser.args.Nodes) != 1 || defaultParser.args.Nodes[0] != "n1" { + t.Fatal("Nodes not set") + } + if !defaultParser.args.Debug { + t.Fatal("Debug not set") + } + if defaultParser.args.Trace { + t.Fatal("Trace should be false") + } + if defaultParser.args.ApiAddr != ":8080" { + t.Fatal("ApiAddr not set") + } + if defaultParser.args.MetricsAddr != ":9090" { + t.Fatal("MetricsAddr not set") + } +} diff --git a/config/parsing/recorder/parse_test.go b/config/parsing/recorder/parse_test.go new file mode 100644 index 00000000..0ac96585 --- /dev/null +++ b/config/parsing/recorder/parse_test.go @@ -0,0 +1,199 @@ +package recorder + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseRecorder_Nil(t *testing.T) { + r := ParseRecorder(nil) + if r != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseRecorder_PluginHTTP(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "http-rec", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder") + } +} + +func TestParseRecorder_PluginGRPC(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "grpc-rec", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "recorder.local", + }, + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder") + } +} + +func TestParseRecorder_PluginDefaultType(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "default-rec", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder") + } +} + +func TestParseRecorder_TCP(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "tcp-rec", + TCP: &config.TCPRecorder{ + Addr: "127.0.0.1:9999", + Timeout: 0, + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for TCP") + } +} + +func TestParseRecorder_HTTP(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "http-rec", + HTTP: &config.HTTPRecorder{ + URL: "http://localhost:8080/record", + Timeout: 0, + Header: map[string]string{"X-Test": "value"}, + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for HTTP") + } +} + +func TestParseRecorder_File(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "file-rec", + File: &config.FileRecorder{ + Path: "/tmp/recorder.log", + Sep: "\n", + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for file") + } +} + +func TestParseRecorder_FileWithRotation(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "file-rot-rec", + File: &config.FileRecorder{ + Path: "/tmp/recorder-rot.log", + Rotation: &config.LogRotationConfig{ + MaxSize: 100, + MaxAge: 30, + MaxBackups: 10, + LocalTime: true, + Compress: true, + }, + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for file with rotation") + } +} + +func TestParseRecorder_RedisSet(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "redis-set-rec", + Redis: &config.RedisRecorder{ + Addr: "127.0.0.1:6379", + Key: "recorder-set", + Type: "set", + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for redis set") + } +} + +func TestParseRecorder_RedisList(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "redis-list-rec", + Redis: &config.RedisRecorder{ + Addr: "127.0.0.1:6379", + Key: "recorder-list", + Type: "list", + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for redis list") + } +} + +func TestParseRecorder_RedisSortedSet(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "redis-sset-rec", + Redis: &config.RedisRecorder{ + Addr: "127.0.0.1:6379", + Key: "recorder-sset", + Type: "sset", + }, + }) + if r == nil { + t.Fatal("expected non-nil recorder for redis sorted set") + } +} + +func TestParseRecorder_EmptyFileReturnsNil(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "empty-rec", + }) + if r != nil { + t.Fatal("expected nil recorder when no sub-config is provided") + } +} + +func TestParseRecorder_FileWithEmptyPathReturnsNil(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "empty-path-rec", + File: &config.FileRecorder{ + Path: "", + }, + }) + if r != nil { + t.Fatal("expected nil recorder when file path is empty") + } +} + +func TestParseRecorder_TCPWithEmptyAddrReturnsNil(t *testing.T) { + r := ParseRecorder(&config.RecorderConfig{ + Name: "empty-tcp-rec", + TCP: &config.TCPRecorder{ + Addr: "", + }, + }) + if r != nil { + t.Fatal("expected nil recorder when TCP addr is empty") + } +} diff --git a/config/parsing/resolver/parse_test.go b/config/parsing/resolver/parse_test.go new file mode 100644 index 00000000..5c407733 --- /dev/null +++ b/config/parsing/resolver/parse_test.go @@ -0,0 +1,146 @@ +package resolver + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseResolver_Nil(t *testing.T) { + r, err := ParseResolver(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseResolver_PluginHTTP(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "http-res", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver") + } +} + +func TestParseResolver_PluginGRPC(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "grpc-res", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "resolver.local", + }, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver") + } +} + +func TestParseResolver_PluginDefaultType(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "default-res", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver") + } +} + +func TestParseResolver_WithNameservers(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "ns-res", + Nameservers: []*config.NameserverConfig{ + {Addr: "8.8.8.8", Prefer: "ipv4"}, + {Addr: "8.8.4.4", Prefer: "ipv4"}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver") + } +} + +func TestParseResolver_EmptyNameservers(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "empty-ns", + Nameservers: []*config.NameserverConfig{}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver even with empty nameservers") + } +} + +func TestParseResolver_NameserverWithClientIP(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "clientip-res", + Nameservers: []*config.NameserverConfig{ + {Addr: "8.8.8.8", ClientIP: "1.2.3.4"}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver") + } +} + +func TestParseResolver_NameserverWithAllFields(t *testing.T) { + r, err := ParseResolver(&config.ResolverConfig{ + Name: "full-res", + Nameservers: []*config.NameserverConfig{ + { + Addr: "8.8.8.8:53", + Chain: "chain1", + Prefer: "ipv4", + ClientIP: "1.2.3.4", + Hostname: "dns.example.com", + TTL: 300, + Timeout: 5, + Async: true, + Only: "A", + }, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r == nil { + t.Fatal("expected non-nil resolver") + } +} diff --git a/config/parsing/router/parse_test.go b/config/parsing/router/parse_test.go new file mode 100644 index 00000000..253eae4a --- /dev/null +++ b/config/parsing/router/parse_test.go @@ -0,0 +1,237 @@ +package router + +import ( + "context" + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseRouter_Nil(t *testing.T) { + r := ParseRouter(nil) + if r != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseRouter_PluginHTTP(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "http-rt", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_PluginGRPC(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "grpc-rt", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "router.local", + }, + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_PluginDefaultType(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "default-rt", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_WithCIDRRoutes(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "cidr-rt", + Routes: []*config.RouterRouteConfig{ + {Net: "192.168.1.0/24", Gateway: "10.0.0.1"}, + {Net: "10.0.0.0/8", Gateway: "10.0.0.2"}, + }, + }) + if r == nil { + t.Fatal("expected non-nil router with CIDR routes") + } +} + +func TestParseRouter_WithDstRoutes(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "dst-rt", + Routes: []*config.RouterRouteConfig{ + {Dst: "192.168.1.0/24", Gateway: "10.0.0.1"}, + }, + }) + if r == nil { + t.Fatal("expected non-nil router with dst routes") + } +} + +func TestParseRouter_MissingGatewaySkipped(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "skip-rt", + Routes: []*config.RouterRouteConfig{ + {Net: "192.168.1.0/24", Gateway: ""}, // skipped: no gateway + {Net: "10.0.0.0/8", Gateway: "10.0.0.1"}, // kept + }, + }) + if r == nil { + t.Fatal("expected non-nil router (with one valid route)") + } +} + +func TestParseRouter_MissingDstSkipped(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "no-dst-rt", + Routes: []*config.RouterRouteConfig{ + {Dst: "", Gateway: "10.0.0.1"}, // skipped: no dst + }, + }) + if r == nil { + t.Fatal("expected non-nil router (all routes skipped)") + } +} + +func TestParseRouter_InvalidCIDRSkipped(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "invalid-cidr-rt", + Routes: []*config.RouterRouteConfig{ + {Net: "not-a-cidr", Gateway: "10.0.0.1"}, // invalid CIDR with empty Dst, skipped + {Net: "192.168.1.0/24", Gateway: "10.0.0.2"}, // valid + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_NilRouteSkipped(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "nil-route-rt", + Routes: []*config.RouterRouteConfig{ + nil, + {Net: "192.168.1.0/24", Gateway: "10.0.0.1"}, + }, + }) + if r == nil { + t.Fatal("expected non-nil router when nil route is in list") + } +} + +func TestParseRouter_EmptyRoutes(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "empty-routes", + Routes: []*config.RouterRouteConfig{}, + }) + if r == nil { + t.Fatal("expected non-nil router with empty routes") + } +} + +func TestParseRouter_FileLoader(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "file-rt", + File: &config.FileLoader{Path: "/tmp/routes.txt"}, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_HTTPLoader(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "http-rt", + HTTP: &config.HTTPLoader{URL: "http://localhost:8080/routes"}, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_RedisHashLoader(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "redis-hash-rt", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "routes-hash", + Type: "hash", + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_RedisListLoader(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "redis-list-rt", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "routes-list", + Type: "list", + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_RedisSetLoader(t *testing.T) { + r := ParseRouter(&config.RouterConfig{ + Name: "redis-set-rt", + Redis: &config.RedisLoader{ + Addr: "127.0.0.1:6379", + Key: "routes-set", + Type: "set", + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } +} + +func TestParseRouter_DstSyncedFromNet(t *testing.T) { + // When Dst is empty but Net is provided, Net is converted to Dst + r := ParseRouter(&config.RouterConfig{ + Name: "net-to-dst-rt", + Routes: []*config.RouterRouteConfig{ + {Net: "192.168.1.0/24", Gateway: "10.0.0.1"}, + }, + }) + if r == nil { + t.Fatal("expected non-nil router") + } + route := r.GetRoute(context.Background(), "192.168.1.5") + if route == nil { + t.Fatal("expected non-nil route for IP in CIDR range") + } + if route.Dst != "192.168.1.0/24" { + t.Fatalf("Dst = %q, want %q", route.Dst, "192.168.1.0/24") + } + if route.Gateway != "10.0.0.1" { + t.Fatalf("Gateway = %q, want %q", route.Gateway, "10.0.0.1") + } +} diff --git a/config/parsing/sd/parse_test.go b/config/parsing/sd/parse_test.go new file mode 100644 index 00000000..b814330a --- /dev/null +++ b/config/parsing/sd/parse_test.go @@ -0,0 +1,75 @@ +package sd + +import ( + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestParseSD_Nil(t *testing.T) { + s := ParseSD(nil) + if s != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseSD_NilPlugin(t *testing.T) { + s := ParseSD(&config.SDConfig{ + Name: "no-plugin", + Plugin: nil, + }) + if s != nil { + t.Fatal("expected nil when plugin is nil") + } +} + +func TestParseSD_PluginHTTP(t *testing.T) { + s := ParseSD(&config.SDConfig{ + Name: "http-sd", + Plugin: &config.PluginConfig{ + Type: "http", + Addr: "127.0.0.1:9000", + }, + }) + if s == nil { + t.Fatal("expected non-nil SD") + } +} + +func TestParseSD_PluginGRPC(t *testing.T) { + s := ParseSD(&config.SDConfig{ + Name: "grpc-sd", + Plugin: &config.PluginConfig{ + Type: "grpc", + Addr: "127.0.0.1:9001", + Token: "secret", + TLS: &config.TLSConfig{ + Secure: true, + ServerName: "sd.local", + }, + }, + }) + if s == nil { + t.Fatal("expected non-nil SD") + } +} + +func TestParseSD_PluginDefaultType(t *testing.T) { + s := ParseSD(&config.SDConfig{ + Name: "default-sd", + Plugin: &config.PluginConfig{ + Addr: "127.0.0.1:9002", + }, + }) + if s == nil { + t.Fatal("expected non-nil SD") + } +} diff --git a/config/parsing/selector/parse_test.go b/config/parsing/selector/parse_test.go new file mode 100644 index 00000000..642d44bd --- /dev/null +++ b/config/parsing/selector/parse_test.go @@ -0,0 +1,144 @@ +package selector + +import ( + "testing" + + "github.com/go-gost/x/config" +) + +func TestParseChainSelector_Nil(t *testing.T) { + sel := ParseChainSelector(nil) + if sel != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseChainSelector_RoundRobin(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "round"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseChainSelector_RR(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "rr"}) + if sel == nil { + t.Fatal("expected non-nil selector for rr strategy") + } +} + +func TestParseChainSelector_Random(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "random"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseChainSelector_Rand(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "rand"}) + if sel == nil { + t.Fatal("expected non-nil selector for rand strategy") + } +} + +func TestParseChainSelector_FIFO(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "fifo"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseChainSelector_HA(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "ha"}) + if sel == nil { + t.Fatal("expected non-nil selector for ha strategy") + } +} + +func TestParseChainSelector_Hash(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "hash"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseChainSelector_DefaultStrategy(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{Strategy: "unknown"}) + if sel == nil { + t.Fatal("expected non-nil selector for unknown strategy (should default to round-robin)") + } +} + +func TestParseChainSelector_WithMaxFails(t *testing.T) { + sel := ParseChainSelector(&config.SelectorConfig{ + Strategy: "round", + MaxFails: 3, + FailTimeout: 0, + }) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseNodeSelector_Nil(t *testing.T) { + sel := ParseNodeSelector(nil) + if sel != nil { + t.Fatal("expected nil for nil config") + } +} + +func TestParseNodeSelector_RoundRobin(t *testing.T) { + sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "round"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseNodeSelector_Random(t *testing.T) { + sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "random"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseNodeSelector_FIFO(t *testing.T) { + sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "fifo"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseNodeSelector_Hash(t *testing.T) { + sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "hash"}) + if sel == nil { + t.Fatal("expected non-nil selector") + } +} + +func TestParseNodeSelector_Parallel(t *testing.T) { + sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "parallel"}) + if sel == nil { + t.Fatal("expected non-nil selector for parallel strategy") + } +} + +func TestParseNodeSelector_DefaultStrategy(t *testing.T) { + sel := ParseNodeSelector(&config.SelectorConfig{Strategy: "unknown"}) + if sel == nil { + t.Fatal("expected non-nil selector for unknown strategy (should default to round-robin)") + } +} + +func TestDefaultNodeSelector(t *testing.T) { + sel := DefaultNodeSelector() + if sel == nil { + t.Fatal("expected non-nil default node selector") + } +} + +func TestDefaultChainSelector(t *testing.T) { + sel := DefaultChainSelector() + if sel == nil { + t.Fatal("expected non-nil default chain selector") + } +} diff --git a/config/parsing/tls_test.go b/config/parsing/tls_test.go new file mode 100644 index 00000000..e783d9d8 --- /dev/null +++ b/config/parsing/tls_test.go @@ -0,0 +1,103 @@ +package parsing + +import ( + "bytes" + "crypto/tls" + "io" + "testing" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + xlogger "github.com/go-gost/x/logger" +) + +func TestMain(m *testing.M) { + logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard))) + m.Run() +} + +func TestSetDefaultTLSConfig(t *testing.T) { + // Save and restore original + orig := DefaultTLSConfig() + defer SetDefaultTLSConfig(orig) + + cfg := &tls.Config{ServerName: "test.local"} + SetDefaultTLSConfig(cfg) + + got := DefaultTLSConfig() + if got == nil { + t.Fatal("expected non-nil default TLS config") + } + if got.ServerName != "test.local" { + t.Fatalf("ServerName = %q, want %q", got.ServerName, "test.local") + } +} + +func TestDefaultTLSConfig_InitiallyNil(t *testing.T) { + orig := DefaultTLSConfig() + defer SetDefaultTLSConfig(orig) + + // Store nil to reset + SetDefaultTLSConfig(nil) + + got := DefaultTLSConfig() + if got != nil { + t.Fatal("expected nil after storing nil") + } +} + +func TestBuildDefaultTLSConfig_Nil(t *testing.T) { + // BuildDefaultTLSConfig with nil should generate a self-signed cert + tlsCfg, err := BuildDefaultTLSConfig(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tlsCfg == nil { + t.Fatal("expected non-nil config") + } + if len(tlsCfg.Certificates) == 0 { + t.Fatal("expected at least one certificate") + } +} + +func TestBuildDefaultTLSConfig_WithOptions(t *testing.T) { + cfg := &config.TLSConfig{ + Validity: 0, // uses default + Organization: "TestOrg", + CommonName: "test.example.com", + } + tlsCfg, err := BuildDefaultTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tlsCfg == nil { + t.Fatal("expected non-nil config") + } + if len(tlsCfg.Certificates) == 0 { + t.Fatal("expected at least one certificate") + } +} + +func TestBuildDefaultTLSConfig_TwoCallsDifferentCerts(t *testing.T) { + cfg1, err := BuildDefaultTLSConfig(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + cfg2, err := BuildDefaultTLSConfig(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cfg1.Certificates) == 0 || len(cfg2.Certificates) == 0 { + t.Fatal("expected certificates in both configs") + } + + // Each call generates a random cert; they should have different raw bytes. + if len(cfg1.Certificates[0].Certificate) == 0 || len(cfg2.Certificates[0].Certificate) == 0 { + t.Fatal("expected non-empty certificate raw bytes") + } + c1 := cfg1.Certificates[0].Certificate[0] + c2 := cfg2.Certificates[0].Certificate[0] + if bytes.Equal(c1, c2) { + t.Fatal("expected different certificates from two calls") + } +}