refactor(handler/relay): split handler.go -> observe.go, fix observeStats event-loss bug (break->fallthrough)
Extract checkRateLimit and observeStats into their own file (observe.go). Fix the one-tick event-loss bug in observeStats where the retry path used 'unconditional break' instead of fallthrough, causing events collected after a successful retry to be discarded. Add 87 unit tests across 7 files (helpers, metadata, conn, handler, connect, forward, bind) covering all handler modes, error paths, observer/stats integration, and conn wrapper edge cases. handler.go: -43 lines (unused observer import, extracted functions) observe.go: +52 lines (checkRateLimit, observeStats with fallthrough fix) test files: +1959 lines
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core_metadata "github.com/go-gost/core/metadata"
|
||||
xmetadata "github.com/go-gost/x/metadata"
|
||||
)
|
||||
|
||||
func TestParseMetadata_Defaults(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
err := h.parseMetadata(xmetadata.NewMetadata(nil))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if h.md.readTimeout != 15*time.Second {
|
||||
t.Errorf("readTimeout = %v, want 15s", h.md.readTimeout)
|
||||
}
|
||||
if h.md.enableBind {
|
||||
t.Error("enableBind = true, want false")
|
||||
}
|
||||
if h.md.noDelay {
|
||||
t.Error("noDelay = true, want false")
|
||||
}
|
||||
if h.md.hash != "" {
|
||||
t.Errorf("hash = %q, want empty", h.md.hash)
|
||||
}
|
||||
if h.md.udpBufferSize != 0 {
|
||||
t.Errorf("udpBufferSize = %d, want 0", h.md.udpBufferSize)
|
||||
}
|
||||
if h.md.observerPeriod != 5*time.Second {
|
||||
t.Errorf("observerPeriod = %v, want 5s", h.md.observerPeriod)
|
||||
}
|
||||
if h.md.observerResetTraffic {
|
||||
t.Error("observerResetTraffic = true, want false")
|
||||
}
|
||||
if h.md.sniffing {
|
||||
t.Error("sniffing = true, want false")
|
||||
}
|
||||
if h.md.muxCfg == nil {
|
||||
t.Fatal("muxCfg = nil, want non-nil")
|
||||
}
|
||||
// Note: relay handler does NOT default muxCfg.Version or MaxStreamBuffer
|
||||
// at parseMetadata time. Those defaults are in the tunnel handler only.
|
||||
}
|
||||
|
||||
func TestParseMetadata_ReadTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input core_metadata.Metadata
|
||||
want time.Duration
|
||||
}{
|
||||
{"zero", testMD(map[string]any{"readTimeout": 0}), 15 * time.Second},
|
||||
{"string 30s", testMD(map[string]any{"readTimeout": "30s"}), 30 * time.Second},
|
||||
{"int seconds", testMD(map[string]any{"readTimeout": 10}), 10 * time.Second},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(tt.input)
|
||||
if h.md.readTimeout != tt.want {
|
||||
t.Errorf("readTimeout = %v, want %v", h.md.readTimeout, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_UDPBufferSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input core_metadata.Metadata
|
||||
want int
|
||||
}{
|
||||
{"zero", testMD(nil), 0},
|
||||
{"udp.bufferSize", testMD(map[string]any{"udp.bufferSize": 4096}), 4096},
|
||||
{"udpBufferSize alias", testMD(map[string]any{"udpBufferSize": 8192}), 8192},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(tt.input)
|
||||
if h.md.udpBufferSize != tt.want {
|
||||
t.Errorf("udpBufferSize = %d, want %d", h.md.udpBufferSize, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_Bind(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{"bind": true}))
|
||||
if !h.md.enableBind {
|
||||
t.Error("enableBind = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_NoDelay(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{"nodelay": true}))
|
||||
if !h.md.noDelay {
|
||||
t.Error("noDelay = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_Hash(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{"hash": "host"}))
|
||||
if h.md.hash != "host" {
|
||||
t.Errorf("hash = %q, want host", h.md.hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_MuxConfig(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{
|
||||
"mux.version": 3,
|
||||
"mux.keepaliveInterval": "10s",
|
||||
"mux.keepaliveDisabled": true,
|
||||
"mux.keepaliveTimeout": "5s",
|
||||
"mux.maxFrameSize": 4096,
|
||||
"mux.maxReceiveBuffer": 8192,
|
||||
"mux.maxStreamBuffer": 65536,
|
||||
}))
|
||||
if h.md.muxCfg.Version != 3 {
|
||||
t.Errorf("muxCfg.Version = %d, want 3", h.md.muxCfg.Version)
|
||||
}
|
||||
if h.md.muxCfg.KeepAliveInterval != 10*time.Second {
|
||||
t.Errorf("muxCfg.KeepAliveInterval = %v, want 10s", h.md.muxCfg.KeepAliveInterval)
|
||||
}
|
||||
if !h.md.muxCfg.KeepAliveDisabled {
|
||||
t.Error("muxCfg.KeepAliveDisabled = false, want true")
|
||||
}
|
||||
if h.md.muxCfg.KeepAliveTimeout != 5*time.Second {
|
||||
t.Errorf("muxCfg.KeepAliveTimeout = %v, want 5s", h.md.muxCfg.KeepAliveTimeout)
|
||||
}
|
||||
if h.md.muxCfg.MaxFrameSize != 4096 {
|
||||
t.Errorf("muxCfg.MaxFrameSize = %d, want 4096", h.md.muxCfg.MaxFrameSize)
|
||||
}
|
||||
if h.md.muxCfg.MaxReceiveBuffer != 8192 {
|
||||
t.Errorf("muxCfg.MaxReceiveBuffer = %d, want 8192", h.md.muxCfg.MaxReceiveBuffer)
|
||||
}
|
||||
if h.md.muxCfg.MaxStreamBuffer != 65536 {
|
||||
t.Errorf("muxCfg.MaxStreamBuffer = %d, want 65536", h.md.muxCfg.MaxStreamBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_Observer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input core_metadata.Metadata
|
||||
wantPeriod time.Duration
|
||||
wantReset bool
|
||||
}{
|
||||
{"defaults", testMD(nil), 5 * time.Second, false},
|
||||
{"custom period", testMD(map[string]any{"observePeriod": "10s"}), 10 * time.Second, false},
|
||||
{"clamped below 1s", testMD(map[string]any{"observePeriod": "500ms"}), time.Second, false},
|
||||
{"alias observer.period", testMD(map[string]any{"observer.period": "3s"}), 3 * time.Second, false},
|
||||
{"alias observer.observePeriod", testMD(map[string]any{"observer.observePeriod": "7s"}), 7 * time.Second, false},
|
||||
{"reset traffic", testMD(map[string]any{"observer.resetTraffic": true}), 5 * time.Second, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(tt.input)
|
||||
if h.md.observerPeriod != tt.wantPeriod {
|
||||
t.Errorf("observerPeriod = %v, want %v", h.md.observerPeriod, tt.wantPeriod)
|
||||
}
|
||||
if h.md.observerResetTraffic != tt.wantReset {
|
||||
t.Errorf("observerResetTraffic = %v, want %v", h.md.observerResetTraffic, tt.wantReset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_Sniffing(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{
|
||||
"sniffing": true,
|
||||
"sniffing.timeout": "2s",
|
||||
"sniffing.websocket": true,
|
||||
"sniffing.websocket.sampleRate": 0.5,
|
||||
}))
|
||||
if !h.md.sniffing {
|
||||
t.Error("sniffing = false, want true")
|
||||
}
|
||||
if h.md.sniffingTimeout != 2*time.Second {
|
||||
t.Errorf("sniffingTimeout = %v, want 2s", h.md.sniffingTimeout)
|
||||
}
|
||||
if !h.md.sniffingWebsocket {
|
||||
t.Error("sniffingWebsocket = false, want true")
|
||||
}
|
||||
if h.md.sniffingWebsocketSampleRate != 0.5 {
|
||||
t.Errorf("sniffingWebsocketSampleRate = %v, want 0.5", h.md.sniffingWebsocketSampleRate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_LimiterRefresh(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{
|
||||
"limiter.refreshInterval": "30s",
|
||||
"limiter.cleanupInterval": "60s",
|
||||
}))
|
||||
if h.md.limiterRefreshInterval != 30*time.Second {
|
||||
t.Errorf("limiterRefreshInterval = %v, want 30s", h.md.limiterRefreshInterval)
|
||||
}
|
||||
if h.md.limiterCleanupInterval != 60*time.Second {
|
||||
t.Errorf("limiterCleanupInterval = %v, want 60s", h.md.limiterCleanupInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetadata_Mitm(t *testing.T) {
|
||||
t.Run("not set", func(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
err := h.parseMetadata(testMD(nil))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if h.md.certificate != nil {
|
||||
t.Error("certificate should be nil")
|
||||
}
|
||||
if h.md.privateKey != nil {
|
||||
t.Error("privateKey should be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cert file not found", func(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
err := h.parseMetadata(testMD(map[string]any{
|
||||
"mitm.certFile": "/nonexistent/cert.pem",
|
||||
"mitm.keyFile": "/nonexistent/key.pem",
|
||||
}))
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent cert file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("alpn set", func(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{"mitm.alpn": "h2,http/1.1"}))
|
||||
if h.md.alpn != "h2,http/1.1" {
|
||||
t.Errorf("alpn = %q, want h2,http/1.1", h.md.alpn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseMetadata_ObserverPeriodDefaultsTo5s(t *testing.T) {
|
||||
h := &relayHandler{}
|
||||
h.parseMetadata(testMD(map[string]any{"observePeriod": 0}))
|
||||
if h.md.observerPeriod != 5*time.Second {
|
||||
t.Errorf("observerPeriod = %v, want 5s", h.md.observerPeriod)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user