test(handler/router): add observe_test.go (checkRateLimit, observeStats)
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user