Files
graphql-monitoring-proxy/api_health_test.go
T
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

257 lines
7.4 KiB
Go

package main
import (
"encoding/json"
"io"
"net/http/httptest"
"testing"
fiber "github.com/gofiber/fiber/v2"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
)
// ---- helpers ---------------------------------------------------------------
func setupMinimalCfg(t *testing.T) {
t.Helper()
logger := libpack_logger.New()
monitoring := libpack_monitoring.NewMonitoring(&libpack_monitoring.InitConfig{})
cfg = &config{
Logger: logger,
Monitoring: monitoring,
}
}
func newHealthApp(t *testing.T) *fiber.App {
t.Helper()
app := fiber.New(fiber.Config{
// suppress stack-trace noise in test output
})
app.Get("/api/backend/health", apiBackendHealth)
app.Get("/api/pool/health", apiConnectionPoolHealth)
app.Get("/api/circuit-breaker/health", apiCircuitBreakerHealth)
return app
}
// ---- apiBackendHealth ------------------------------------------------------
func TestApiBackendHealth_NilManager_Returns503(t *testing.T) {
// Ensure global manager is nil for this test.
orig := backendHealthManager
backendHealthManager = nil
defer func() { backendHealthManager = orig }()
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/backend/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 503, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "unknown", body["status"])
assert.NotEmpty(t, body["message"])
}
func TestApiBackendHealth_HealthyManager_Returns200(t *testing.T) {
orig := backendHealthManager
defer func() { backendHealthManager = orig }()
// inject a healthy manager directly (bypassing sync.Once)
mgr := NewBackendHealthManager(&fasthttp.Client{}, "http://localhost:8080", libpack_logger.New())
mgr.isHealthy.Store(true)
backendHealthManager = mgr
setupMinimalCfg(t)
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/backend/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "healthy", body["status"])
assert.NotNil(t, body["backend_url"])
assert.NotNil(t, body["consecutive_failures"])
assert.NotNil(t, body["check_interval"])
}
func TestApiBackendHealth_UnhealthyManager_Returns503(t *testing.T) {
orig := backendHealthManager
defer func() { backendHealthManager = orig }()
mgr := NewBackendHealthManager(&fasthttp.Client{}, "http://localhost:8080", libpack_logger.New())
mgr.isHealthy.Store(false)
backendHealthManager = mgr
setupMinimalCfg(t)
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/backend/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 503, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "unhealthy", body["status"])
}
// ---- apiConnectionPoolHealth -----------------------------------------------
func TestApiConnectionPoolHealth_NilManager_Returns503(t *testing.T) {
connectionPoolMutex.Lock()
orig := connectionPoolManager
connectionPoolManager = nil
connectionPoolMutex.Unlock()
defer func() {
connectionPoolMutex.Lock()
connectionPoolManager = orig
connectionPoolMutex.Unlock()
}()
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/pool/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 503, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "unknown", body["status"])
assert.NotEmpty(t, body["message"])
}
func TestApiConnectionPoolHealth_HealthyPool_Returns200(t *testing.T) {
connectionPoolMutex.Lock()
orig := connectionPoolManager
mgr := NewConnectionPoolManager(&fasthttp.Client{})
connectionPoolManager = mgr
connectionPoolMutex.Unlock()
defer func() {
connectionPoolMutex.Lock()
_ = mgr.Shutdown()
connectionPoolManager = orig
connectionPoolMutex.Unlock()
}()
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/pool/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "healthy", body["status"])
assert.NotNil(t, body["active_connections"])
assert.NotNil(t, body["total_connections"])
assert.NotNil(t, body["connection_failures"])
}
func TestApiConnectionPoolHealth_DegradedPool_Returns200WithDegradedStatus(t *testing.T) {
connectionPoolMutex.Lock()
orig := connectionPoolManager
mgr := NewConnectionPoolManager(&fasthttp.Client{})
// push failure counter above threshold (10)
for range 15 {
mgr.connectionFailures.Add(1)
}
connectionPoolManager = mgr
connectionPoolMutex.Unlock()
defer func() {
connectionPoolMutex.Lock()
_ = mgr.Shutdown()
connectionPoolManager = orig
connectionPoolMutex.Unlock()
}()
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/pool/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
// handler returns 200 even for degraded
assert.Equal(t, 200, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "degraded", body["status"])
}
// ---- apiCircuitBreakerHealth -----------------------------------------------
func TestApiCircuitBreakerHealth_NilCB_Returns503(t *testing.T) {
cbMutex.Lock()
origCB := cb
cb = nil
cbMutex.Unlock()
defer func() {
cbMutex.Lock()
cb = origCB
cbMutex.Unlock()
}()
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/circuit-breaker/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 503, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "disabled", body["status"])
assert.NotEmpty(t, body["message"])
}
func TestApiCircuitBreakerHealth_ClosedCB_Returns200Healthy(t *testing.T) {
cbMutex.Lock()
origCB := cb
cbMutex.Unlock()
defer func() {
cbMutex.Lock()
cb = origCB
cbMutex.Unlock()
}()
logger := libpack_logger.New()
monitoring := libpack_monitoring.NewMonitoring(&libpack_monitoring.InitConfig{})
cfg = &config{Logger: logger, Monitoring: monitoring}
cfg.CircuitBreaker.Enable = true
cfg.CircuitBreaker.MaxFailures = 5
cfg.CircuitBreaker.Timeout = 30
initCircuitBreaker(cfg)
// cb is now set by initCircuitBreaker; circuit starts closed (healthy)
app := newHealthApp(t)
req := httptest.NewRequest("GET", "/api/circuit-breaker/health", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
var body map[string]any
raw, _ := io.ReadAll(resp.Body)
assert.NoError(t, json.Unmarshal(raw, &body))
assert.Equal(t, "healthy", body["status"])
assert.NotNil(t, body["state"])
assert.NotNil(t, body["counts"])
assert.NotNil(t, body["configuration"])
counts, ok := body["counts"].(map[string]any)
assert.True(t, ok)
assert.NotNil(t, counts["requests"])
assert.NotNil(t, counts["total_successes"])
assert.NotNil(t, counts["total_failures"])
assert.NotNil(t, counts["consecutive_successes"])
assert.NotNil(t, counts["consecutive_failures"])
}