ba3e1cbe09
Services are the only component group that binds a port during parsing
(ParseService calls listener.Init). The generic registerGroup "parse-all-
then-swap" sequence bound every new listener while the old services were
still listening, causing EADDRINUSE on SIGHUP reload — a regression from
82e7e50.
Close and unregister all old services first (unregisterAll relies on
registry.Unregister closing io.Closer values, which frees each service's
port), then parse, bind, and register the new ones. This restores the
pre-82e7e50 close-old-before-bind ordering for the services group.
936 lines
24 KiB
Go
936 lines
24 KiB
Go
package loader
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"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")
|
|
}
|
|
}
|
|
|
|
// bindingStubListener is a stubListener whose Init actually binds a TCP port
|
|
// via net.Listen, reproducing the EADDRINUSE condition the close-old-before-
|
|
// bind ordering in register() must prevent. Used only by reload-collision tests.
|
|
type bindingStubListener struct {
|
|
addr string
|
|
ln net.Listener
|
|
}
|
|
|
|
func (l *bindingStubListener) Init(md metadata.Metadata) error {
|
|
ln, err := net.Listen("tcp", l.addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l.ln = ln
|
|
return nil
|
|
}
|
|
|
|
func (l *bindingStubListener) Accept() (net.Conn, error) {
|
|
if l.ln == nil {
|
|
return nil, net.ErrClosed
|
|
}
|
|
return l.ln.Accept()
|
|
}
|
|
|
|
func (l *bindingStubListener) Addr() net.Addr {
|
|
if l.ln != nil {
|
|
return l.ln.Addr()
|
|
}
|
|
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
|
|
}
|
|
|
|
func (l *bindingStubListener) Close() error {
|
|
if l.ln == nil {
|
|
return nil
|
|
}
|
|
return l.ln.Close()
|
|
}
|
|
|
|
// freeTCPPort returns the number of a TCP port that is free at call time by
|
|
// opening and immediately closing a listener on 127.0.0.1:0. There is a small
|
|
// race window before the caller rebinds, which is acceptable for tests.
|
|
func freeTCPPort(t *testing.T) int {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen for free port: %v", err)
|
|
}
|
|
addr := l.Addr().(*net.TCPAddr)
|
|
l.Close()
|
|
return addr.Port
|
|
}
|
|
|
|
// TestRegister_ServiceReloadNoCollision verifies that calling register() twice
|
|
// with a service on a fixed port does NOT return EADDRINUSE on the second call
|
|
// (issue #754). Before the close-old-before-bind ordering, the second
|
|
// register() bound the new listener while the old one was still listening.
|
|
func TestRegister_ServiceReloadNoCollision(t *testing.T) {
|
|
port := freeTCPPort(t)
|
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
|
|
|
// Register a binding listener factory under a test-specific name so we
|
|
// don't interfere with the "tcp" save/restore in TestRegister_Services.
|
|
const factoryName = "tcp-binding-test"
|
|
origListener := registry.ListenerRegistry().Get(factoryName)
|
|
registry.ListenerRegistry().Register(factoryName, func(opts ...listener.Option) listener.Listener {
|
|
options := listener.Options{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
return &bindingStubListener{addr: options.Addr}
|
|
})
|
|
// Reuse the inert "auto" handler stub.
|
|
origHandler := registry.HandlerRegistry().Get("auto")
|
|
registry.HandlerRegistry().Register("auto", func(opts ...handler.Option) handler.Handler {
|
|
return &stubHandler{}
|
|
})
|
|
t.Cleanup(func() {
|
|
registry.ListenerRegistry().Unregister(factoryName)
|
|
registry.HandlerRegistry().Unregister("auto")
|
|
if origListener != nil {
|
|
registry.ListenerRegistry().Register(factoryName, origListener)
|
|
}
|
|
if origHandler != nil {
|
|
registry.HandlerRegistry().Register("auto", origHandler)
|
|
}
|
|
})
|
|
|
|
r := registry.ServiceRegistry()
|
|
const svcName = "reload-svc"
|
|
t.Cleanup(func() { r.Unregister(svcName) })
|
|
|
|
buildCfg := func() *config.Config {
|
|
return &config.Config{
|
|
Services: []*config.ServiceConfig{
|
|
{
|
|
Name: svcName,
|
|
Addr: addr,
|
|
Listener: &config.ListenerConfig{Type: factoryName},
|
|
Handler: &config.HandlerConfig{Type: "auto"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// First load: registers the service and binds the port.
|
|
if err := register(buildCfg()); err != nil {
|
|
t.Fatalf("first register: unexpected error: %v", err)
|
|
}
|
|
if !r.IsRegistered(svcName) {
|
|
t.Fatal("expected service registered after first load")
|
|
}
|
|
|
|
// Second load simulates a SIGHUP reload. With the old ordering this bound
|
|
// the new listener while the old one was still open and returned
|
|
// "address already in use"; the fix closes old services first.
|
|
if err := register(buildCfg()); err != nil {
|
|
if strings.Contains(err.Error(), "address already in use") {
|
|
t.Fatalf("second register (reload): port collision not avoided: %v", err)
|
|
}
|
|
t.Fatalf("second register (reload): unexpected error: %v", err)
|
|
}
|
|
if !r.IsRegistered(svcName) {
|
|
t.Fatal("expected service registered after reload")
|
|
}
|
|
}
|