The Gin framework uses a global mode setting (gin.SetMode()) which is not thread-safe when tests run in parallel. Removing t.Parallel() from all server tests that use gin.SetMode() prevents data races when running tests with the race detector enabled. All tests now pass with 'make test' which includes -race flag.
287 lines
6.6 KiB
Go
287 lines
6.6 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.dcentral.systems/toolz/goplt/internal/health"
|
|
"git.dcentral.systems/toolz/goplt/internal/metrics"
|
|
"github.com/gin-gonic/gin"
|
|
"go.opentelemetry.io/otel/trace/noop"
|
|
)
|
|
|
|
func TestNewServer(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mockConfig := &mockConfigProvider{
|
|
values: map[string]any{
|
|
"environment": "test",
|
|
"server.port": 8080,
|
|
"server.host": "127.0.0.1",
|
|
"server.read_timeout": "30s",
|
|
"server.write_timeout": "30s",
|
|
},
|
|
}
|
|
|
|
mockLogger := &mockLogger{}
|
|
healthRegistry := health.NewRegistry()
|
|
metricsRegistry := metrics.NewMetrics()
|
|
mockErrorBus := &mockErrorBusServer{}
|
|
tracer := noop.NewTracerProvider()
|
|
|
|
srv, err := NewServer(mockConfig, mockLogger, healthRegistry, metricsRegistry, mockErrorBus, tracer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
if srv == nil {
|
|
t.Fatal("Expected server, got nil")
|
|
}
|
|
|
|
if srv.httpServer == nil {
|
|
t.Error("Expected http server, got nil")
|
|
}
|
|
|
|
if srv.router == nil {
|
|
t.Error("Expected router, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNewServer_DefaultValues(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mockConfig := &mockConfigProvider{
|
|
values: map[string]any{
|
|
"environment": "test",
|
|
},
|
|
}
|
|
|
|
mockLogger := &mockLogger{}
|
|
healthRegistry := health.NewRegistry()
|
|
metricsRegistry := metrics.NewMetrics()
|
|
mockErrorBus := &mockErrorBusServer{}
|
|
tracer := noop.NewTracerProvider()
|
|
|
|
srv, err := NewServer(mockConfig, mockLogger, healthRegistry, metricsRegistry, mockErrorBus, tracer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
if srv.httpServer.Addr != "0.0.0.0:8080" {
|
|
t.Errorf("Expected default address 0.0.0.0:8080, got %s", srv.httpServer.Addr)
|
|
}
|
|
}
|
|
|
|
func TestServer_Router(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mockConfig := &mockConfigProvider{
|
|
values: map[string]any{
|
|
"environment": "test",
|
|
},
|
|
}
|
|
|
|
mockLogger := &mockLogger{}
|
|
healthRegistry := health.NewRegistry()
|
|
metricsRegistry := metrics.NewMetrics()
|
|
mockErrorBus := &mockErrorBusServer{}
|
|
tracer := noop.NewTracerProvider()
|
|
|
|
srv, err := NewServer(mockConfig, mockLogger, healthRegistry, metricsRegistry, mockErrorBus, tracer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
router := srv.Router()
|
|
if router == nil {
|
|
t.Error("Expected router, got nil")
|
|
}
|
|
}
|
|
|
|
func TestServer_Shutdown(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mockConfig := &mockConfigProvider{
|
|
values: map[string]any{
|
|
"environment": "test",
|
|
"server.port": 0, // Use random port
|
|
},
|
|
}
|
|
|
|
mockLogger := &mockLogger{}
|
|
healthRegistry := health.NewRegistry()
|
|
metricsRegistry := metrics.NewMetrics()
|
|
mockErrorBus := &mockErrorBusServer{}
|
|
tracer := noop.NewTracerProvider()
|
|
|
|
srv, err := NewServer(mockConfig, mockLogger, healthRegistry, metricsRegistry, mockErrorBus, tracer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
// Start server in background
|
|
go func() {
|
|
_ = srv.Start()
|
|
}()
|
|
|
|
// Wait a bit for server to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Shutdown
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
t.Errorf("Shutdown failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestServer_HealthEndpoints(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mockConfig := &mockConfigProvider{
|
|
values: map[string]any{
|
|
"environment": "test",
|
|
},
|
|
}
|
|
|
|
mockLogger := &mockLogger{}
|
|
healthRegistry := health.NewRegistry()
|
|
metricsRegistry := metrics.NewMetrics()
|
|
mockErrorBus := &mockErrorBusServer{}
|
|
tracer := noop.NewTracerProvider()
|
|
|
|
srv, err := NewServer(mockConfig, mockLogger, healthRegistry, metricsRegistry, mockErrorBus, tracer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
// Test /healthz endpoint
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200 for /healthz, got %d", w.Code)
|
|
}
|
|
|
|
// Test /ready endpoint
|
|
req = httptest.NewRequest(http.MethodGet, "/ready", nil)
|
|
w = httptest.NewRecorder()
|
|
srv.router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200 for /ready, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestServer_MetricsEndpoint(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
mockConfig := &mockConfigProvider{
|
|
values: map[string]any{
|
|
"environment": "test",
|
|
},
|
|
}
|
|
|
|
mockLogger := &mockLogger{}
|
|
healthRegistry := health.NewRegistry()
|
|
metricsRegistry := metrics.NewMetrics()
|
|
mockErrorBus := &mockErrorBusServer{}
|
|
tracer := noop.NewTracerProvider()
|
|
|
|
srv, err := NewServer(mockConfig, mockLogger, healthRegistry, metricsRegistry, mockErrorBus, tracer)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create server: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200 for /metrics, got %d", w.Code)
|
|
}
|
|
|
|
// Prometheus handler may return empty body if no metrics are recorded yet
|
|
// This is acceptable - we just verify the endpoint works
|
|
_ = w.Body.String()
|
|
}
|
|
|
|
// mockConfigProvider implements config.ConfigProvider for testing.
|
|
type mockConfigProvider struct {
|
|
values map[string]any
|
|
}
|
|
|
|
func (m *mockConfigProvider) Get(key string) any {
|
|
return m.values[key]
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetString(key string) string {
|
|
if val, ok := m.values[key].(string); ok {
|
|
return val
|
|
}
|
|
if val, ok := m.values[key]; ok {
|
|
return val.(string)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetInt(key string) int {
|
|
if val, ok := m.values[key].(int); ok {
|
|
return val
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetBool(key string) bool {
|
|
if val, ok := m.values[key].(bool); ok {
|
|
return val
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetDuration(key string) time.Duration {
|
|
if val, ok := m.values[key].(string); ok {
|
|
dur, err := time.ParseDuration(val)
|
|
if err == nil {
|
|
return dur
|
|
}
|
|
}
|
|
if val, ok := m.values[key].(time.Duration); ok {
|
|
return val
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetStringSlice(key string) []string {
|
|
if val, ok := m.values[key].([]string); ok {
|
|
return val
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockConfigProvider) IsSet(key string) bool {
|
|
_, ok := m.values[key]
|
|
return ok
|
|
}
|
|
|
|
func (m *mockConfigProvider) Unmarshal(v any) error {
|
|
return nil
|
|
}
|
|
|
|
// Note: mockLogger and mockErrorBusMiddleware are defined in middleware_test.go
|
|
// We use mockErrorBusServer here to avoid conflicts
|
|
type mockErrorBusServer struct {
|
|
errors []error
|
|
ctxs []context.Context
|
|
}
|
|
|
|
func (m *mockErrorBusServer) Publish(ctx context.Context, err error) {
|
|
m.errors = append(m.errors, err)
|
|
m.ctxs = append(m.ctxs, ctx)
|
|
}
|