test(handler/router): add helpers_test.go with mocks and test infrastructure
This commit is contained in:
@@ -0,0 +1,477 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/auth"
|
||||||
|
"github.com/go-gost/core/ingress"
|
||||||
|
"github.com/go-gost/core/limiter"
|
||||||
|
"github.com/go-gost/core/limiter/rate"
|
||||||
|
"github.com/go-gost/core/limiter/traffic"
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
|
core_metadata "github.com/go-gost/core/metadata"
|
||||||
|
"github.com/go-gost/core/observer"
|
||||||
|
"github.com/go-gost/core/router"
|
||||||
|
"github.com/go-gost/core/sd"
|
||||||
|
"github.com/go-gost/relay"
|
||||||
|
xmetadata "github.com/go-gost/x/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// testLogger
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type testLogger struct{}
|
||||||
|
|
||||||
|
func (l *testLogger) WithFields(map[string]any) logger.Logger { return l }
|
||||||
|
func (l *testLogger) IsLevelEnabled(logger.LogLevel) bool { return false }
|
||||||
|
func (l *testLogger) GetLevel() logger.LogLevel { return logger.InfoLevel }
|
||||||
|
func (l *testLogger) Trace(args ...any) {}
|
||||||
|
func (l *testLogger) Tracef(string, ...any) {}
|
||||||
|
func (l *testLogger) Debug(args ...any) {}
|
||||||
|
func (l *testLogger) Debugf(string, ...any) {}
|
||||||
|
func (l *testLogger) Info(args ...any) {}
|
||||||
|
func (l *testLogger) Infof(string, ...any) {}
|
||||||
|
func (l *testLogger) Warn(args ...any) {}
|
||||||
|
func (l *testLogger) Warnf(string, ...any) {}
|
||||||
|
func (l *testLogger) Error(args ...any) {}
|
||||||
|
func (l *testLogger) Errorf(string, ...any) {}
|
||||||
|
func (l *testLogger) Fatal(args ...any) {}
|
||||||
|
func (l *testLogger) Fatalf(string, ...any) {}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// testMD
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func testMD(m map[string]any) core_metadata.Metadata {
|
||||||
|
return xmetadata.NewMetadata(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// fakeConn — bytes.Buffer-backed net.Conn
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type fakeConn struct {
|
||||||
|
net.Conn
|
||||||
|
buf []byte
|
||||||
|
offset int
|
||||||
|
writeBuf bytes.Buffer
|
||||||
|
closed bool
|
||||||
|
mu sync.Mutex
|
||||||
|
laddr net.Addr
|
||||||
|
raddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeConn) Read(b []byte) (int, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if c.offset >= len(c.buf) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n := copy(b, c.buf[c.offset:])
|
||||||
|
c.offset += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeConn) Write(b []byte) (int, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.writeBuf.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeConn) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeConn) LocalAddr() net.Addr {
|
||||||
|
if c.laddr != nil {
|
||||||
|
return c.laddr
|
||||||
|
}
|
||||||
|
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeConn) RemoteAddr() net.Addr {
|
||||||
|
if c.raddr != nil {
|
||||||
|
return c.raddr
|
||||||
|
}
|
||||||
|
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (c *fakeConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (c *fakeConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// pipeConn — bidirectional pipe-based net.Conn
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type pipeConn struct {
|
||||||
|
reader *io.PipeReader
|
||||||
|
writer *io.PipeWriter
|
||||||
|
laddr net.Addr
|
||||||
|
raddr net.Addr
|
||||||
|
closed bool
|
||||||
|
closeMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPipePair() (*pipeConn, *pipeConn) {
|
||||||
|
pr1, pw1 := io.Pipe()
|
||||||
|
pr2, pw2 := io.Pipe()
|
||||||
|
a := &pipeConn{reader: pr1, writer: pw2}
|
||||||
|
b := &pipeConn{reader: pr2, writer: pw1}
|
||||||
|
a.laddr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
|
||||||
|
a.raddr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}
|
||||||
|
b.laddr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}
|
||||||
|
b.raddr = &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
|
||||||
|
return a, b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pipeConn) Read(b []byte) (int, error) { return c.reader.Read(b) }
|
||||||
|
func (c *pipeConn) Write(b []byte) (int, error) { return c.writer.Write(b) }
|
||||||
|
|
||||||
|
func (c *pipeConn) Close() error {
|
||||||
|
c.closeMu.Lock()
|
||||||
|
defer c.closeMu.Unlock()
|
||||||
|
if c.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.closed = true
|
||||||
|
c.reader.Close()
|
||||||
|
c.writer.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pipeConn) LocalAddr() net.Addr {
|
||||||
|
if c.laddr != nil {
|
||||||
|
return c.laddr
|
||||||
|
}
|
||||||
|
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pipeConn) RemoteAddr() net.Addr {
|
||||||
|
if c.raddr != nil {
|
||||||
|
return c.raddr
|
||||||
|
}
|
||||||
|
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pipeConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (c *pipeConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (c *pipeConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// fakePacketConn — channel-backed net.PacketConn
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type fakePacketConn struct {
|
||||||
|
dataCh chan []byte
|
||||||
|
addrCh chan net.Addr
|
||||||
|
closed bool
|
||||||
|
closeMu sync.Mutex
|
||||||
|
laddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFakePacketConn(laddr net.Addr) *fakePacketConn {
|
||||||
|
return &fakePacketConn{
|
||||||
|
dataCh: make(chan []byte, 64),
|
||||||
|
addrCh: make(chan net.Addr, 64),
|
||||||
|
laddr: laddr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakePacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
||||||
|
data, ok := <-c.dataCh
|
||||||
|
if !ok {
|
||||||
|
return 0, nil, io.EOF
|
||||||
|
}
|
||||||
|
n = copy(b, data)
|
||||||
|
addr = <-c.addrCh
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakePacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||||
|
data := make([]byte, len(b))
|
||||||
|
copy(data, b)
|
||||||
|
c.dataCh <- data
|
||||||
|
c.addrCh <- addr
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakePacketConn) Close() error {
|
||||||
|
c.closeMu.Lock()
|
||||||
|
defer c.closeMu.Unlock()
|
||||||
|
if !c.closed {
|
||||||
|
c.closed = true
|
||||||
|
close(c.dataCh)
|
||||||
|
close(c.addrCh)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakePacketConn) LocalAddr() net.Addr { return c.laddr }
|
||||||
|
|
||||||
|
func (c *fakePacketConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (c *fakePacketConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (c *fakePacketConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// fakeObserver — channel-based observer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type fakeObserver struct {
|
||||||
|
eventsCh chan []observer.Event
|
||||||
|
errFunc func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFakeObserver(buffer int) *fakeObserver {
|
||||||
|
return &fakeObserver{
|
||||||
|
eventsCh: make(chan []observer.Event, buffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *fakeObserver) Observe(ctx context.Context, events []observer.Event, opts ...observer.Option) error {
|
||||||
|
if o.errFunc != nil {
|
||||||
|
if err := o.errFunc(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case o.eventsCh <- events:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *fakeObserver) Events() <-chan []observer.Event { return o.eventsCh }
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockAuther
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type mockAuther struct {
|
||||||
|
authenticateFn func(ctx context.Context, user, pass string, opts ...auth.Option) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *mockAuther) Authenticate(ctx context.Context, user, pass string, opts ...auth.Option) (string, bool) {
|
||||||
|
if a.authenticateFn != nil {
|
||||||
|
return a.authenticateFn(ctx, user, pass, opts...)
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockRateLimiter + mockRateLimiterContainer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type mockRateLimiter struct {
|
||||||
|
allowFn func(n int) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *mockRateLimiter) Allow(n int) bool {
|
||||||
|
if l.allowFn != nil {
|
||||||
|
return l.allowFn(n)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (l *mockRateLimiter) Limit() float64 { return 1 }
|
||||||
|
func (l *mockRateLimiter) Wait(ctx context.Context) error { return nil }
|
||||||
|
|
||||||
|
type mockRateLimiterContainer struct {
|
||||||
|
limiterFn func(key string) rate.Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockRateLimiterContainer) Limiter(key string) rate.Limiter {
|
||||||
|
if c.limiterFn != nil {
|
||||||
|
return c.limiterFn(key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockTrafficLimiter
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type mockTrafficLimiter struct{}
|
||||||
|
|
||||||
|
func (l *mockTrafficLimiter) In(ctx context.Context, key string, opts ...limiter.Option) traffic.Limiter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (l *mockTrafficLimiter) Out(ctx context.Context, key string, opts ...limiter.Option) traffic.Limiter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockRouter — implements core/router.Router
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type mockRouter struct {
|
||||||
|
getRouteFn func(ctx context.Context, dst string, opts ...router.Option) *router.Route
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRouter) GetRoute(ctx context.Context, dst string, opts ...router.Option) *router.Route {
|
||||||
|
if r.getRouteFn != nil {
|
||||||
|
return r.getRouteFn(ctx, dst, opts...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockSD
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type mockSD struct {
|
||||||
|
getFn func(ctx context.Context, name string, opts ...sd.Option) ([]*sd.Service, error)
|
||||||
|
registerFn func(ctx context.Context, svc *sd.Service, opts ...sd.Option) error
|
||||||
|
deregisterFn func(ctx context.Context, svc *sd.Service, opts ...sd.Option) error
|
||||||
|
renewFn func(ctx context.Context, svc *sd.Service, opts ...sd.Option) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockSD) Get(ctx context.Context, name string, opts ...sd.Option) ([]*sd.Service, error) {
|
||||||
|
if s.getFn != nil {
|
||||||
|
return s.getFn(ctx, name, opts...)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (s *mockSD) Register(ctx context.Context, svc *sd.Service, opts ...sd.Option) error {
|
||||||
|
if s.registerFn != nil {
|
||||||
|
return s.registerFn(ctx, svc, opts...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *mockSD) Deregister(ctx context.Context, svc *sd.Service, opts ...sd.Option) error {
|
||||||
|
if s.deregisterFn != nil {
|
||||||
|
return s.deregisterFn(ctx, svc, opts...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *mockSD) Renew(ctx context.Context, svc *sd.Service, opts ...sd.Option) error {
|
||||||
|
if s.renewFn != nil {
|
||||||
|
return s.renewFn(ctx, svc, opts...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockIngress
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type mockIngress struct {
|
||||||
|
getRuleFn func(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ing *mockIngress) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule {
|
||||||
|
if ing.getRuleFn != nil {
|
||||||
|
return ing.getRuleFn(ctx, host, opts...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// buildRelayAssociateRequest — serializes a relay association request to bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func buildRelayAssociateRequest(t testingT, address string, routerID relay.TunnelID, network string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
req := relay.Request{
|
||||||
|
Version: relay.Version1,
|
||||||
|
Cmd: relay.CmdAssociate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if address != "" {
|
||||||
|
af := &relay.AddrFeature{}
|
||||||
|
if err := af.ParseFrom(address); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Features = append(req.Features, af)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Features = append(req.Features, &relay.TunnelFeature{ID: routerID})
|
||||||
|
|
||||||
|
networkID := relay.NetworkIP
|
||||||
|
switch network {
|
||||||
|
case "ip", "ip4", "ip6":
|
||||||
|
networkID = relay.NetworkIP
|
||||||
|
}
|
||||||
|
req.Features = append(req.Features, &relay.NetworkFeature{Network: networkID})
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := req.WriteTo(&buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// readRelayResponse — deserializes relay.Response from bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func readRelayResponse(t testingT, data []byte) *relay.Response {
|
||||||
|
t.Helper()
|
||||||
|
resp := &relay.Response{}
|
||||||
|
if _, err := resp.ReadFrom(bytes.NewReader(data)); err != nil {
|
||||||
|
t.Fatalf("read relay response: %v", err)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// testingT interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type testingT interface {
|
||||||
|
Helper()
|
||||||
|
Fatal(args ...any)
|
||||||
|
Fatalf(format string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// IP packet builders for handlePacket tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func buildIPv4Packet(srcIP, dstIP string, payload []byte) []byte {
|
||||||
|
const headerLen = 20
|
||||||
|
totalLen := headerLen + len(payload)
|
||||||
|
pkt := make([]byte, totalLen)
|
||||||
|
|
||||||
|
pkt[0] = 0x45
|
||||||
|
pkt[2] = byte(totalLen >> 8)
|
||||||
|
pkt[3] = byte(totalLen)
|
||||||
|
pkt[8] = 64
|
||||||
|
pkt[9] = 17
|
||||||
|
copy(pkt[12:16], net.ParseIP(srcIP).To4())
|
||||||
|
copy(pkt[16:20], net.ParseIP(dstIP).To4())
|
||||||
|
|
||||||
|
var sum uint32
|
||||||
|
for i := 0; i < headerLen; i += 2 {
|
||||||
|
sum += uint32(pkt[i])<<8 | uint32(pkt[i+1])
|
||||||
|
}
|
||||||
|
for sum>>16 > 0 {
|
||||||
|
sum = (sum & 0xFFFF) + (sum >> 16)
|
||||||
|
}
|
||||||
|
pkt[10] = byte(^sum >> 8)
|
||||||
|
pkt[11] = byte(^sum & 0xFF)
|
||||||
|
|
||||||
|
copy(pkt[headerLen:], payload)
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildIPv6Packet(srcIP, dstIP string, payload []byte) []byte {
|
||||||
|
pkt := make([]byte, 40+len(payload))
|
||||||
|
pkt[0] = 0x60
|
||||||
|
pkt[4] = byte(len(payload) >> 8)
|
||||||
|
pkt[5] = byte(len(payload))
|
||||||
|
pkt[6] = 17
|
||||||
|
pkt[7] = 64
|
||||||
|
copy(pkt[8:24], net.ParseIP(srcIP).To16())
|
||||||
|
copy(pkt[24:40], net.ParseIP(dstIP).To16())
|
||||||
|
copy(pkt[40:], payload)
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user