test: add comprehensive tests for all Epic 1 stories
Story 1.2: Database Layer - Test database client creation, connection, ping, and close - Test connection pooling configuration - Tests skip if database is not available (short mode) Story 1.3: Health Monitoring and Metrics - Test health registry registration and checking - Test database health checker - Test liveness and readiness checks - Test metrics creation, middleware, and handler - Test Prometheus metrics endpoint Story 1.4: Error Handling and Error Bus - Test channel-based error bus creation - Test error publishing with context - Test nil error handling - Test channel full scenario - Test graceful shutdown - Fix Close() method to handle multiple calls safely Story 1.5: HTTP Server and Middleware - Test server creation with all middleware - Test request ID middleware - Test logging middleware - Test panic recovery middleware - Test CORS middleware - Test timeout middleware - Test health and metrics endpoints - Test server shutdown Story 1.6: OpenTelemetry Tracing - Test tracer initialization (enabled/disabled) - Test development and production modes - Test OTLP exporter configuration - Test graceful shutdown - Test no-op tracer provider All tests follow Go testing best practices: - Table-driven tests where appropriate - Parallel execution - Proper mocking of interfaces - Skip tests requiring external dependencies in short mode
This commit is contained in:
290
internal/server/server_test.go
Normal file
290
internal/server/server_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user