From 105f7e32d0c1bea81267a5eb58bd06d293ac9693 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Wed, 3 Jun 2026 23:47:54 +0800 Subject: [PATCH] test(handler/router): add observe_test.go (checkRateLimit, observeStats) --- handler/router/observe_test.go | 213 +++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 handler/router/observe_test.go diff --git a/handler/router/observe_test.go b/handler/router/observe_test.go new file mode 100644 index 00000000..f539e59a --- /dev/null +++ b/handler/router/observe_test.go @@ -0,0 +1,213 @@ +package router + +import ( + "context" + "errors" + "net" + "sync" + "testing" + "time" + + "github.com/go-gost/core/handler" + "github.com/go-gost/core/limiter/rate" + "github.com/go-gost/core/observer" + "github.com/go-gost/core/observer/stats" + stats_util "github.com/go-gost/x/internal/util/stats" +) + +// --------------------------------------------------------------------------- +// checkRateLimit tests +// --------------------------------------------------------------------------- + +func TestCheckRateLimit_NilLimiter(t *testing.T) { + h := &routerHandler{ + options: handler.Options{ + RateLimiter: nil, + }, + } + addr := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 12345} + if !h.checkRateLimit(addr) { + t.Error("checkRateLimit returned false, want true") + } +} + +func TestCheckRateLimit_Allowed(t *testing.T) { + h := &routerHandler{ + options: handler.Options{ + RateLimiter: &mockRateLimiterContainer{ + limiterFn: func(key string) rate.Limiter { + return &mockRateLimiter{ + allowFn: func(n int) bool { return true }, + } + }, + }, + }, + } + addr := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 12345} + if !h.checkRateLimit(addr) { + t.Error("checkRateLimit returned false, want true") + } +} + +func TestCheckRateLimit_Denied(t *testing.T) { + h := &routerHandler{ + options: handler.Options{ + RateLimiter: &mockRateLimiterContainer{ + limiterFn: func(key string) rate.Limiter { + return &mockRateLimiter{ + allowFn: func(n int) bool { return false }, + } + }, + }, + }, + } + addr := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 12345} + if h.checkRateLimit(addr) { + t.Error("checkRateLimit returned true, want false") + } +} + +func TestCheckRateLimit_LimiterByHost(t *testing.T) { + var gotKey string + h := &routerHandler{ + options: handler.Options{ + RateLimiter: &mockRateLimiterContainer{ + limiterFn: func(key string) rate.Limiter { + gotKey = key + return &mockRateLimiter{ + allowFn: func(n int) bool { return true }, + } + }, + }, + }, + } + addr := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 12345} + h.checkRateLimit(addr) + if gotKey != "10.0.0.1" { + t.Errorf("limiter key = %q, want 10.0.0.1", gotKey) + } +} + +// --------------------------------------------------------------------------- +// observeStats tests +// --------------------------------------------------------------------------- + +// newHandlerWithObserver creates a routerHandler with a fake observer. +func newHandlerWithObserver(t *testing.T, obs *fakeObserver) *routerHandler { + t.Helper() + h := &routerHandler{ + options: handler.Options{ + Observer: obs, + Service: "test-svc", + }, + md: metadata{ + observerPeriod: 50 * time.Millisecond, + observerResetTraffic: false, + }, + } + h.stats = stats_util.NewHandlerStats("test-svc", false) + return h +} + +func TestObserveStats_NilObserver(t *testing.T) { + h := &routerHandler{} + // Should return immediately without panic + h.observeStats(context.Background()) +} + +func TestObserveStats_NormalCycle(t *testing.T) { + obs := newFakeObserver(10) + h := newHandlerWithObserver(t, obs) + + // Simulate some stat activity so Events() returns data. + h.stats.Stats("client-1").Add(stats.KindTotalConns, 1) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + h.observeStats(ctx) + }() + + // Wait for at least one observation cycle + select { + case <-obs.Events(): + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for observer event") + } + + cancel() + wg.Wait() +} + +func TestObserveStats_ContextCancel(t *testing.T) { + obs := newFakeObserver(10) + h := newHandlerWithObserver(t, obs) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + + // Should return immediately when context is already cancelled + done := make(chan struct{}) + go func() { + h.observeStats(ctx) + close(done) + }() + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("observeStats did not exit after context cancel") + } +} + +func TestObserveStats_RetryOnError(t *testing.T) { + var callCount int + var mu sync.Mutex + obs := &fakeObserver{ + eventsCh: make(chan []observer.Event, 10), + errFunc: func() error { + mu.Lock() + callCount++ + count := callCount + mu.Unlock() + if count <= 2 { + return errors.New("observer error") + } + return nil + }, + } + h := newHandlerWithObserver(t, obs) + + // Simulate stat activity so Events() returns data. + h.stats.Stats("client-1").Add(stats.KindTotalConns, 1) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + h.observeStats(ctx) + }() + + // Wait for successful observation (callCount > 2) + select { + case <-obs.Events(): + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for successful observer event") + } + + mu.Lock() + if callCount < 2 { + t.Errorf("callCount = %d, want at least 2 (error retries)", callCount) + } + mu.Unlock() + + cancel() + wg.Wait() +} \ No newline at end of file