Files
goplt/internal/server/server_test.go
0x1d 5fdbb729bd
Some checks failed
CI / Test (pull_request) Failing after 17s
CI / Lint (pull_request) Failing after 18s
CI / Build (pull_request) Successful in 12s
CI / Format Check (pull_request) Successful in 2s
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
2025-11-05 21:05:36 +01:00

291 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) {
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)
}