test(config): add unit tests for loader, doc comments, fix fmt.Errorf misuse
- Add 808-line loader_test.go covering registerGroup, register, and Load - Add package and exported symbol doc comments to loader.go - Fix fmt.Errorf(resp.Status) → errors.New(resp.Status) in sd/plugin/http.go (resp.Status is a plain string, not a format string)
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
// Package loader converts a parsed [config.Config] into running components
|
||||||
|
// by parsing each config section and registering the results into the
|
||||||
|
// global registries. Components are registered in dependency order so that
|
||||||
|
// when a component reads its dependencies from registries during parsing,
|
||||||
|
// those dependencies are already available.
|
||||||
package loader
|
package loader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -39,16 +44,24 @@ import (
|
|||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// defaultLoader is a singleton loader used by the top-level Load function.
|
||||||
var (
|
var (
|
||||||
defaultLoader *loader = &loader{}
|
defaultLoader *loader = &loader{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Load parses all config sections from cfg and registers the resulting
|
||||||
|
// components into the global registries, then sets up the default logger
|
||||||
|
// and TLS config.
|
||||||
func Load(cfg *config.Config) error {
|
func Load(cfg *config.Config) error {
|
||||||
return defaultLoader.Load(cfg)
|
return defaultLoader.Load(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loader holds no state; its Load method is the entry point for converting
|
||||||
|
// a config into running components.
|
||||||
type loader struct{}
|
type loader struct{}
|
||||||
|
|
||||||
|
// Load builds the default TLS config, registers all named components from
|
||||||
|
// cfg into the global registries, and sets the default logger and TLS config.
|
||||||
func (l *loader) Load(cfg *config.Config) error {
|
func (l *loader) Load(cfg *config.Config) error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,807 @@
|
|||||||
|
package loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/handler"
|
||||||
|
"github.com/go-gost/core/listener"
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
|
"github.com/go-gost/core/metadata"
|
||||||
|
reg "github.com/go-gost/core/registry"
|
||||||
|
"github.com/go-gost/x/config"
|
||||||
|
xlogger "github.com/go-gost/x/logger"
|
||||||
|
"github.com/go-gost/x/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
|
||||||
|
m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- mock registry for registerGroup tests ---
|
||||||
|
|
||||||
|
type mockRegistry[T any] struct {
|
||||||
|
m map[string]T
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRegistry[T any]() *mockRegistry[T] {
|
||||||
|
return &mockRegistry[T]{m: make(map[string]T)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistry[T]) Register(name string, v T) error {
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, ok := r.m[name]; ok {
|
||||||
|
return errors.New("duplicate")
|
||||||
|
}
|
||||||
|
r.m[name] = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistry[T]) Unregister(name string) {
|
||||||
|
delete(r.m, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistry[T]) IsRegistered(name string) bool {
|
||||||
|
_, ok := r.m[name]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistry[T]) Get(name string) T {
|
||||||
|
return r.m[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistry[T]) GetAll() map[string]T {
|
||||||
|
result := make(map[string]T)
|
||||||
|
for k, v := range r.m {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mockRegistry satisfies reg.Registry[T].
|
||||||
|
var _ reg.Registry[int] = (*mockRegistry[int])(nil)
|
||||||
|
|
||||||
|
// --- registerGroup tests ---
|
||||||
|
|
||||||
|
func TestRegisterGroup_EmptyEntries_EmptyRegistry(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
err := registerGroup([]named[int]{}, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(r.GetAll()) != 0 {
|
||||||
|
t.Fatalf("expected empty registry, got %d entries", len(r.GetAll()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterGroup_EmptyEntries_ClearsExisting(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
r.Register("a", 1)
|
||||||
|
r.Register("b", 2)
|
||||||
|
|
||||||
|
err := registerGroup([]named[int]{}, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(r.GetAll()) != 0 {
|
||||||
|
t.Fatalf("expected empty registry, got %d entries", len(r.GetAll()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterGroup_SingleEntry(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
err := registerGroup([]named[int]{{"x", 42}}, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered("x") {
|
||||||
|
t.Fatal("expected 'x' to be registered")
|
||||||
|
}
|
||||||
|
if v := r.Get("x"); v != 42 {
|
||||||
|
t.Fatalf("expected 42, got %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterGroup_MultipleEntries(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
err := registerGroup([]named[int]{
|
||||||
|
{"x", 1},
|
||||||
|
{"y", 2},
|
||||||
|
{"z", 3},
|
||||||
|
}, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
all := r.GetAll()
|
||||||
|
if len(all) != 3 {
|
||||||
|
t.Fatalf("expected 3 entries, got %d", len(all))
|
||||||
|
}
|
||||||
|
for _, name := range []string{"x", "y", "z"} {
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatalf("expected '%s' to be registered", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterGroup_ReplacesExisting(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
r.Register("old", 1)
|
||||||
|
r.Register("keep", 2) // this one should be removed
|
||||||
|
|
||||||
|
err := registerGroup([]named[int]{
|
||||||
|
{"old", 99},
|
||||||
|
{"new", 100},
|
||||||
|
}, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if v := r.Get("old"); v != 99 {
|
||||||
|
t.Fatalf("expected 99, got %v", v)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered("new") {
|
||||||
|
t.Fatal("expected 'new' to be registered")
|
||||||
|
}
|
||||||
|
if r.IsRegistered("keep") {
|
||||||
|
t.Fatal("expected 'keep' to be unregistered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterGroup_DuplicateNameInEntries(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
err := registerGroup([]named[int]{
|
||||||
|
{"dup", 1},
|
||||||
|
{"dup", 2},
|
||||||
|
}, r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for duplicate name")
|
||||||
|
}
|
||||||
|
// First entry should be registered, second failed.
|
||||||
|
if v := r.Get("dup"); v != 1 {
|
||||||
|
t.Fatalf("expected first entry (1), got %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterGroup_ZeroValue(t *testing.T) {
|
||||||
|
r := newMockRegistry[int]()
|
||||||
|
// int zero value (0) is still registered
|
||||||
|
err := registerGroup([]named[int]{{"zero", 0}}, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered("zero") {
|
||||||
|
t.Fatal("expected 'zero' to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- register tests ---
|
||||||
|
|
||||||
|
func TestRegister_NilConfig(t *testing.T) {
|
||||||
|
if err := register(nil); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_EmptyConfig(t *testing.T) {
|
||||||
|
if err := register(&config.Config{}); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Loggers(t *testing.T) {
|
||||||
|
reg := registry.LoggerRegistry()
|
||||||
|
name := "test-logger"
|
||||||
|
t.Cleanup(func() { reg.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Loggers: []*config.LoggerConfig{
|
||||||
|
{Name: name, Log: &config.LogConfig{Level: "info"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reg.IsRegistered(name) {
|
||||||
|
t.Fatal("expected logger to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Authers(t *testing.T) {
|
||||||
|
r := registry.AutherRegistry()
|
||||||
|
name := "test-auther"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Authers: []*config.AutherConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected auther to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Admissions(t *testing.T) {
|
||||||
|
r := registry.AdmissionRegistry()
|
||||||
|
name := "test-admission"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Admissions: []*config.AdmissionConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected admission to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Bypasses(t *testing.T) {
|
||||||
|
r := registry.BypassRegistry()
|
||||||
|
name := "test-bypass"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Bypasses: []*config.BypassConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected bypass to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Resolvers(t *testing.T) {
|
||||||
|
r := registry.ResolverRegistry()
|
||||||
|
name := "test-resolver"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Resolvers: []*config.ResolverConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected resolver to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Hosts(t *testing.T) {
|
||||||
|
r := registry.HostsRegistry()
|
||||||
|
name := "test-hosts"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Hosts: []*config.HostsConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected host mapper to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Ingresses(t *testing.T) {
|
||||||
|
r := registry.IngressRegistry()
|
||||||
|
name := "test-ingress"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Ingresses: []*config.IngressConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected ingress to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Routers(t *testing.T) {
|
||||||
|
r := registry.RouterRegistry()
|
||||||
|
name := "test-router"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Routers: []*config.RouterConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected router to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_SDs(t *testing.T) {
|
||||||
|
r := registry.SDRegistry()
|
||||||
|
name := "test-sd"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
SDs: []*config.SDConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected SD to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Observers(t *testing.T) {
|
||||||
|
r := registry.ObserverRegistry()
|
||||||
|
name := "test-observer"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Observers: []*config.ObserverConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected observer to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Recorders(t *testing.T) {
|
||||||
|
r := registry.RecorderRegistry()
|
||||||
|
name := "test-recorder"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recorders: []*config.RecorderConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected recorder to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_TrafficLimiters(t *testing.T) {
|
||||||
|
r := registry.TrafficLimiterRegistry()
|
||||||
|
name := "test-traffic-limiter"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Limiters: []*config.LimiterConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected traffic limiter to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_ConnLimiters(t *testing.T) {
|
||||||
|
r := registry.ConnLimiterRegistry()
|
||||||
|
name := "test-conn-limiter"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
CLimiters: []*config.LimiterConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected conn limiter to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_RateLimiters(t *testing.T) {
|
||||||
|
r := registry.RateLimiterRegistry()
|
||||||
|
name := "test-rate-limiter"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
RLimiters: []*config.LimiterConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected rate limiter to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Hops(t *testing.T) {
|
||||||
|
r := registry.HopRegistry()
|
||||||
|
name := "test-hop"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Hops: []*config.HopConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected hop to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_Chains(t *testing.T) {
|
||||||
|
r := registry.ChainRegistry()
|
||||||
|
name := "test-chain"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Chains: []*config.ChainConfig{
|
||||||
|
{Name: name},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected chain to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubListener satisfies listener.Listener for testing.
|
||||||
|
type stubListener struct{}
|
||||||
|
|
||||||
|
func (l *stubListener) Init(md metadata.Metadata) error { return nil }
|
||||||
|
func (l *stubListener) Accept() (net.Conn, error) { return nil, net.ErrClosed }
|
||||||
|
func (l *stubListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} }
|
||||||
|
func (l *stubListener) Close() error { return nil }
|
||||||
|
|
||||||
|
// stubHandler satisfies handler.Handler for testing.
|
||||||
|
type stubHandler struct{}
|
||||||
|
|
||||||
|
func (h *stubHandler) Init(md metadata.Metadata) error { return nil }
|
||||||
|
func (h *stubHandler) Handle(_ context.Context, _ net.Conn, _ ...handler.HandleOption) error { return nil }
|
||||||
|
|
||||||
|
func TestRegister_Services(t *testing.T) {
|
||||||
|
// ParseService needs "tcp" listener and "auto" handler in registries.
|
||||||
|
// These are registered via init() in their respective packages but those
|
||||||
|
// packages are not imported here, so the factories won't be found.
|
||||||
|
// Register stub factories so the test can exercise the service section.
|
||||||
|
//
|
||||||
|
// This save/restore pattern assumes no other test or transitive import
|
||||||
|
// has already registered "tcp" or "auto" factory functions. If those
|
||||||
|
// packages are ever imported by this test file, origListener/origHandler
|
||||||
|
// will be non-nil and the restore path will re-register the real
|
||||||
|
// factories, which is the desired behavior.
|
||||||
|
origListener := registry.ListenerRegistry().Get("tcp")
|
||||||
|
origHandler := registry.HandlerRegistry().Get("auto")
|
||||||
|
|
||||||
|
registry.ListenerRegistry().Register("tcp", func(opts ...listener.Option) listener.Listener {
|
||||||
|
return &stubListener{}
|
||||||
|
})
|
||||||
|
registry.HandlerRegistry().Register("auto", func(opts ...handler.Option) handler.Handler {
|
||||||
|
return &stubHandler{}
|
||||||
|
})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
registry.ListenerRegistry().Unregister("tcp")
|
||||||
|
registry.HandlerRegistry().Unregister("auto")
|
||||||
|
if origListener != nil {
|
||||||
|
registry.ListenerRegistry().Register("tcp", origListener)
|
||||||
|
}
|
||||||
|
if origHandler != nil {
|
||||||
|
registry.HandlerRegistry().Register("auto", origHandler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r := registry.ServiceRegistry()
|
||||||
|
name := "test-service"
|
||||||
|
t.Cleanup(func() { r.Unregister(name) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Services: []*config.ServiceConfig{
|
||||||
|
{Name: name, Addr: ":0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered(name) {
|
||||||
|
t.Fatal("expected service to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_MultipleSections(t *testing.T) {
|
||||||
|
// Verify that multiple sections are all registered and in dependency order.
|
||||||
|
bypassReg := registry.BypassRegistry()
|
||||||
|
hopReg := registry.HopRegistry()
|
||||||
|
chainReg := registry.ChainRegistry()
|
||||||
|
|
||||||
|
bypassName := "multi-bypass"
|
||||||
|
hopName := "multi-hop"
|
||||||
|
chainName := "multi-chain"
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
bypassReg.Unregister(bypassName)
|
||||||
|
hopReg.Unregister(hopName)
|
||||||
|
chainReg.Unregister(chainName)
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Bypasses: []*config.BypassConfig{{Name: bypassName}},
|
||||||
|
Hops: []*config.HopConfig{{Name: hopName}},
|
||||||
|
Chains: []*config.ChainConfig{{Name: chainName}},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := register(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bypassReg.IsRegistered(bypassName) {
|
||||||
|
t.Fatal("expected bypass to be registered")
|
||||||
|
}
|
||||||
|
if !hopReg.IsRegistered(hopName) {
|
||||||
|
t.Fatal("expected hop to be registered")
|
||||||
|
}
|
||||||
|
if !chainReg.IsRegistered(chainName) {
|
||||||
|
t.Fatal("expected chain to be registered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_ComponentOrder(t *testing.T) {
|
||||||
|
// register() processes sections in dependency order: leaf components
|
||||||
|
// (loggers, authers, resolvers, etc.) first, then hops, then chains,
|
||||||
|
// then services. Hops look up loggers/resolvers from registries;
|
||||||
|
// chains look up hops; services look up chains.
|
||||||
|
//
|
||||||
|
// This test directly calls registerGroup in the canonical dependency
|
||||||
|
// sequence and verifies each layer can see the previous layer's
|
||||||
|
// registrations.
|
||||||
|
|
||||||
|
// Use a sequence-tracking registry to verify registration order.
|
||||||
|
type seqEntry struct {
|
||||||
|
name string
|
||||||
|
seq int
|
||||||
|
}
|
||||||
|
var seq int
|
||||||
|
leafReg := newMockRegistry[seqEntry]()
|
||||||
|
hopReg := newMockRegistry[seqEntry]()
|
||||||
|
chainReg := newMockRegistry[seqEntry]()
|
||||||
|
svcReg := newMockRegistry[seqEntry]()
|
||||||
|
|
||||||
|
seq++
|
||||||
|
registerGroup([]named[seqEntry]{{"leaf", seqEntry{"logger", seq}}}, leafReg)
|
||||||
|
|
||||||
|
// Hops should see leaf registrations.
|
||||||
|
if !leafReg.IsRegistered("leaf") {
|
||||||
|
t.Fatal("expected leaf to be registered before hops")
|
||||||
|
}
|
||||||
|
seq++
|
||||||
|
registerGroup([]named[seqEntry]{{"hop", seqEntry{"hop", seq}}}, hopReg)
|
||||||
|
|
||||||
|
// Chains should see hop registrations.
|
||||||
|
if !hopReg.IsRegistered("hop") {
|
||||||
|
t.Fatal("expected hop to be registered before chains")
|
||||||
|
}
|
||||||
|
seq++
|
||||||
|
registerGroup([]named[seqEntry]{{"chain", seqEntry{"chain", seq}}}, chainReg)
|
||||||
|
|
||||||
|
// Services should see chain registrations.
|
||||||
|
if !chainReg.IsRegistered("chain") {
|
||||||
|
t.Fatal("expected chain to be registered before services")
|
||||||
|
}
|
||||||
|
seq++
|
||||||
|
registerGroup([]named[seqEntry]{{"svc", seqEntry{"svc", seq}}}, svcReg)
|
||||||
|
|
||||||
|
// All should be registered.
|
||||||
|
for _, r := range []struct {
|
||||||
|
name string
|
||||||
|
reg *mockRegistry[seqEntry]
|
||||||
|
}{
|
||||||
|
{"leaf", leafReg},
|
||||||
|
{"hop", hopReg},
|
||||||
|
{"chain", chainReg},
|
||||||
|
{"svc", svcReg},
|
||||||
|
} {
|
||||||
|
if len(r.reg.GetAll()) != 1 {
|
||||||
|
t.Fatalf("expected 1 entry in %s registry", r.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify registration sequence: leaf < hop < chain < svc.
|
||||||
|
if leafReg.Get("leaf").seq >= hopReg.Get("hop").seq {
|
||||||
|
t.Fatal("leaf must be registered before hop")
|
||||||
|
}
|
||||||
|
if hopReg.Get("hop").seq >= chainReg.Get("chain").seq {
|
||||||
|
t.Fatal("hop must be registered before chain")
|
||||||
|
}
|
||||||
|
if chainReg.Get("chain").seq >= svcReg.Get("svc").seq {
|
||||||
|
t.Fatal("chain must be registered before service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Load tests ---
|
||||||
|
|
||||||
|
func TestLoad_NilConfig(t *testing.T) {
|
||||||
|
if err := Load(nil); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_EmptyConfig(t *testing.T) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
if err := Load(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_WithTLS(t *testing.T) {
|
||||||
|
// BuildDefaultTLSConfig with nil TLS falls back to default cert/key files
|
||||||
|
// which don't exist, but it then generates a self-signed cert, so it
|
||||||
|
// should still succeed.
|
||||||
|
cfg := &config.Config{
|
||||||
|
TLS: &config.TLSConfig{
|
||||||
|
Validity: 3600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := Load(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_WithLog(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Log: &config.LogConfig{
|
||||||
|
Level: "error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := Load(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_SetsDefaultLogger(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Log: &config.LogConfig{
|
||||||
|
Level: "warn",
|
||||||
|
Format: "json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := Load(cfg); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
// After Load, logger.Default() should be non-nil.
|
||||||
|
if logger.Default() == nil {
|
||||||
|
t.Fatal("expected default logger to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- defaultLoader singleton tests ---
|
||||||
|
|
||||||
|
func TestDefaultLoader_IsSet(t *testing.T) {
|
||||||
|
if defaultLoader == nil {
|
||||||
|
t.Fatal("defaultLoader should not be nil")
|
||||||
|
}
|
||||||
|
if err := defaultLoader.Load(nil); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- registerGroup edge cases ---
|
||||||
|
|
||||||
|
type countingRegistry[T any] struct {
|
||||||
|
m map[string]T
|
||||||
|
close int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCountingRegistry[T any]() *countingRegistry[T] {
|
||||||
|
return &countingRegistry[T]{m: make(map[string]T)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *countingRegistry[T]) Register(name string, v T) error { r.m[name] = v; return nil }
|
||||||
|
func (r *countingRegistry[T]) Unregister(name string) { delete(r.m, name); r.close++ }
|
||||||
|
func (r *countingRegistry[T]) IsRegistered(name string) bool { _, ok := r.m[name]; return ok }
|
||||||
|
func (r *countingRegistry[T]) Get(name string) T { return r.m[name] }
|
||||||
|
func (r *countingRegistry[T]) GetAll() map[string]T { return r.m }
|
||||||
|
|
||||||
|
func TestRegisterGroup_UnregistersAllOldEntries(t *testing.T) {
|
||||||
|
r := newCountingRegistry[int]()
|
||||||
|
r.Register("a", 1)
|
||||||
|
r.Register("b", 2)
|
||||||
|
r.Register("c", 3)
|
||||||
|
|
||||||
|
_ = registerGroup([]named[int]{{"x", 10}}, r)
|
||||||
|
|
||||||
|
if r.close != 3 {
|
||||||
|
t.Fatalf("expected 3 unregister calls, got %d", r.close)
|
||||||
|
}
|
||||||
|
if !r.IsRegistered("x") {
|
||||||
|
t.Fatal("expected 'x' to be registered")
|
||||||
|
}
|
||||||
|
if r.IsRegistered("a") || r.IsRegistered("b") || r.IsRegistered("c") {
|
||||||
|
t.Fatal("old entries should be unregistered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- register error paths ---
|
||||||
|
|
||||||
|
func TestRegister_ServiceBadTLS(t *testing.T) {
|
||||||
|
// A non-empty CertFile that doesn't exist causes
|
||||||
|
// tls_util.LoadServerConfig to fail before reaching the
|
||||||
|
// listener registry lookup.
|
||||||
|
//
|
||||||
|
// ParseService logs an error via the default logger when the
|
||||||
|
// TLS cert can't be loaded. We suppress that log here since
|
||||||
|
// the error is expected.
|
||||||
|
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
|
||||||
|
discardLogger := xlogger.NewLogger(xlogger.OutputOption(io.Discard))
|
||||||
|
t.Cleanup(func() { logger.SetDefault(discardLogger) })
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Services: []*config.ServiceConfig{
|
||||||
|
{
|
||||||
|
Name: "bad-tls",
|
||||||
|
Addr: ":0",
|
||||||
|
Listener: &config.ListenerConfig{
|
||||||
|
TLS: &config.TLSConfig{
|
||||||
|
CertFile: "/nonexistent/cert.pem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err == nil {
|
||||||
|
t.Fatal("expected error for bad TLS config, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister_ServiceUnknownListener(t *testing.T) {
|
||||||
|
// An unregistered listener type triggers the
|
||||||
|
// "unknown listener" error in ParseService.
|
||||||
|
cfg := &config.Config{
|
||||||
|
Services: []*config.ServiceConfig{
|
||||||
|
{
|
||||||
|
Name: "unknown-ln",
|
||||||
|
Addr: ":0",
|
||||||
|
Listener: &config.ListenerConfig{
|
||||||
|
Type: "nonexistent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := register(cfg); err == nil {
|
||||||
|
t.Fatal("expected error for unknown listener type, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-2
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ func (p *httpPlugin) Register(ctx context.Context, service *sd.Service, opts ...
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf(resp.Status)
|
return errors.New(resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -119,7 +120,7 @@ func (p *httpPlugin) Deregister(ctx context.Context, service *sd.Service) error
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf(resp.Status)
|
return errors.New(resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user