Files
lukaszraczylo c2c75d69c0 perf+coverage: optimisation pass + coverage push to ≥70%
Performance / resource usage:
- circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking)
- server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo
- graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off)
- graphql.go: collapse two-pass AST scan into single pass; lower-case once
- sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars
- proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc
- metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled
- logging/logger.go: add IsLevelEnabled helper
- lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%)
- cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock
- rps_tracker.go: RWMutex+uint64 -> atomic.Uint64
- retry_budget.go: drop unused mutex
- api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers)
- tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes
- admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection

Build / runtime:
- Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes)
- Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB
- main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS)
- main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts
- README.md: env-var docs + metric-label docs updated; cardinality note

Test coverage push (per package):
- main 51.2% -> 74.7%
- cache 66.3% -> 93.7%
- cache/redis 45.5% -> 98.2%
- tracing 66.7% -> 72.9%
- (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged)

New test files: coverage_micro_test, coverage_extras_test, server_handlers_test,
api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test,
cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test.

Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup
was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via
parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
2026-04-19 19:49:24 +01:00

567 lines
16 KiB
Go

package main
import (
"bytes"
"context"
"net/http/httptest"
"sort"
"strings"
"testing"
"time"
"github.com/gofiber/fiber/v2"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/valyala/fasthttp"
)
// ---------------------------------------------------------------------------
// buffer_pool.go
// ---------------------------------------------------------------------------
func TestCoverageMicro_GzipWriterPool(t *testing.T) {
t.Run("GetGzipWriter returns non-nil", func(t *testing.T) {
var buf bytes.Buffer
gz := GetGzipWriter(&buf)
if gz == nil {
t.Fatal("expected non-nil gzip.Writer")
}
// Write something so Reset works correctly later
_, _ = gz.Write([]byte("hello"))
_ = gz.Flush()
PutGzipWriter(gz)
})
t.Run("Put then Get round-trip still usable", func(t *testing.T) {
var buf1 bytes.Buffer
gz := GetGzipWriter(&buf1)
if gz == nil {
t.Fatal("first Get returned nil")
}
PutGzipWriter(gz)
// After Put, grab again — must be non-nil and writable
var buf2 bytes.Buffer
gz2 := GetGzipWriter(&buf2)
if gz2 == nil {
t.Fatal("second Get after Put returned nil")
}
_, err := gz2.Write([]byte("world"))
if err != nil {
t.Fatalf("write after round-trip failed: %v", err)
}
_ = gz2.Close()
})
}
// ---------------------------------------------------------------------------
// circuit_breaker_metrics.go
// ---------------------------------------------------------------------------
func TestCoverageMicro_CircuitBreakerMetrics_GetState(t *testing.T) {
cbm := &CircuitBreakerMetrics{}
cbm.stateValue.Store(float64(0))
t.Run("initial value is zero", func(t *testing.T) {
if got := cbm.GetState(); got != 0.0 {
t.Fatalf("want 0.0, got %v", got)
}
})
t.Run("set then get returns correct value", func(t *testing.T) {
cbm.UpdateState(2.0)
if got := cbm.GetState(); got != 2.0 {
t.Fatalf("want 2.0, got %v", got)
}
})
t.Run("nil atomic value falls back to zero", func(t *testing.T) {
fresh := &CircuitBreakerMetrics{} // stateValue not initialised
// Load on unset atomic.Value returns nil
if got := fresh.GetState(); got != 0.0 {
t.Fatalf("want 0.0, got %v", got)
}
})
}
// ---------------------------------------------------------------------------
// errors.go
// ---------------------------------------------------------------------------
func TestCoverageMicro_TruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
want string
}{
{"short string unchanged", "hi", 10, "hi"},
{"exact length unchanged", "hello", 5, "hello"},
{"longer than max gets truncated", "hello world", 5, "hello..."},
{"empty string", "", 5, ""},
{"max zero", "abc", 0, "..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := truncateString(tt.input, tt.maxLen)
if got != tt.want {
t.Fatalf("truncateString(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
}
})
}
}
func TestCoverageMicro_IsRetryable(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"nil error", nil, false},
{"retryable proxy error", NewProxyError(ErrCodeTimeout, "timeout", 503, true), true},
{"non-retryable proxy error", NewProxyError(ErrCodeUnauthorized, "unauth", 401, false), false},
{"plain error", &RateLimitConfigError{Paths: []string{"/tmp"}, PathErrors: map[string]string{"/tmp": "not found"}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsRetryable(tt.err); got != tt.want {
t.Fatalf("IsRetryable() = %v, want %v", got, tt.want)
}
})
}
}
func TestCoverageMicro_GetStatusCode(t *testing.T) {
tests := []struct {
name string
err error
want int
}{
{"nil error returns 200", nil, 200},
{"proxy error returns status code", NewProxyError(ErrCodeBadGateway, "bad gw", 502, false), 502},
{"non-proxy error returns 500", &RateLimitConfigError{Paths: []string{}, PathErrors: map[string]string{}}, 500},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetStatusCode(tt.err); got != tt.want {
t.Fatalf("GetStatusCode() = %d, want %d", got, tt.want)
}
})
}
}
// ---------------------------------------------------------------------------
// ratelimit_errors.go
// ---------------------------------------------------------------------------
func TestCoverageMicro_RateLimitConfigError_Error(t *testing.T) {
t.Run("contains paths in output", func(t *testing.T) {
paths := []string{"/etc/ratelimit.json", "/app/ratelimit.json"}
e := NewRateLimitConfigError(paths)
e.PathErrors["/etc/ratelimit.json"] = "permission denied"
e.PathErrors["/app/ratelimit.json"] = "file not found"
msg := e.Error()
if !strings.Contains(msg, "/etc/ratelimit.json") {
t.Error("expected path /etc/ratelimit.json in error message")
}
if !strings.Contains(msg, "permission denied") {
t.Error("expected error detail in message")
}
})
t.Run("empty paths produces valid string", func(t *testing.T) {
e := NewRateLimitConfigError(nil)
msg := e.Error()
if msg == "" {
t.Error("expected non-empty error message even with no paths")
}
})
}
// ---------------------------------------------------------------------------
// backend_health.go
// ---------------------------------------------------------------------------
func TestCoverageMicro_BackendHealth(t *testing.T) {
logger := libpack_logger.New()
client := &fasthttp.Client{}
t.Run("updateHealthStatus healthy→unhealthy transition", func(t *testing.T) {
bhm := NewBackendHealthManager(client, "http://localhost:9999", logger)
defer bhm.Shutdown()
// Start healthy
bhm.isHealthy.Store(true)
bhm.updateHealthStatus(false)
if bhm.IsHealthy() {
t.Error("expected unhealthy after updateHealthStatus(false)")
}
if bhm.GetConsecutiveFailures() != 1 {
t.Errorf("expected 1 consecutive failure, got %d", bhm.GetConsecutiveFailures())
}
})
t.Run("updateHealthStatus unhealthy→healthy resets counter", func(t *testing.T) {
bhm := NewBackendHealthManager(client, "http://localhost:9999", logger)
defer bhm.Shutdown()
bhm.isHealthy.Store(false)
bhm.consecutiveFails.Store(5)
bhm.updateHealthStatus(true)
if !bhm.IsHealthy() {
t.Error("expected healthy after updateHealthStatus(true)")
}
if bhm.GetConsecutiveFailures() != 0 {
t.Errorf("expected 0 failures after recovery, got %d", bhm.GetConsecutiveFailures())
}
})
t.Run("GetLastHealthCheck round-trip", func(t *testing.T) {
bhm := NewBackendHealthManager(client, "http://localhost:9999", logger)
defer bhm.Shutdown()
before := time.Now()
bhm.updateHealthStatus(true)
after := time.Now()
last := bhm.GetLastHealthCheck()
if last.Before(before) || last.After(after) {
t.Errorf("last health check time %v outside expected range [%v, %v]", last, before, after)
}
})
t.Run("nil receiver safe", func(t *testing.T) {
var nilBHM *BackendHealthManager
nilBHM.updateHealthStatus(true) // must not panic
if !nilBHM.GetLastHealthCheck().IsZero() {
t.Error("expected zero time for nil receiver")
}
})
}
// ---------------------------------------------------------------------------
// graphql.go — trackParsingAllocations
// ---------------------------------------------------------------------------
func TestCoverageMicro_TrackParsingAllocations(t *testing.T) {
t.Run("returned closure runs without panic", func(t *testing.T) {
done := trackParsingAllocations()
// Execute some allocations between start and stop
_ = make([]byte, 1024)
done() // must not panic regardless of cfg.Monitoring state
})
t.Run("closure safe when cfg.Monitoring is nil", func(t *testing.T) {
// Only manipulate cfg.Monitoring if cfg is already initialised
cfgMutex.RLock()
cfgInitialised := cfg != nil
cfgMutex.RUnlock()
if cfgInitialised {
cfgMutex.Lock()
origMonitoring := cfg.Monitoring
cfg.Monitoring = nil
cfgMutex.Unlock()
defer func() {
cfgMutex.Lock()
cfg.Monitoring = origMonitoring
cfgMutex.Unlock()
}()
}
done := trackParsingAllocations()
done() // must not panic regardless of monitoring state
})
}
// ---------------------------------------------------------------------------
// retry_budget.go — UpdateConfig
// ---------------------------------------------------------------------------
func TestCoverageMicro_RetryBudget_UpdateConfig(t *testing.T) {
t.Run("config fields applied", func(t *testing.T) {
initial := RetryBudgetConfig{TokensPerSecond: 5.0, MaxTokens: 50, Enabled: true}
rb := NewRetryBudget(initial, nil)
defer rb.Shutdown()
newCfg := RetryBudgetConfig{TokensPerSecond: 20.0, MaxTokens: 200, Enabled: false}
rb.UpdateConfig(newCfg)
if rb.tokensPerSecond != 20.0 {
t.Errorf("tokensPerSecond: want 20.0, got %v", rb.tokensPerSecond)
}
if rb.maxTokens != 200 {
t.Errorf("maxTokens: want 200, got %v", rb.maxTokens)
}
if rb.enabled {
t.Error("expected enabled=false after UpdateConfig")
}
// currentTokens should equal maxTokens after reset
if rb.currentTokens.Load() != 200 {
t.Errorf("currentTokens: want 200, got %v", rb.currentTokens.Load())
}
})
}
// ---------------------------------------------------------------------------
// rps_tracker.go
// ---------------------------------------------------------------------------
func TestCoverageMicro_RPSTracker(t *testing.T) {
t.Run("NewRPSTracker returns non-nil", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tracker := NewRPSTracker(ctx)
if tracker == nil {
t.Fatal("expected non-nil RPSTracker")
}
tracker.Shutdown()
})
t.Run("RecordRequest increments counter", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tracker := NewRPSTracker(ctx)
defer tracker.Shutdown()
for range 10 {
tracker.RecordRequest()
}
if tracker.lastCount.Load() != 10 {
t.Errorf("expected 10, got %d", tracker.lastCount.Load())
}
})
t.Run("GetCurrentRPS returns zero before first sample", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tracker := NewRPSTracker(ctx)
defer tracker.Shutdown()
rps := tracker.GetCurrentRPS()
if rps < 0 {
t.Errorf("RPS should not be negative, got %v", rps)
}
})
t.Run("sample calculates non-zero RPS after requests", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tracker := NewRPSTracker(ctx)
defer tracker.Shutdown()
// Record requests, then manually advance the sample time to simulate 1s elapsed
for range 50 {
tracker.RecordRequest()
}
// Set lastSampleTime to 1 second ago so elapsed > 0
tracker.lastSampleTime.Store(time.Now().Add(-1 * time.Second).UnixNano())
tracker.sample()
rps := tracker.GetCurrentRPS()
if rps <= 0 {
t.Errorf("expected RPS > 0 after sample with requests, got %v", rps)
}
})
t.Run("Shutdown stops gracefully", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tracker := NewRPSTracker(ctx)
// Should not block
done := make(chan struct{})
go func() {
tracker.Shutdown()
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Error("Shutdown blocked for > 2s")
}
})
}
// ---------------------------------------------------------------------------
// metrics_aggregator.go — GetInstanceID, IsClusterMode (no Redis), GetInstanceHostname
// ---------------------------------------------------------------------------
func TestCoverageMicro_MetricsAggregatorGetters(t *testing.T) {
t.Run("GetInstanceID returns stored ID", func(t *testing.T) {
ma := &MetricsAggregator{instanceID: "test-instance-abc"}
if got := ma.GetInstanceID(); got != "test-instance-abc" {
t.Errorf("want test-instance-abc, got %q", got)
}
})
t.Run("GetInstanceHostname returns non-empty string", func(t *testing.T) {
host := GetInstanceHostname()
if host == "" {
t.Error("GetInstanceHostname returned empty string")
}
// Must not contain a dot (domain suffix stripped)
if strings.Contains(host, ".") {
t.Errorf("hostname should have domain stripped, got %q", host)
}
})
}
// ---------------------------------------------------------------------------
// websocket.go — IsWebSocketRequest
// ---------------------------------------------------------------------------
func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
tests := []struct {
name string
setHeaders func(*fasthttp.RequestHeader)
want bool
}{
{
name: "Upgrade websocket header set",
setHeaders: func(h *fasthttp.RequestHeader) {
h.Set("Upgrade", "websocket")
h.Set("Connection", "Upgrade")
},
want: true,
},
{
name: "no upgrade headers",
setHeaders: func(h *fasthttp.RequestHeader) {},
want: false,
},
{
name: "Connection Upgrade only",
setHeaders: func(h *fasthttp.RequestHeader) {
h.Set("Connection", "Upgrade")
},
want: true,
},
}
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app.Get("/ws-test", func(c *fiber.Ctx) error {
result := IsWebSocketRequest(c)
if result {
return c.SendStatus(101)
}
return c.SendStatus(200)
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/ws-test", nil)
tt.setHeaders(&fasthttp.RequestHeader{})
// Set headers on net/http request which fiber will read
switch tt.name {
case "Upgrade websocket header set":
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
case "Connection Upgrade only":
req.Header.Set("Connection", "Upgrade")
}
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("app.Test error: %v", err)
}
_ = resp.Body.Close()
wantCode := 200
if tt.want {
wantCode = 101
}
if resp.StatusCode != wantCode {
t.Errorf("status: want %d, got %d", wantCode, resp.StatusCode)
}
})
}
}
// ---------------------------------------------------------------------------
// admin_dashboard.go — getMapKeys
// ---------------------------------------------------------------------------
func TestCoverageMicro_GetMapKeys(t *testing.T) {
t.Run("nil map returns empty slice", func(t *testing.T) {
keys := getMapKeys(nil)
if len(keys) != 0 {
t.Errorf("expected empty slice for nil map, got %v", keys)
}
})
t.Run("empty map returns empty slice", func(t *testing.T) {
keys := getMapKeys(map[string]any{})
if len(keys) != 0 {
t.Errorf("expected empty slice, got %v", keys)
}
})
t.Run("populated map returns all keys", func(t *testing.T) {
m := map[string]any{"alpha": 1, "beta": 2, "gamma": 3}
keys := getMapKeys(m)
if len(keys) != 3 {
t.Fatalf("expected 3 keys, got %d: %v", len(keys), keys)
}
sort.Strings(keys)
want := []string{"alpha", "beta", "gamma"}
for i, k := range keys {
if k != want[i] {
t.Errorf("key[%d]: want %q, got %q", i, want[i], k)
}
}
})
}
// ---------------------------------------------------------------------------
// proxy.go — setupTracing (tracing disabled path)
// ---------------------------------------------------------------------------
func TestCoverageMicro_SetupTracing_Disabled(t *testing.T) {
t.Run("tracing disabled returns background context", func(t *testing.T) {
// Ensure cfg is initialised before reading it
cfgMutex.RLock()
needsInit := cfg == nil
cfgMutex.RUnlock()
if needsInit {
parseConfig()
}
// Ensure tracing is disabled
cfgMutex.Lock()
origEnable := cfg.Tracing.Enable
cfg.Tracing.Enable = false
cfgMutex.Unlock()
defer func() {
cfgMutex.Lock()
cfg.Tracing.Enable = origEnable
cfgMutex.Unlock()
}()
app := fiber.New(fiber.Config{DisableStartupMessage: true})
var capturedCtx context.Context
app.Get("/trace-test", func(c *fiber.Ctx) error {
capturedCtx = setupTracing(c)
return c.SendStatus(200)
})
req := httptest.NewRequest("GET", "/trace-test", nil)
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("app.Test error: %v", err)
}
_ = resp.Body.Close()
if capturedCtx == nil {
t.Fatal("setupTracing returned nil context")
}
// Background context has no deadline
if _, hasDeadline := capturedCtx.Deadline(); hasDeadline {
t.Error("expected no deadline on returned context")
}
})
}