c59566d96f
112 tests covering Transport, Chain, Route, Router, packetConn wrapper, routePath, and all interface satisfaction checks.
1770 lines
53 KiB
Go
1770 lines
53 KiB
Go
package chain
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/chain"
|
|
"github.com/go-gost/core/connector"
|
|
"github.com/go-gost/core/dialer"
|
|
"github.com/go-gost/core/hop"
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/core/metadata"
|
|
"github.com/go-gost/core/recorder"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
xmetadata "github.com/go-gost/x/metadata"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- Mock types ---
|
|
|
|
// testConn is a minimal net.Conn for testing.
|
|
type testConn struct {
|
|
closed bool
|
|
}
|
|
|
|
func (c *testConn) Read(b []byte) (n int, err error) { return 0, nil }
|
|
func (c *testConn) Write(b []byte) (n int, err error) { return len(b), nil }
|
|
func (c *testConn) Close() error { c.closed = true; return nil }
|
|
func (c *testConn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345} }
|
|
func (c *testConn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080} }
|
|
func (c *testConn) SetDeadline(t time.Time) error { return nil }
|
|
func (c *testConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (c *testConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
// testDialer implements dialer.Dialer and optionally dialer.Handshaker and
|
|
// dialer.Multiplexer. Hook functions allow per-test customization.
|
|
type testDialer struct {
|
|
dialFn func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error)
|
|
handshakeFn func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error)
|
|
multiplexVal bool
|
|
hasMux bool // whether Multiplex() method exists (simulates optional interface)
|
|
}
|
|
|
|
func (d *testDialer) Init(md metadata.Metadata) error { return nil }
|
|
|
|
func (d *testDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
if d.dialFn != nil {
|
|
return d.dialFn(ctx, addr, opts...)
|
|
}
|
|
return &testConn{}, nil
|
|
}
|
|
|
|
func (d *testDialer) Handshake(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
if d.handshakeFn != nil {
|
|
return d.handshakeFn(ctx, conn, opts...)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func (d *testDialer) Multiplex() bool {
|
|
return d.hasMux && d.multiplexVal
|
|
}
|
|
|
|
// plainDialer implements only dialer.Dialer (no Handshaker, no Multiplexer).
|
|
type plainDialer struct{}
|
|
|
|
func (d *plainDialer) Init(md metadata.Metadata) error { return nil }
|
|
func (d *plainDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return &testConn{}, nil
|
|
}
|
|
|
|
// testConnector implements connector.Connector and optionally connector.Handshaker
|
|
// and connector.Binder.
|
|
type testConnector struct {
|
|
connectFn func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error)
|
|
handshakeFn func(ctx context.Context, conn net.Conn) (net.Conn, error)
|
|
bindFn func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error)
|
|
}
|
|
|
|
func (c *testConnector) Init(md metadata.Metadata) error { return nil }
|
|
|
|
func (c *testConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
if c.connectFn != nil {
|
|
return c.connectFn(ctx, conn, network, address, opts...)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func (c *testConnector) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
|
if c.handshakeFn != nil {
|
|
return c.handshakeFn(ctx, conn)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func (c *testConnector) Bind(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
if c.bindFn != nil {
|
|
return c.bindFn(ctx, conn, network, address, opts...)
|
|
}
|
|
return nil, connector.ErrBindUnsupported
|
|
}
|
|
|
|
// plainConnector implements only connector.Connector (no Handshaker, no Binder).
|
|
type plainConnector struct {
|
|
connectFn func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error)
|
|
}
|
|
|
|
func (c *plainConnector) Init(md metadata.Metadata) error { return nil }
|
|
func (c *plainConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
if c.connectFn != nil {
|
|
return c.connectFn(ctx, conn, network, address, opts...)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// testHop implements hop.Hop.
|
|
type testHop struct {
|
|
selectFn func(ctx context.Context, opts ...hop.SelectOption) *chain.Node
|
|
}
|
|
|
|
func (h *testHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
if h.selectFn != nil {
|
|
return h.selectFn(ctx, opts...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// trackedConn wraps a net.Conn and reports whether Close was called.
|
|
type trackedConn struct {
|
|
*testConn
|
|
closed bool
|
|
closeFn func() // optional, called on Close
|
|
}
|
|
|
|
func (c *trackedConn) Close() error {
|
|
if c.closed {
|
|
return nil
|
|
}
|
|
c.closed = true
|
|
if c.closeFn != nil {
|
|
c.closeFn()
|
|
}
|
|
return c.testConn.Close()
|
|
}
|
|
|
|
type testListener struct {
|
|
closed bool
|
|
}
|
|
|
|
func (l *testListener) Accept() (net.Conn, error) { return &testConn{}, nil }
|
|
func (l *testListener) Close() error { l.closed = true; return nil }
|
|
func (l *testListener) Addr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} }
|
|
|
|
// testRecorder implements recorder.Recorder.
|
|
type testRecorder struct {
|
|
records []recordCall
|
|
mu sync.Mutex
|
|
}
|
|
|
|
type recordCall struct {
|
|
data string
|
|
}
|
|
|
|
func (r *testRecorder) Record(ctx context.Context, b []byte, opts ...recorder.RecordOption) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.records = append(r.records, recordCall{data: string(b)})
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
logger.SetDefault(xlogger.Nop())
|
|
}
|
|
|
|
// =============================================================================
|
|
// Transport tests
|
|
// =============================================================================
|
|
|
|
func TestNewTransport(t *testing.T) {
|
|
d := &testDialer{}
|
|
c := &testConnector{}
|
|
tr := NewTransport(d, c)
|
|
require.NotNil(t, tr)
|
|
assert.Equal(t, d, tr.dialer)
|
|
assert.Equal(t, c, tr.connector)
|
|
}
|
|
|
|
func TestNewTransport_WithOptions(t *testing.T) {
|
|
rt := &chainRoute{}
|
|
d := &testDialer{}
|
|
c := &testConnector{}
|
|
|
|
tr := NewTransport(d, c,
|
|
chain.AddrTransportOption("example.com:8080"),
|
|
chain.InterfaceTransportOption("eth0"),
|
|
chain.NetnsTransportOption("ns1"),
|
|
chain.SockOptsTransportOption(&chain.SockOpts{Mark: 42}),
|
|
chain.RouteTransportOption(rt),
|
|
)
|
|
require.NotNil(t, tr)
|
|
assert.Equal(t, "example.com:8080", tr.options.Addr)
|
|
assert.Equal(t, "eth0", tr.options.IfceName)
|
|
assert.Equal(t, "ns1", tr.options.Netns)
|
|
assert.Equal(t, 42, tr.options.SockOpts.Mark)
|
|
assert.Equal(t, rt, tr.options.Route)
|
|
}
|
|
|
|
func TestTransport_Dial_Success(t *testing.T) {
|
|
var calledAddr string
|
|
d := &testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
calledAddr = addr
|
|
return &testConn{}, nil
|
|
},
|
|
}
|
|
tr := NewTransport(d, &testConnector{})
|
|
|
|
conn, err := tr.Dial(context.Background(), "192.168.1.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
assert.Equal(t, "192.168.1.1:80", calledAddr)
|
|
}
|
|
|
|
func TestTransport_Dial_Error(t *testing.T) {
|
|
dialErr := errors.New("dial failed")
|
|
d := &testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return nil, dialErr
|
|
},
|
|
}
|
|
tr := NewTransport(d, &testConnector{})
|
|
|
|
conn, err := tr.Dial(context.Background(), "192.168.1.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestTransport_Dial_WithHostOption(t *testing.T) {
|
|
tr := NewTransport(&testDialer{}, &testConnector{},
|
|
chain.AddrTransportOption("proxy.example.com:8080"),
|
|
)
|
|
|
|
var calledAddr string
|
|
// Wrap to check options are passed - we set dialFn after construction
|
|
tr.dialer = &testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
calledAddr = addr
|
|
return &testConn{}, nil
|
|
},
|
|
}
|
|
|
|
conn, err := tr.Dial(context.Background(), "1.2.3.4:443")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
// The addr parameter is what's passed through; Host option is inside opts
|
|
assert.Equal(t, "1.2.3.4:443", calledAddr)
|
|
}
|
|
|
|
func TestTransport_Dial_WithRouteNodes(t *testing.T) {
|
|
// When the transport's Route option is set and has nodes, Dial should use
|
|
// Route.Dial as the underlying dial function.
|
|
innerRoute := NewRoute()
|
|
innerNode := chain.NewNode("inner", "127.0.0.1:9090",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
innerRoute.addNode(innerNode)
|
|
|
|
tr := NewTransport(&testDialer{}, &testConnector{},
|
|
chain.RouteTransportOption(innerRoute),
|
|
)
|
|
|
|
conn, err := tr.Dial(context.Background(), "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
}
|
|
|
|
func TestTransport_Dial_RouteSet_NoNodes(t *testing.T) {
|
|
// Route is set but has zero nodes — should NOT use Route.Dial as the
|
|
// underlying dial function (short-circuit on len(Nodes()) > 0).
|
|
emptyRoute := NewRoute() // no nodes added
|
|
tr := NewTransport(&testDialer{}, &testConnector{},
|
|
chain.RouteTransportOption(emptyRoute),
|
|
)
|
|
|
|
conn, err := tr.Dial(context.Background(), "192.168.1.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
}
|
|
|
|
func TestTransport_Dial_WithSockOpts(t *testing.T) {
|
|
// Dial with SockOpts set — exercises the Mark assignment path.
|
|
tr := NewTransport(&testDialer{}, &testConnector{},
|
|
chain.SockOptsTransportOption(&chain.SockOpts{Mark: 999}),
|
|
)
|
|
|
|
conn, err := tr.Dial(context.Background(), "192.168.1.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
}
|
|
|
|
func TestTransport_Dial_DialFuncActive(t *testing.T) {
|
|
// Use a dialer that actually invokes the NetDialerDialOption, which in
|
|
// turn triggers the DialFunc on the net_dialer when Route has nodes.
|
|
d := &testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
var do dialer.DialOptions
|
|
for _, opt := range opts {
|
|
if opt != nil {
|
|
opt(&do)
|
|
}
|
|
}
|
|
if do.Dialer != nil {
|
|
return do.Dialer.Dial(ctx, "tcp", addr)
|
|
}
|
|
return &testConn{}, nil
|
|
},
|
|
}
|
|
|
|
innerRoute := NewRoute()
|
|
innerNode := chain.NewNode("inner", "127.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
innerRoute.addNode(innerNode)
|
|
|
|
tr := NewTransport(d, &testConnector{},
|
|
chain.RouteTransportOption(innerRoute),
|
|
)
|
|
|
|
conn, err := tr.Dial(context.Background(), "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
}
|
|
|
|
func TestTransport_Dial_NoSockOpts(t *testing.T) {
|
|
// Dial with no SockOpts set (nil path).
|
|
d := &testDialer{}
|
|
c := &testConnector{}
|
|
tr := NewTransport(d, c) // no SockOptsTransportOption
|
|
|
|
conn, err := tr.Dial(context.Background(), "192.168.1.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
}
|
|
|
|
func TestTransport_Connect_NoSockOpts(t *testing.T) {
|
|
// Connect with no SockOpts set (nil path).
|
|
c := &testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return conn, nil
|
|
},
|
|
}
|
|
tr := NewTransport(&testDialer{}, c) // no SockOptsTransportOption
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Connect(context.Background(), conn, "tcp", "10.0.0.1:80")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
}
|
|
|
|
func TestTransport_Handshake_DialerOnly(t *testing.T) {
|
|
d := &testDialer{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
return conn, nil
|
|
},
|
|
}
|
|
c := &plainConnector{}
|
|
tr := NewTransport(d, c, chain.AddrTransportOption("proxy:8080"))
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Handshake(context.Background(), conn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
}
|
|
|
|
func TestTransport_Handshake_ConnectorOnly(t *testing.T) {
|
|
d := &plainDialer{}
|
|
c := &testConnector{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
|
return conn, nil
|
|
},
|
|
}
|
|
tr := NewTransport(d, c)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Handshake(context.Background(), conn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
}
|
|
|
|
func TestTransport_Handshake_Both(t *testing.T) {
|
|
callOrder := make([]string, 0)
|
|
d := &testDialer{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
callOrder = append(callOrder, "dialer")
|
|
return conn, nil
|
|
},
|
|
}
|
|
c := &testConnector{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
|
callOrder = append(callOrder, "connector")
|
|
return conn, nil
|
|
},
|
|
}
|
|
tr := NewTransport(d, c)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Handshake(context.Background(), conn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
assert.Equal(t, []string{"dialer", "connector"}, callOrder)
|
|
}
|
|
|
|
func TestTransport_Handshake_Neither(t *testing.T) {
|
|
d := &plainDialer{}
|
|
c := &plainConnector{}
|
|
tr := NewTransport(d, c)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Handshake(context.Background(), conn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
}
|
|
|
|
func TestTransport_Handshake_DialerError(t *testing.T) {
|
|
handshakeErr := errors.New("handshake failed")
|
|
d := &testDialer{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
return nil, handshakeErr
|
|
},
|
|
}
|
|
tr := NewTransport(d, &testConnector{})
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Handshake(context.Background(), conn)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, result)
|
|
assert.Equal(t, handshakeErr, err)
|
|
}
|
|
|
|
func TestTransport_Handshake_ConnectorError(t *testing.T) {
|
|
handshakeErr := errors.New("connector handshake failed")
|
|
d := &testDialer{} // dialer handshake succeeds first
|
|
c := &testConnector{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
|
return nil, handshakeErr
|
|
},
|
|
}
|
|
tr := NewTransport(d, c)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Handshake(context.Background(), conn)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestTransport_Connect_Success(t *testing.T) {
|
|
c := &testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return conn, nil
|
|
},
|
|
}
|
|
tr := NewTransport(&testDialer{}, c)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Connect(context.Background(), conn, "tcp", "10.0.0.1:80")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
}
|
|
|
|
func TestTransport_Connect_Error(t *testing.T) {
|
|
connectErr := errors.New("connect failed")
|
|
c := &testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return nil, connectErr
|
|
},
|
|
}
|
|
tr := NewTransport(&testDialer{}, c)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Connect(context.Background(), conn, "tcp", "10.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestTransport_Connect_WithSockOpts(t *testing.T) {
|
|
c := &testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return conn, nil
|
|
},
|
|
}
|
|
tr := NewTransport(&testDialer{}, c,
|
|
chain.SockOptsTransportOption(&chain.SockOpts{Mark: 123}),
|
|
)
|
|
|
|
conn := &testConn{}
|
|
result, err := tr.Connect(context.Background(), conn, "tcp", "10.0.0.1:80")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, conn, result)
|
|
}
|
|
|
|
func TestTransport_Bind_Binder(t *testing.T) {
|
|
expectedLn := &testListener{}
|
|
c := &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return expectedLn, nil
|
|
},
|
|
}
|
|
tr := NewTransport(&testDialer{}, c)
|
|
|
|
conn := &testConn{}
|
|
ln, err := tr.Bind(context.Background(), conn, "tcp", ":0")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedLn, ln)
|
|
}
|
|
|
|
func TestTransport_Bind_BindError(t *testing.T) {
|
|
bindErr := errors.New("bind failed")
|
|
c := &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return nil, bindErr
|
|
},
|
|
}
|
|
tr := NewTransport(&testDialer{}, c)
|
|
|
|
conn := &testConn{}
|
|
ln, err := tr.Bind(context.Background(), conn, "tcp", ":0")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ln)
|
|
}
|
|
|
|
func TestTransport_Bind_NotBinder(t *testing.T) {
|
|
c := &plainConnector{}
|
|
tr := NewTransport(&testDialer{}, c)
|
|
|
|
conn := &testConn{}
|
|
ln, err := tr.Bind(context.Background(), conn, "tcp", ":0")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ln)
|
|
assert.Equal(t, connector.ErrBindUnsupported, err)
|
|
}
|
|
|
|
func TestTransport_Multiplex_True(t *testing.T) {
|
|
d := &testDialer{hasMux: true, multiplexVal: true}
|
|
tr := NewTransport(d, &testConnector{})
|
|
assert.True(t, tr.Multiplex())
|
|
}
|
|
|
|
func TestTransport_Multiplex_False(t *testing.T) {
|
|
d := &testDialer{hasMux: true, multiplexVal: false}
|
|
tr := NewTransport(d, &testConnector{})
|
|
assert.False(t, tr.Multiplex())
|
|
}
|
|
|
|
func TestTransport_Multiplex_NotImplemented(t *testing.T) {
|
|
d := &plainDialer{}
|
|
tr := NewTransport(d, &testConnector{})
|
|
assert.False(t, tr.Multiplex())
|
|
}
|
|
|
|
func TestTransport_Options(t *testing.T) {
|
|
tr := NewTransport(&testDialer{}, &testConnector{},
|
|
chain.AddrTransportOption("test:1234"),
|
|
chain.InterfaceTransportOption("eth1"),
|
|
)
|
|
opts := tr.Options()
|
|
require.NotNil(t, opts)
|
|
assert.Equal(t, "test:1234", opts.Addr)
|
|
assert.Equal(t, "eth1", opts.IfceName)
|
|
}
|
|
|
|
func TestTransport_Options_NilReceiver(t *testing.T) {
|
|
var tr *Transport
|
|
opts := tr.Options()
|
|
assert.Nil(t, opts)
|
|
}
|
|
|
|
func TestTransport_Copy_Isolation(t *testing.T) {
|
|
original := NewTransport(&testDialer{}, &testConnector{},
|
|
chain.AddrTransportOption("original:8080"),
|
|
)
|
|
|
|
cp := original.Copy()
|
|
require.NotNil(t, cp)
|
|
|
|
// The copy should not be the same pointer.
|
|
assert.NotSame(t, original, cp)
|
|
|
|
// Modifying the copy's options must not affect the original.
|
|
cp.Options().Addr = "modified:9090"
|
|
assert.Equal(t, "original:8080", original.Options().Addr)
|
|
assert.Equal(t, "modified:9090", cp.Options().Addr)
|
|
}
|
|
|
|
func TestTransport_Copy_SupportsEmbeddingSubRoute(t *testing.T) {
|
|
// Verify that Copy produces a transporter whose Options() can independently
|
|
// hold a sub-route (as used by Chain.Route for multiplex splitting).
|
|
original := NewTransport(&testDialer{}, &testConnector{})
|
|
cp := original.Copy().(*Transport)
|
|
require.NotNil(t, cp)
|
|
|
|
subRoute := NewRoute()
|
|
cp.Options().Route = subRoute
|
|
|
|
assert.Nil(t, original.Options().Route)
|
|
assert.Equal(t, subRoute, cp.Options().Route)
|
|
}
|
|
|
|
func TestTransport_Copy_ChainTransporterInterface(t *testing.T) {
|
|
// Verify Copy satisfies chain.Transporter interface.
|
|
var tr chain.Transporter = NewTransport(&testDialer{}, &testConnector{})
|
|
cp := tr.Copy()
|
|
require.NotNil(t, cp)
|
|
assert.IsType(t, &Transport{}, cp)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Chain tests
|
|
// =============================================================================
|
|
|
|
func TestNewChain(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
require.NotNil(t, c)
|
|
assert.Equal(t, "test-chain", c.Name())
|
|
assert.NotNil(t, c.Marker())
|
|
}
|
|
|
|
func TestNewChain_WithMetadata(t *testing.T) {
|
|
md := xmetadata.NewMetadata(map[string]any{"key": "value"})
|
|
c := NewChain("test-chain", MetadataChainOption(md))
|
|
assert.Equal(t, md, c.Metadata())
|
|
}
|
|
|
|
func TestNewChain_WithLogger(t *testing.T) {
|
|
l := xlogger.Nop()
|
|
c := NewChain("test-chain", LoggerChainOption(l))
|
|
assert.Equal(t, l, c.logger)
|
|
}
|
|
|
|
func TestChain_AddHop(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
h := &testHop{}
|
|
c.AddHop(h)
|
|
assert.Len(t, c.hops, 1)
|
|
}
|
|
|
|
func TestChain_Metadata(t *testing.T) {
|
|
md := xmetadata.NewMetadata(map[string]any{"a": 1})
|
|
c := NewChain("test-chain", MetadataChainOption(md))
|
|
assert.Equal(t, md, c.Metadata())
|
|
}
|
|
|
|
func TestChain_Marker(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
m := c.Marker()
|
|
require.NotNil(t, m)
|
|
m.Mark()
|
|
assert.Equal(t, int64(1), m.Count())
|
|
}
|
|
|
|
func TestChain_Name(t *testing.T) {
|
|
c := NewChain("my-chain")
|
|
assert.Equal(t, "my-chain", c.Name())
|
|
}
|
|
|
|
func TestChain_Route_NilChain(t *testing.T) {
|
|
var c *Chain
|
|
rt := c.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Nil(t, rt)
|
|
}
|
|
|
|
func TestChain_Route_EmptyHops(t *testing.T) {
|
|
c := NewChain("empty")
|
|
rt := c.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Nil(t, rt)
|
|
}
|
|
|
|
func TestChain_Route_SingleHop(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
|
|
return node
|
|
},
|
|
}
|
|
c.AddHop(h)
|
|
|
|
rt := c.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NotNil(t, rt)
|
|
assert.Len(t, rt.Nodes(), 1)
|
|
assert.Equal(t, "node1", rt.Nodes()[0].Name)
|
|
}
|
|
|
|
func TestChain_Route_MultipleHops(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
node1 := chain.NewNode("hop1", "10.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
node2 := chain.NewNode("hop2", "10.0.0.2:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
|
|
h1 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node1 }}
|
|
h2 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node2 }}
|
|
c.AddHop(h1)
|
|
c.AddHop(h2)
|
|
|
|
rt := c.Route(context.Background(), "tcp", "192.168.1.1:80")
|
|
require.NotNil(t, rt)
|
|
assert.Len(t, rt.Nodes(), 2)
|
|
assert.Equal(t, "hop1", rt.Nodes()[0].Name)
|
|
assert.Equal(t, "hop2", rt.Nodes()[1].Name)
|
|
}
|
|
|
|
func TestChain_Route_MultiplexSplit(t *testing.T) {
|
|
// When a node supports multiplexing, the route is split: nodes before the
|
|
// multiplex node become a sub-route embedded in that node's transport, and
|
|
// the returned route starts fresh after the multiplex node.
|
|
c := NewChain("test-chain")
|
|
|
|
node1 := chain.NewNode("hop1", "10.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
|
|
muxTr := NewTransport(&testDialer{hasMux: true, multiplexVal: true}, &testConnector{})
|
|
node2 := chain.NewNode("hop2", "10.0.0.2:8080",
|
|
chain.TransportNodeOption(muxTr))
|
|
|
|
node3 := chain.NewNode("hop3", "10.0.0.3:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
|
|
h1 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node1 }}
|
|
h2 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node2 }}
|
|
h3 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node3 }}
|
|
c.AddHop(h1)
|
|
c.AddHop(h2)
|
|
c.AddHop(h3)
|
|
|
|
rt := c.Route(context.Background(), "tcp", "192.168.1.1:80")
|
|
require.NotNil(t, rt)
|
|
|
|
// hop1 is in the sub-route embedded in hop2's transport
|
|
// hop2 starts the new route (it's the multiplex node), hop3 follows
|
|
nodes := rt.Nodes()
|
|
require.Len(t, nodes, 2)
|
|
assert.Equal(t, "hop2", nodes[0].Name)
|
|
assert.Equal(t, "hop3", nodes[1].Name)
|
|
|
|
// Verify hop2's transport has the sub-route with hop1
|
|
subRoute := nodes[0].Options().Transport.Options().Route
|
|
require.NotNil(t, subRoute)
|
|
assert.Len(t, subRoute.Nodes(), 1)
|
|
assert.Equal(t, "hop1", subRoute.Nodes()[0].Name)
|
|
}
|
|
|
|
func TestChain_Route_NilNodeFromHop(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return nil },
|
|
}
|
|
c.AddHop(h)
|
|
|
|
rt := c.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NotNil(t, rt)
|
|
// Empty route because the hop returned nil (no suitable node)
|
|
assert.Len(t, rt.Nodes(), 0)
|
|
}
|
|
|
|
func TestChain_Route_NilNodeInMiddle(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
node1 := chain.NewNode("hop1", "10.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
|
|
h1 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node1 }}
|
|
h2 := &testHop{selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return nil }}
|
|
c.AddHop(h1)
|
|
c.AddHop(h2)
|
|
|
|
rt := c.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NotNil(t, rt)
|
|
// Only hop1 is present; route stops at the nil node
|
|
assert.Len(t, rt.Nodes(), 1)
|
|
}
|
|
|
|
func TestNewChainGroup(t *testing.T) {
|
|
ch1 := NewChain("ch1")
|
|
ch2 := NewChain("ch2")
|
|
g := NewChainGroup(ch1, ch2)
|
|
require.NotNil(t, g)
|
|
assert.Len(t, g.chains, 2)
|
|
}
|
|
|
|
func TestChainGroup_Route_NoChains(t *testing.T) {
|
|
g := NewChainGroup()
|
|
rt := g.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Nil(t, rt)
|
|
}
|
|
|
|
func TestChainGroup_Route_WithEmptyChain(t *testing.T) {
|
|
// Chain group with empty chain (no hops) returns nil route.
|
|
ch1 := NewChain("ch1")
|
|
g := NewChainGroup(ch1)
|
|
sel := &testSelector{chains: []chain.Chainer{ch1}}
|
|
g.WithSelector(sel)
|
|
|
|
rt := g.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Nil(t, rt)
|
|
}
|
|
|
|
func TestChainGroup_Route_NilReceiver(t *testing.T) {
|
|
var g *chainGroup
|
|
rt := g.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Nil(t, rt)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Route tests
|
|
// =============================================================================
|
|
|
|
func TestNewRoute(t *testing.T) {
|
|
rt := NewRoute()
|
|
require.NotNil(t, rt)
|
|
assert.Len(t, rt.Nodes(), 0)
|
|
}
|
|
|
|
func TestNewRoute_WithChainOption(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
rt := NewRoute(ChainRouteOption(c))
|
|
require.NotNil(t, rt)
|
|
assert.Equal(t, c, rt.options.Chain)
|
|
}
|
|
|
|
func TestChainRoute_AddNode(t *testing.T) {
|
|
rt := NewRoute()
|
|
node := chain.NewNode("n1", "127.0.0.1:8080")
|
|
rt.addNode(node)
|
|
assert.Len(t, rt.Nodes(), 1)
|
|
rt.addNode(chain.NewNode("n2", "127.0.0.1:9090"))
|
|
assert.Len(t, rt.Nodes(), 2)
|
|
}
|
|
|
|
func TestChainRoute_Nodes(t *testing.T) {
|
|
rt := NewRoute()
|
|
node1 := chain.NewNode("n1", "127.0.0.1:8080")
|
|
node2 := chain.NewNode("n2", "127.0.0.1:9090")
|
|
rt.addNode(node1, node2)
|
|
|
|
nodes := rt.Nodes()
|
|
assert.Len(t, nodes, 2)
|
|
assert.Equal(t, "n1", nodes[0].Name)
|
|
assert.Equal(t, "n2", nodes[1].Name)
|
|
}
|
|
|
|
func TestChainRoute_Nodes_NilReceiver(t *testing.T) {
|
|
var rt *chainRoute
|
|
assert.Nil(t, rt.Nodes())
|
|
}
|
|
|
|
func TestChainRoute_GetNode_Valid(t *testing.T) {
|
|
rt := NewRoute()
|
|
node := chain.NewNode("n1", "127.0.0.1:8080")
|
|
rt.addNode(node)
|
|
|
|
assert.Equal(t, node, rt.getNode(0))
|
|
}
|
|
|
|
func TestChainRoute_GetNode_OutOfBounds(t *testing.T) {
|
|
rt := NewRoute()
|
|
rt.addNode(chain.NewNode("n1", "127.0.0.1:8080"))
|
|
|
|
assert.Nil(t, rt.getNode(-1))
|
|
assert.Nil(t, rt.getNode(1))
|
|
}
|
|
|
|
func TestChainRoute_GetNode_EmptyRoute(t *testing.T) {
|
|
rt := NewRoute()
|
|
assert.Nil(t, rt.getNode(0))
|
|
}
|
|
|
|
func TestChainRoute_GetNode_NilReceiver(t *testing.T) {
|
|
var rt *chainRoute
|
|
assert.Nil(t, rt.getNode(0))
|
|
}
|
|
|
|
func TestChainRoute_Dial_EmptyRoute(t *testing.T) {
|
|
// Empty route falls through to DefaultRoute. The DefaultRoute path
|
|
// requires real network I/O; test it through Router integration tests.
|
|
rt := NewRoute()
|
|
assert.Len(t, rt.Nodes(), 0)
|
|
}
|
|
|
|
func TestChainRoute_Dial_SingleNode(t *testing.T) {
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
conn.Close()
|
|
}
|
|
|
|
func TestChainRoute_Dial_MultipleNodes(t *testing.T) {
|
|
// Two-hop chain: node1 (entry proxy) → node2 (exit proxy) → destination
|
|
tr1 := NewTransport(&testDialer{}, &testConnector{})
|
|
tr2 := NewTransport(&testDialer{}, &testConnector{})
|
|
|
|
node1 := chain.NewNode("entry", "127.0.0.1:8080", chain.TransportNodeOption(tr1))
|
|
node2 := chain.NewNode("exit", "127.0.0.1:9090", chain.TransportNodeOption(tr2))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(node1, node2)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "192.168.1.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
conn.Close()
|
|
}
|
|
|
|
func TestChainRoute_Dial_DialError(t *testing.T) {
|
|
dialErr := errors.New("cannot dial")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return nil, dialErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestChainRoute_Dial_HandshakeError(t *testing.T) {
|
|
handshakeErr := errors.New("handshake error")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
return nil, handshakeErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestChainRoute_Dial_ConnectError(t *testing.T) {
|
|
connectErr := errors.New("connect failed")
|
|
tr := NewTransport(
|
|
&testDialer{},
|
|
&testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return nil, connectErr
|
|
},
|
|
},
|
|
)
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestChainRoute_Dial_MultiHopConnectError(t *testing.T) {
|
|
// First hop succeeds, second hop's Connect fails.
|
|
connectErr := errors.New("intermediate connect failed")
|
|
tr1 := NewTransport(&testDialer{}, &testConnector{})
|
|
tr2 := NewTransport(
|
|
&testDialer{},
|
|
&testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return nil, connectErr
|
|
},
|
|
},
|
|
)
|
|
|
|
node1 := chain.NewNode("entry", "127.0.0.1:8080", chain.TransportNodeOption(tr1))
|
|
node2 := chain.NewNode("exit", "127.0.0.1:9090", chain.TransportNodeOption(tr2))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(node1, node2)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "192.168.1.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestChainRoute_Dial_MultiHopHandshakeError(t *testing.T) {
|
|
// First hop succeeds, second hop's Handshake fails.
|
|
handshakeErr := errors.New("handshake failed")
|
|
tr1 := NewTransport(&testDialer{}, &testConnector{})
|
|
tr2 := NewTransport(
|
|
&testDialer{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
return nil, handshakeErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
|
|
node1 := chain.NewNode("entry", "127.0.0.1:8080", chain.TransportNodeOption(tr1))
|
|
node2 := chain.NewNode("exit", "127.0.0.1:9090", chain.TransportNodeOption(tr2))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(node1, node2)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "192.168.1.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestChainRoute_Dial_ConnectionCleanupOnError(t *testing.T) {
|
|
// Verify that on intermediate node failure, previous connections are closed.
|
|
conn1 := &trackedConn{testConn: &testConn{}}
|
|
|
|
tr1 := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return conn1, nil
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
|
|
// Second transport fails on Connect, which should trigger conn1.Close()
|
|
tr2 := NewTransport(
|
|
&testDialer{},
|
|
&testConnector{
|
|
connectFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) {
|
|
return nil, errors.New("fail")
|
|
},
|
|
},
|
|
)
|
|
|
|
node1 := chain.NewNode("entry", "127.0.0.1:8080", chain.TransportNodeOption(tr1))
|
|
node2 := chain.NewNode("exit", "127.0.0.1:9090", chain.TransportNodeOption(tr2))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(node1, node2)
|
|
|
|
_, err := rt.Dial(context.Background(), "tcp", "192.168.1.1:80")
|
|
assert.Error(t, err)
|
|
assert.True(t, conn1.closed, "first hop connection should be closed on second hop failure")
|
|
}
|
|
|
|
func TestChainRoute_Bind_EmptyRoute(t *testing.T) {
|
|
rt := NewRoute()
|
|
// Empty route falls to DefaultRoute.Bind — TCP bind on port 0 should succeed.
|
|
ln, err := rt.Bind(context.Background(), "tcp", ":0")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ln)
|
|
ln.Close()
|
|
}
|
|
|
|
func TestChainRoute_Bind_SingleNode(t *testing.T) {
|
|
tr := NewTransport(&testDialer{}, &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return &testListener{}, nil
|
|
},
|
|
})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
ln, err := rt.Bind(context.Background(), "tcp", ":0")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ln)
|
|
ln.Close()
|
|
}
|
|
|
|
func TestChainRoute_Bind_BindError(t *testing.T) {
|
|
bindErr := errors.New("bind not available")
|
|
tr := NewTransport(&testDialer{}, &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return nil, bindErr
|
|
},
|
|
})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
ln, err := rt.Bind(context.Background(), "tcp", ":0")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ln)
|
|
}
|
|
|
|
func TestChainRoute_Bind_ConnectError(t *testing.T) {
|
|
// Dial succeeds but Bind on the last node fails — connection should be cleaned up.
|
|
dialErr := errors.New("dial failed")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return nil, dialErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
ln, err := rt.Bind(context.Background(), "tcp", ":0")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ln)
|
|
}
|
|
|
|
func TestChainRoute_Bind_WithBindOptions(t *testing.T) {
|
|
// Verify Bind passes through BindOptions correctly.
|
|
tr := NewTransport(&testDialer{}, &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return &testListener{}, nil
|
|
},
|
|
})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
ln, err := rt.Bind(context.Background(), "tcp", ":0",
|
|
chain.MuxBindOption(true),
|
|
chain.BacklogBindOption(128),
|
|
chain.UDPConnTTLBindOption(30*time.Second),
|
|
chain.UDPDataBufferSizeBindOption(4096),
|
|
chain.UDPDataQueueSizeBindOption(64),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ln)
|
|
ln.Close()
|
|
}
|
|
|
|
func TestChainRoute_Dial_WithDialOptions(t *testing.T) {
|
|
// Verify Dial passes through DialOptions correctly.
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute()
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80",
|
|
chain.InterfaceDialOption("eth0"),
|
|
chain.NetnsDialOption("ns1"),
|
|
chain.SockOptsDialOption(&chain.SockOpts{Mark: 99}),
|
|
chain.LoggerDialOption(xlogger.Nop()),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
conn.Close()
|
|
}
|
|
|
|
func TestChainRoute_Connect_MarkerOnError(t *testing.T) {
|
|
// When connect fails and the route has a parent Chain, the node and chain
|
|
// markers should be updated.
|
|
c := NewChain("test-chain")
|
|
dialErr := errors.New("dial failed")
|
|
var dialCalled bool
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
dialCalled = true
|
|
// Return error after first call to test marker behavior
|
|
if addr == "127.0.0.1:8080" {
|
|
return nil, dialErr
|
|
}
|
|
return &testConn{}, nil
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute(ChainRouteOption(c))
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
assert.True(t, dialCalled)
|
|
|
|
// Chain marker should reflect the error
|
|
assert.Greater(t, c.Marker().Count(), int64(0))
|
|
}
|
|
|
|
func TestChainRoute_Connect_SuccessResetsChainMarker(t *testing.T) {
|
|
// On success, the chain marker should be reset.
|
|
c := NewChain("test-chain")
|
|
// Pre-mark the chain
|
|
c.Marker().Mark()
|
|
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("node1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute(ChainRouteOption(c))
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
conn.Close()
|
|
|
|
// Chain marker should be reset to 0 on success
|
|
assert.Equal(t, int64(0), c.Marker().Count())
|
|
}
|
|
|
|
func TestDefaultRoute_Nodes(t *testing.T) {
|
|
assert.Nil(t, DefaultRoute.Nodes())
|
|
}
|
|
|
|
func TestDefaultRoute_ImplementsRoute(t *testing.T) {
|
|
var r chain.Route = DefaultRoute
|
|
assert.NotNil(t, r)
|
|
}
|
|
|
|
func TestRoutePath(t *testing.T) {
|
|
// Simple route with no sub-routes.
|
|
node1 := chain.NewNode("n1", "127.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
node2 := chain.NewNode("n2", "127.0.0.1:9090",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(node1, node2)
|
|
|
|
path := routePath(rt)
|
|
require.Len(t, path, 2)
|
|
assert.Equal(t, "n1", path[0].Name)
|
|
assert.Equal(t, "n2", path[1].Name)
|
|
}
|
|
|
|
func TestRoutePath_NilRoute(t *testing.T) {
|
|
assert.Nil(t, routePath(nil))
|
|
}
|
|
|
|
func TestRoutePath_NestedRoutes(t *testing.T) {
|
|
// Simulate what Chain.Route does for multiplex splitting: the sub-route
|
|
// is embedded in the transport options of the multiplex node.
|
|
innerNode := chain.NewNode("inner", "10.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
innerRoute := NewRoute()
|
|
innerRoute.addNode(innerNode)
|
|
|
|
muxTr := NewTransport(&testDialer{}, &testConnector{})
|
|
muxTr.Options().Route = innerRoute
|
|
muxNode := chain.NewNode("mux", "10.0.0.2:8080", chain.TransportNodeOption(muxTr))
|
|
|
|
outerNode := chain.NewNode("outer", "10.0.0.3:9090",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(muxNode, outerNode)
|
|
|
|
path := routePath(rt)
|
|
require.Len(t, path, 3)
|
|
assert.Equal(t, "inner", path[0].Name)
|
|
assert.Equal(t, "mux", path[1].Name)
|
|
assert.Equal(t, "outer", path[2].Name)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Router tests
|
|
// =============================================================================
|
|
|
|
func TestNewRouter_Defaults(t *testing.T) {
|
|
r := NewRouter()
|
|
require.NotNil(t, r)
|
|
assert.Equal(t, 15*time.Second, r.options.Timeout)
|
|
assert.NotNil(t, r.options.Logger)
|
|
}
|
|
|
|
func TestNewRouter_WithOptions(t *testing.T) {
|
|
testChain := NewChain("test-chain")
|
|
l := xlogger.Nop()
|
|
|
|
r := NewRouter(
|
|
chain.TimeoutRouterOption(30*time.Second),
|
|
chain.RetriesRouterOption(3),
|
|
chain.InterfaceRouterOption("eth0"),
|
|
chain.NetnsRouterOption("ns1"),
|
|
chain.SockOptsRouterOption(&chain.SockOpts{Mark: 99}),
|
|
chain.ChainRouterOption(testChain),
|
|
chain.LoggerRouterOption(l),
|
|
)
|
|
require.NotNil(t, r)
|
|
assert.Equal(t, 30*time.Second, r.options.Timeout)
|
|
assert.Equal(t, 3, r.options.Retries)
|
|
assert.Equal(t, "eth0", r.options.IfceName)
|
|
assert.Equal(t, "ns1", r.options.Netns)
|
|
assert.Equal(t, 99, r.options.SockOpts.Mark)
|
|
assert.Equal(t, testChain, r.options.Chain)
|
|
assert.Equal(t, l, r.options.Logger)
|
|
}
|
|
|
|
func TestNewRouter_ZeroTimeoutDefault(t *testing.T) {
|
|
// Timeout of 0 is replaced with 15s default.
|
|
r := NewRouter(chain.TimeoutRouterOption(0))
|
|
assert.Equal(t, 15*time.Second, r.options.Timeout)
|
|
}
|
|
|
|
func TestRouter_Options(t *testing.T) {
|
|
r := NewRouter(chain.TimeoutRouterOption(5 * time.Second))
|
|
opts := r.Options()
|
|
require.NotNil(t, opts)
|
|
assert.Equal(t, 5*time.Second, opts.Timeout)
|
|
}
|
|
|
|
func TestRouter_Options_NilReceiver(t *testing.T) {
|
|
var r *Router
|
|
assert.Nil(t, r.Options())
|
|
}
|
|
|
|
func TestRouter_Record_WithMatchingRecorder(t *testing.T) {
|
|
rec := &testRecorder{}
|
|
r := NewRouter(
|
|
chain.RecordersRouterOption(recorder.RecorderObject{
|
|
Recorder: rec,
|
|
Record: recorder.RecorderServiceRouterDialAddress,
|
|
}),
|
|
)
|
|
err := r.record(context.Background(), recorder.RecorderServiceRouterDialAddress, []byte("test-addr"))
|
|
assert.NoError(t, err)
|
|
assert.Len(t, rec.records, 1)
|
|
assert.Equal(t, "test-addr", rec.records[0].data)
|
|
}
|
|
|
|
func TestRouter_Record_NoMatch(t *testing.T) {
|
|
rec := &testRecorder{}
|
|
r := NewRouter(
|
|
chain.RecordersRouterOption(recorder.RecorderObject{
|
|
Recorder: rec,
|
|
Record: "other.record.name",
|
|
}),
|
|
)
|
|
err := r.record(context.Background(), recorder.RecorderServiceRouterDialAddress, []byte("test-addr"))
|
|
assert.NoError(t, err)
|
|
assert.Len(t, rec.records, 0)
|
|
}
|
|
|
|
func TestRouter_Record_EmptyData(t *testing.T) {
|
|
rec := &testRecorder{}
|
|
r := NewRouter(
|
|
chain.RecordersRouterOption(recorder.RecorderObject{
|
|
Recorder: rec,
|
|
Record: recorder.RecorderServiceRouterDialAddress,
|
|
}),
|
|
)
|
|
err := r.record(context.Background(), recorder.RecorderServiceRouterDialAddress, nil)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, rec.records, 0)
|
|
}
|
|
|
|
func TestRouter_Dial_RecordsAddress(t *testing.T) {
|
|
rec := &testRecorder{}
|
|
testChain := NewChain("test-chain")
|
|
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
testChain.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(testChain),
|
|
chain.RecordersRouterOption(recorder.RecorderObject{
|
|
Recorder: rec,
|
|
Record: recorder.RecorderServiceRouterDialAddress,
|
|
}),
|
|
)
|
|
|
|
conn, err := r.Dial(context.Background(), "tcp", "192.168.1.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
conn.Close()
|
|
|
|
assert.Len(t, rec.records, 1)
|
|
assert.Equal(t, "192.168.1.1", rec.records[0].data)
|
|
}
|
|
|
|
func TestRouter_Dial_ErrorRecordsAddress(t *testing.T) {
|
|
// Verify that Dial records the address before the dial attempt, regardless
|
|
// of whether the dial succeeds or fails.
|
|
rec := &testRecorder{}
|
|
dialErr := errors.New("dial failed")
|
|
testChain := NewChain("test-chain")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return nil, dialErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
testChain.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(testChain),
|
|
chain.RetriesRouterOption(0),
|
|
chain.RecordersRouterOption(
|
|
recorder.RecorderObject{
|
|
Recorder: rec,
|
|
Record: recorder.RecorderServiceRouterDialAddress,
|
|
},
|
|
recorder.RecorderObject{
|
|
Recorder: rec,
|
|
Record: recorder.RecorderServiceRouterDialAddressError,
|
|
},
|
|
),
|
|
)
|
|
|
|
_, err := r.Dial(context.Background(), "tcp", "192.168.1.1:80")
|
|
assert.Error(t, err)
|
|
// Dial always records the address first (before dial attempt)
|
|
assert.Equal(t, "192.168.1.1", rec.records[0].data)
|
|
}
|
|
|
|
func TestRouter_Dial_UDPWrapsAsPacketConn(t *testing.T) {
|
|
// When network is "udp", the returned connection should implement net.PacketConn.
|
|
testChain := NewChain("test-chain")
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
testChain.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(testChain),
|
|
)
|
|
|
|
// The transport returns a testConn which does NOT implement net.PacketConn,
|
|
// so Router.Dial should wrap it in a packetConn.
|
|
conn, err := r.Dial(context.Background(), "udp", "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
|
|
_, ok := conn.(net.PacketConn)
|
|
assert.True(t, ok, "UDP dial should return a net.PacketConn")
|
|
conn.Close()
|
|
}
|
|
|
|
func TestRouter_Bind_EmptyChain(t *testing.T) {
|
|
// Chain with no hops → empty route → ErrEmptyRoute
|
|
c := NewChain("empty")
|
|
r := NewRouter(chain.ChainRouterOption(c), chain.RetriesRouterOption(0))
|
|
|
|
ln, err := r.Bind(context.Background(), "tcp", ":0")
|
|
assert.ErrorIs(t, err, ErrEmptyRoute)
|
|
assert.Nil(t, ln)
|
|
}
|
|
|
|
func TestRouter_Bind_NoChain(t *testing.T) {
|
|
// No chain → falls to DefaultRoute.Bind
|
|
r := NewRouter(chain.RetriesRouterOption(0))
|
|
|
|
ln, err := r.Bind(context.Background(), "tcp", ":0")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ln)
|
|
ln.Close()
|
|
}
|
|
|
|
func TestRouter_Bind_WithChain(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
tr := NewTransport(&testDialer{}, &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return &testListener{}, nil
|
|
},
|
|
})
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
c.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(c),
|
|
chain.RetriesRouterOption(0),
|
|
)
|
|
|
|
ln, err := r.Bind(context.Background(), "tcp", ":0")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ln)
|
|
ln.Close()
|
|
}
|
|
|
|
func TestRouter_Bind_Retries(t *testing.T) {
|
|
// First attempt fails, second succeeds.
|
|
attempts := 0
|
|
c := NewChain("test-chain")
|
|
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
attempts++
|
|
if attempts == 1 {
|
|
return nil, errors.New("transient error")
|
|
}
|
|
return &testConn{}, nil
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
c.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(c),
|
|
chain.RetriesRouterOption(2),
|
|
)
|
|
|
|
conn, err := r.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
conn.Close()
|
|
|
|
assert.Equal(t, 2, attempts, "should have retried once and succeeded on second attempt")
|
|
}
|
|
|
|
func TestRouter_Dial_NegativeRetries(t *testing.T) {
|
|
// Negative retries should be clamped to 1 (count must be at least 1).
|
|
c := NewChain("test-chain")
|
|
tr := NewTransport(&testDialer{}, &testConnector{})
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
c.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(c),
|
|
chain.RetriesRouterOption(-5),
|
|
)
|
|
|
|
conn, err := r.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
conn.Close()
|
|
}
|
|
|
|
func TestRouter_Bind_NegativeRetries(t *testing.T) {
|
|
c := NewChain("test-chain")
|
|
tr := NewTransport(&testDialer{}, &testConnector{
|
|
bindFn: func(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) {
|
|
return &testListener{}, nil
|
|
},
|
|
})
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
c.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(c),
|
|
chain.RetriesRouterOption(-1),
|
|
)
|
|
|
|
ln, err := r.Bind(context.Background(), "tcp", ":0")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ln)
|
|
ln.Close()
|
|
}
|
|
|
|
func TestRouter_Bind_ErrorOnRetry(t *testing.T) {
|
|
// All retries fail — verifies error logging path.
|
|
c := NewChain("test-chain")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return nil, errors.New("persistent error")
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("n1", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
c.AddHop(h)
|
|
|
|
r := NewRouter(
|
|
chain.ChainRouterOption(c),
|
|
chain.RetriesRouterOption(1),
|
|
)
|
|
|
|
ln, err := r.Bind(context.Background(), "tcp", ":0")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ln)
|
|
}
|
|
|
|
// =============================================================================
|
|
// PacketConn wrapper tests
|
|
// =============================================================================
|
|
|
|
func TestPacketConn_ReadFrom(t *testing.T) {
|
|
c := &packetConn{Conn: &testConn{}}
|
|
buf := make([]byte, 1024)
|
|
n, addr, err := c.ReadFrom(buf)
|
|
assert.NoError(t, err)
|
|
assert.GreaterOrEqual(t, n, 0)
|
|
assert.NotNil(t, addr)
|
|
assert.Equal(t, c.Conn.RemoteAddr(), addr)
|
|
}
|
|
|
|
func TestPacketConn_WriteTo(t *testing.T) {
|
|
c := &packetConn{Conn: &testConn{}}
|
|
addr := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 9999}
|
|
n, err := c.WriteTo([]byte("hello"), addr)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 5, n)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Integration: full route traversal
|
|
// =============================================================================
|
|
|
|
func TestConnect_FirstNodeDialError(t *testing.T) {
|
|
// connect() should mark both the node and the chain on dial failure.
|
|
c := NewChain("test-chain")
|
|
dialErr := errors.New("dial refused")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
dialFn: func(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) {
|
|
return nil, dialErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("bad-node", "127.0.0.1:9999", chain.TransportNodeOption(tr))
|
|
rt := NewRoute(ChainRouteOption(c))
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestConnect_FirstNodeHandshakeError(t *testing.T) {
|
|
// connect() should close the dialed connection on handshake failure.
|
|
c := NewChain("test-chain")
|
|
handshakeErr := errors.New("tls handshake failed")
|
|
tr := NewTransport(
|
|
&testDialer{
|
|
handshakeFn: func(ctx context.Context, conn net.Conn, opts ...dialer.HandshakeOption) (net.Conn, error) {
|
|
return nil, handshakeErr
|
|
},
|
|
},
|
|
&testConnector{},
|
|
)
|
|
node := chain.NewNode("bad-tls", "127.0.0.1:8080", chain.TransportNodeOption(tr))
|
|
rt := NewRoute(ChainRouteOption(c))
|
|
rt.addNode(node)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conn)
|
|
}
|
|
|
|
func TestConnect_SecondNodeEmptyAddress(t *testing.T) {
|
|
// When the second node has an empty address, xnet.Resolve returns ("", nil)
|
|
// and Connect is called with an empty target.
|
|
tr1 := NewTransport(&testDialer{}, &testConnector{})
|
|
node1 := chain.NewNode("ok-node", "127.0.0.1:8080", chain.TransportNodeOption(tr1))
|
|
|
|
node2 := chain.NewNode("empty-addr", "", chain.TransportNodeOption(
|
|
NewTransport(&testDialer{}, &testConnector{}),
|
|
))
|
|
|
|
rt := NewRoute()
|
|
rt.addNode(node1, node2)
|
|
|
|
conn, err := rt.Dial(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NoError(t, err)
|
|
conn.Close()
|
|
}
|
|
|
|
// =============================================================================
|
|
// Verify interface satisfaction
|
|
// =============================================================================
|
|
|
|
func TestTransport_ImplementsTransporter(t *testing.T) {
|
|
var _ chain.Transporter = (*Transport)(nil)
|
|
}
|
|
|
|
func TestChain_ImplementsChainer(t *testing.T) {
|
|
var _ chain.Chainer = (*Chain)(nil)
|
|
}
|
|
|
|
func TestChainGroup_ImplementsChainer(t *testing.T) {
|
|
var _ chain.Chainer = (*chainGroup)(nil)
|
|
}
|
|
|
|
func TestChainRoute_ImplementsRoute(t *testing.T) {
|
|
var _ chain.Route = (*chainRoute)(nil)
|
|
}
|
|
|
|
func TestDefaultRoute_ImplementsRoute_Interface(t *testing.T) {
|
|
var _ chain.Route = DefaultRoute
|
|
}
|
|
|
|
func TestRouter_ImplementsRouter(t *testing.T) {
|
|
var _ chain.Router = (*Router)(nil)
|
|
}
|
|
|
|
// =============================================================================
|
|
// selector.Selector usage in chainGroup
|
|
// =============================================================================
|
|
|
|
// testSelector implements selector.Selector[chain.Chainer].
|
|
type testSelector struct {
|
|
chains []chain.Chainer
|
|
idx int
|
|
}
|
|
|
|
func (s *testSelector) Select(ctx context.Context, chains ...chain.Chainer) chain.Chainer {
|
|
if len(s.chains) == 0 {
|
|
return nil
|
|
}
|
|
ch := s.chains[s.idx%len(s.chains)]
|
|
s.idx++
|
|
return ch
|
|
}
|
|
|
|
func TestChainGroup_WithSelector(t *testing.T) {
|
|
ch1 := NewChain("ch1")
|
|
ch2 := NewChain("ch2")
|
|
g := NewChainGroup(ch1, ch2)
|
|
|
|
sel := &testSelector{chains: []chain.Chainer{ch1, ch2}}
|
|
g.WithSelector(sel)
|
|
|
|
// With selector, Route should delegate to the selected chain.
|
|
// But NewChain creates an empty chain (no hops), so Route returns nil.
|
|
// We need to add hops to make Route return a non-nil route.
|
|
node := chain.NewNode("n1", "127.0.0.1:8080",
|
|
chain.TransportNodeOption(NewTransport(&testDialer{}, &testConnector{})))
|
|
h := &testHop{
|
|
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node { return node },
|
|
}
|
|
ch1.AddHop(h)
|
|
|
|
rt := g.Route(context.Background(), "tcp", "127.0.0.1:80")
|
|
require.NotNil(t, rt)
|
|
assert.Len(t, rt.Nodes(), 1)
|
|
}
|
|
|
|
func TestChainGroup_Next_NilReceiver(t *testing.T) {
|
|
var g *chainGroup
|
|
assert.Nil(t, g.next(context.Background()))
|
|
}
|
|
|
|
func TestChainGroup_Next_EmptyChains(t *testing.T) {
|
|
g := NewChainGroup()
|
|
assert.Nil(t, g.next(context.Background()))
|
|
}
|