mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-15 05:58:33 +00:00
On signal, main waited on wg for StartHTTPProxy's server.Listen, which never returns because nothing called server.Shutdown(); it timed out after 10s and process-exited, abruptly killing in-flight GraphQL requests. Expose the proxy Fiber app and register an http-proxy Shutdown component so server.Listen returns after Shutdown, letting connections drain. Guard the shared app handle with a mutex (written by the StartHTTPProxy goroutine, read by the shutdown component). Verified with build, vet, full test and full race suite.
663 lines
18 KiB
Go
663 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AddRequestUUID
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
|
|
app := fiber.New()
|
|
app.Use(AddRequestUUID)
|
|
|
|
var captured string
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
if v, ok := c.Locals("request_uuid").(string); ok {
|
|
captured = v
|
|
}
|
|
return c.SendStatus(200)
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("want 200, got %d", resp.StatusCode)
|
|
}
|
|
if captured == "" {
|
|
t.Fatal("request_uuid not set in Locals")
|
|
}
|
|
// UUIDs are 36 chars (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
|
if len(captured) != 36 {
|
|
t.Errorf("unexpected UUID length: %q", captured)
|
|
}
|
|
}
|
|
|
|
func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
|
|
app := fiber.New()
|
|
app.Use(AddRequestUUID)
|
|
|
|
seen := make([]string, 0, 5)
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
if v, ok := c.Locals("request_uuid").(string); ok {
|
|
seen = append(seen, v)
|
|
}
|
|
return c.SendStatus(200)
|
|
})
|
|
|
|
for i := range 5 {
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
|
|
if err != nil {
|
|
t.Fatalf("request %d: %v", i, err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
}
|
|
|
|
set := make(map[string]struct{}, len(seen))
|
|
for _, id := range seen {
|
|
set[id] = struct{}{}
|
|
}
|
|
if len(set) != 5 {
|
|
t.Errorf("expected 5 unique UUIDs, got %d unique in %v", len(set), seen)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// healthCheck
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHealthCheck_Returns200WithJSON(t *testing.T) {
|
|
// Ensure cfg is ready and GraphQL check is disabled via query param
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
app := fiber.New()
|
|
app.Get("/health", healthCheck)
|
|
|
|
// Pass check_graphql=false to avoid real network call
|
|
req := httptest.NewRequest("GET", "/health?check_graphql=false&check_redis=false", nil)
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("want 200, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]any
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
|
|
if _, ok := body["status"]; !ok {
|
|
t.Error("response missing 'status' field")
|
|
}
|
|
if _, ok := body["timestamp"]; !ok {
|
|
t.Error("response missing 'timestamp' field")
|
|
}
|
|
if body["status"] != "healthy" {
|
|
t.Errorf("want status=healthy, got %v", body["status"])
|
|
}
|
|
}
|
|
|
|
func TestHealthCheck_UnhealthyWhenGraphQLDown(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
// Point to a server that refuses connections
|
|
cfgMutex.Lock()
|
|
origHost := cfg.Server.HostGraphQL
|
|
cfg.Server.HostGraphQL = "http://127.0.0.1:1" // port 1 always refused
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Server.HostGraphQL = origHost
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
app.Get("/health", healthCheck)
|
|
|
|
req := httptest.NewRequest("GET", "/health?check_redis=false", nil)
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 15000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Should return 503 when backend is unreachable
|
|
if resp.StatusCode != fiber.StatusServiceUnavailable {
|
|
t.Fatalf("want 503, got %d", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]any
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if body["status"] != "unhealthy" {
|
|
t.Errorf("want unhealthy, got %v", body["status"])
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// processGraphQLRequest
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestProcessGraphQLRequest_ValidBodyProxiesToBackend(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte(`{"data":{"test":"ok"}}`))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
cfgMutex.Lock()
|
|
origHost := cfg.Server.HostGraphQL
|
|
origHostRO := cfg.Server.HostGraphQLReadOnly
|
|
origCache := cfg.Cache.CacheEnable
|
|
cfg.Server.HostGraphQL = backend.URL
|
|
cfg.Server.HostGraphQLReadOnly = backend.URL
|
|
cfg.Cache.CacheEnable = false
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Server.HostGraphQL = origHost
|
|
cfg.Server.HostGraphQLReadOnly = origHostRO
|
|
cfg.Cache.CacheEnable = origCache
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
app.Post("/*", processGraphQLRequest)
|
|
|
|
body := `{"query":"query { __typename }"}`
|
|
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Errorf("want 200, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestProcessGraphQLRequest_MalformedBodyStillHandled(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
// Backend that always returns 200 (malformed body is handled by proxy layer)
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte(`{"errors":[{"message":"parse error"}]}`))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
cfgMutex.Lock()
|
|
origHost := cfg.Server.HostGraphQL
|
|
origHostRO := cfg.Server.HostGraphQLReadOnly
|
|
origCache := cfg.Cache.CacheEnable
|
|
cfg.Server.HostGraphQL = backend.URL
|
|
cfg.Server.HostGraphQLReadOnly = backend.URL
|
|
cfg.Cache.CacheEnable = false
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Server.HostGraphQL = origHost
|
|
cfg.Server.HostGraphQLReadOnly = origHostRO
|
|
cfg.Cache.CacheEnable = origCache
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
app.Post("/*", processGraphQLRequest)
|
|
|
|
// Not valid JSON — proxy should still forward or return gracefully
|
|
body := `not-json-at-all`
|
|
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Should not panic; any 2xx or 5xx is acceptable — just must not crash
|
|
if resp.StatusCode < 100 || resp.StatusCode > 599 {
|
|
t.Errorf("unexpected status %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handleCaching — wasCached=true path (cache hit)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
// Enable in-memory cache
|
|
libpack_cache.EnableCache(&libpack_cache.CacheConfig{
|
|
Logger: cfg.Logger,
|
|
TTL: 60,
|
|
})
|
|
libpack_cache.CacheClear()
|
|
|
|
cfgMutex.Lock()
|
|
origEnable := cfg.Cache.CacheEnable
|
|
cfg.Cache.CacheEnable = true
|
|
cfg.Cache.CacheTTL = 60
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Cache.CacheEnable = origEnable
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte(`{"data":{"users":[]}}`))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
cfgMutex.Lock()
|
|
origHost := cfg.Server.HostGraphQL
|
|
origHostRO := cfg.Server.HostGraphQLReadOnly
|
|
cfg.Server.HostGraphQL = backend.URL
|
|
cfg.Server.HostGraphQLReadOnly = backend.URL
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Server.HostGraphQL = origHost
|
|
cfg.Server.HostGraphQLReadOnly = origHostRO
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
app.Post("/*", processGraphQLRequest)
|
|
|
|
queryBody := `{"query":"query { users { id } }"}`
|
|
|
|
// First request — cache miss, hits backend
|
|
req1 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
|
req1.Header.Set("Content-Type", "application/json")
|
|
resp1, err := app.Test(req1, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("first request: %v", err)
|
|
}
|
|
_ = resp1.Body.Close()
|
|
|
|
if resp1.StatusCode != 200 {
|
|
t.Fatalf("first request want 200, got %d", resp1.StatusCode)
|
|
}
|
|
|
|
// Second identical request — should hit cache
|
|
req2 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
resp2, err := app.Test(req2, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("second request: %v", err)
|
|
}
|
|
_ = resp2.Body.Close()
|
|
|
|
if resp2.StatusCode != 200 {
|
|
t.Fatalf("second request want 200, got %d", resp2.StatusCode)
|
|
}
|
|
if resp2.Header.Get("X-Cache-Hit") != "true" {
|
|
t.Error("second request should have X-Cache-Hit: true header")
|
|
}
|
|
}
|
|
|
|
func TestHandleCaching_CacheMissProxiesRequest(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
libpack_cache.EnableCache(&libpack_cache.CacheConfig{
|
|
Logger: cfg.Logger,
|
|
TTL: 60,
|
|
})
|
|
libpack_cache.CacheClear()
|
|
|
|
cfgMutex.Lock()
|
|
origEnable := cfg.Cache.CacheEnable
|
|
cfg.Cache.CacheEnable = true
|
|
cfg.Cache.CacheTTL = 60
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Cache.CacheEnable = origEnable
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
backendCalled := 0
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
backendCalled++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
_, _ = fmt.Fprintf(w, `{"data":{"call":%d}}`, backendCalled)
|
|
}))
|
|
defer backend.Close()
|
|
|
|
cfgMutex.Lock()
|
|
origHost := cfg.Server.HostGraphQL
|
|
origHostRO := cfg.Server.HostGraphQLReadOnly
|
|
cfg.Server.HostGraphQL = backend.URL
|
|
cfg.Server.HostGraphQLReadOnly = backend.URL
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Server.HostGraphQL = origHost
|
|
cfg.Server.HostGraphQLReadOnly = origHostRO
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
app.Post("/*", processGraphQLRequest)
|
|
|
|
// Unique query so no prior cache entry
|
|
queryBody := `{"query":"query { uniqueMissTest_12345 { id } }"}`
|
|
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
_ = resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Errorf("want 200, got %d", resp.StatusCode)
|
|
}
|
|
if resp.Header.Get("X-Cache-Hit") == "true" {
|
|
t.Error("first request should not be a cache hit")
|
|
}
|
|
if backendCalled == 0 {
|
|
t.Error("backend should have been called on cache miss")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handleCaching — direct unit test for wasCached=true branch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandleCaching_DirectCacheHitBranch(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
libpack_cache.EnableCache(&libpack_cache.CacheConfig{
|
|
Logger: cfg.Logger,
|
|
TTL: 60,
|
|
})
|
|
libpack_cache.CacheClear()
|
|
|
|
cfgMutex.Lock()
|
|
origEnable := cfg.Cache.CacheEnable
|
|
cfg.Cache.CacheEnable = true
|
|
cfg.Cache.CacheTTL = 60
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Cache.CacheEnable = origEnable
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
|
|
var wasCachedResult bool
|
|
app.Post("/test", func(c fiber.Ctx) error {
|
|
parsedResult := &parseGraphQLQueryResult{
|
|
cacheTime: 60,
|
|
cacheRequest: true,
|
|
activeEndpoint: cfg.Server.HostGraphQL,
|
|
}
|
|
|
|
// Pre-populate the cache so lookup hits
|
|
cacheKey := libpack_cache.CalculateHash(c, "-", "-")
|
|
libpack_cache.CacheStore(cacheKey, []byte(`{"data":{"cached":true}}`))
|
|
|
|
var err error
|
|
wasCachedResult, err = handleCaching(c, parsedResult, "-", "-")
|
|
return err
|
|
})
|
|
|
|
reqCtx := &fasthttp.RequestCtx{}
|
|
reqCtx.Request.SetRequestURI("/test")
|
|
reqCtx.Request.Header.SetMethod("POST")
|
|
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
|
reqCtx.Request.SetBody([]byte(`{"query":"query { cachedQuery }"}`))
|
|
|
|
ctx := app.AcquireCtx(reqCtx)
|
|
defer app.ReleaseCtx(ctx)
|
|
|
|
parsedResult := &parseGraphQLQueryResult{
|
|
cacheTime: 60,
|
|
cacheRequest: true,
|
|
activeEndpoint: cfg.Server.HostGraphQL,
|
|
}
|
|
|
|
cacheKey := libpack_cache.CalculateHash(ctx, "-", "-")
|
|
libpack_cache.CacheStore(cacheKey, []byte(`{"data":{"cached":true}}`))
|
|
|
|
wasCached, err := handleCaching(ctx, parsedResult, "-", "-")
|
|
if err != nil {
|
|
t.Fatalf("handleCaching returned error: %v", err)
|
|
}
|
|
if !wasCached {
|
|
t.Error("expected wasCached=true when cache hit")
|
|
}
|
|
_ = wasCachedResult
|
|
}
|
|
|
|
func TestHandleCaching_NoCacheEnabled_ProxiesDirect(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write([]byte(`{"data":{"noCacheTest":true}}`))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
cfgMutex.Lock()
|
|
origEnable := cfg.Cache.CacheEnable
|
|
origRedis := cfg.Cache.CacheRedisEnable
|
|
origHost := cfg.Server.HostGraphQL
|
|
origHostRO := cfg.Server.HostGraphQLReadOnly
|
|
cfg.Cache.CacheEnable = false
|
|
cfg.Cache.CacheRedisEnable = false
|
|
cfg.Server.HostGraphQL = backend.URL
|
|
cfg.Server.HostGraphQLReadOnly = backend.URL
|
|
cfgMutex.Unlock()
|
|
defer func() {
|
|
cfgMutex.Lock()
|
|
cfg.Cache.CacheEnable = origEnable
|
|
cfg.Cache.CacheRedisEnable = origRedis
|
|
cfg.Server.HostGraphQL = origHost
|
|
cfg.Server.HostGraphQLReadOnly = origHostRO
|
|
cfgMutex.Unlock()
|
|
}()
|
|
|
|
app := fiber.New()
|
|
|
|
reqCtx := &fasthttp.RequestCtx{}
|
|
reqCtx.Request.SetRequestURI("/v1/graphql")
|
|
reqCtx.Request.Header.SetMethod("POST")
|
|
reqCtx.Request.Header.Set("Content-Type", "application/json")
|
|
reqCtx.Request.SetBody([]byte(`{"query":"query { noCacheTest }"}`))
|
|
|
|
fCtx := app.AcquireCtx(reqCtx)
|
|
defer app.ReleaseCtx(fCtx)
|
|
|
|
parsedResult := &parseGraphQLQueryResult{
|
|
cacheRequest: false,
|
|
cacheTime: 0,
|
|
activeEndpoint: backend.URL,
|
|
}
|
|
|
|
wasCached, err := handleCaching(fCtx, parsedResult, "-", "-")
|
|
if err != nil {
|
|
t.Fatalf("handleCaching error: %v", err)
|
|
}
|
|
if wasCached {
|
|
t.Error("expected wasCached=false when cache disabled")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// StartHTTPProxy — starts then shuts down cleanly
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStartHTTPProxy_StartsAndShutdown(t *testing.T) {
|
|
parseConfig()
|
|
_ = StartMonitoringServer()
|
|
|
|
// Grab a free port
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("net.Listen: %v", err)
|
|
}
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
_ = l.Close()
|
|
|
|
cfgMutex.Lock()
|
|
origPort := cfg.Server.PortGraphQL
|
|
origTimeout := cfg.Client.ClientTimeout
|
|
origWS := cfg.WebSocket.Enable
|
|
origAdmin := cfg.AdminDashboard.Enable
|
|
cfg.Server.PortGraphQL = port
|
|
cfg.Client.ClientTimeout = 5
|
|
cfg.WebSocket.Enable = false
|
|
cfg.AdminDashboard.Enable = false
|
|
cfgMutex.Unlock()
|
|
|
|
t.Cleanup(func() {
|
|
cfgMutex.Lock()
|
|
cfg.Server.PortGraphQL = origPort
|
|
cfg.Client.ClientTimeout = origTimeout
|
|
cfg.WebSocket.Enable = origWS
|
|
cfg.AdminDashboard.Enable = origAdmin
|
|
cfgMutex.Unlock()
|
|
})
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
errCh <- StartHTTPProxy()
|
|
}()
|
|
|
|
// Wait for server to bind
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
var conn net.Conn
|
|
for time.Now().Before(deadline) {
|
|
conn, err = net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond)
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
if conn == nil {
|
|
t.Fatalf("server did not start on port %d within 3s", port)
|
|
}
|
|
_ = conn.Close()
|
|
|
|
// Send a health check to confirm it's serving
|
|
httpResp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/health?check_graphql=false&check_redis=false", port))
|
|
if err != nil {
|
|
t.Fatalf("GET /health: %v", err)
|
|
}
|
|
_ = httpResp.Body.Close()
|
|
if httpResp.StatusCode != 200 {
|
|
t.Errorf("want 200, got %d", httpResp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// Test_shutdownHTTPProxy verifies the graceful-shutdown helper: it is a no-op
|
|
// when no server is running, and it stops a listening proxy server so that
|
|
// server.Listen returns (enabling a clean in-flight-request drain instead of
|
|
// process-exit cutoff).
|
|
func (suite *Tests) Test_shutdownHTTPProxy() {
|
|
// No server running: must be a safe no-op.
|
|
httpProxyMu.Lock()
|
|
original := httpProxyApp
|
|
httpProxyApp = nil
|
|
httpProxyMu.Unlock()
|
|
defer func() {
|
|
httpProxyMu.Lock()
|
|
httpProxyApp = original
|
|
httpProxyMu.Unlock()
|
|
}()
|
|
suite.NoError(shutdownHTTPProxy(context.Background()))
|
|
|
|
// With a listening server assigned, shutdown must stop it.
|
|
app := fiber.New()
|
|
app.Get("/ping", func(c fiber.Ctx) error { return c.SendStatus(200) })
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
suite.Require().NoError(err)
|
|
|
|
listenDone := make(chan struct{})
|
|
go func() {
|
|
defer close(listenDone)
|
|
_ = app.Listener(ln)
|
|
}()
|
|
|
|
// Wait until the listener is actually serving (poll the port) before
|
|
// shutting down; otherwise Shutdown can race server startup and no-op.
|
|
addr := ln.Addr().String()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for {
|
|
conn, err := net.Dial("tcp", addr)
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
suite.Fail("proxy server never started listening on " + addr)
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
httpProxyMu.Lock()
|
|
httpProxyApp = app
|
|
httpProxyMu.Unlock()
|
|
suite.NoError(shutdownHTTPProxy(context.Background()))
|
|
|
|
select {
|
|
case <-listenDone:
|
|
// Listen returned because we called Shutdown.
|
|
case <-time.After(3 * time.Second):
|
|
suite.Fail("proxy server did not shut down; in-flight requests would be cut off")
|
|
}
|
|
}
|