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:
259
internal/server/middleware_test.go
Normal file
259
internal/server/middleware_test.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestRequestIDMiddleware(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(RequestIDMiddleware())
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
requestID, exists := c.Get(string(requestIDKey))
|
||||
if !exists {
|
||||
t.Error("Expected request ID in context")
|
||||
}
|
||||
if requestID == nil || requestID == "" {
|
||||
t.Error("Expected non-empty request ID")
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"request_id": requestID})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Verify X-Request-ID header is set
|
||||
if w.Header().Get("X-Request-ID") == "" {
|
||||
t.Error("Expected X-Request-ID header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestIDMiddleware_ExistingHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(RequestIDMiddleware())
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
req.Header.Set("X-Request-ID", "existing-id")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Header().Get("X-Request-ID") != "existing-id" {
|
||||
t.Errorf("Expected existing request ID, got %s", w.Header().Get("X-Request-ID"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingMiddleware(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
router := gin.New()
|
||||
router.Use(RequestIDMiddleware())
|
||||
router.Use(LoggingMiddleware(mockLogger))
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "test"})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Verify logging was called
|
||||
if len(mockLogger.infoLogs) == 0 {
|
||||
t.Error("Expected info log to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPanicRecoveryMiddleware(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
mockErrorBus := &mockErrorBusMiddleware{}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(PanicRecoveryMiddleware(mockErrorBus))
|
||||
|
||||
router.GET("/panic", func(c *gin.Context) {
|
||||
panic("test panic")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/panic", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("Expected status 500, got %d", w.Code)
|
||||
}
|
||||
|
||||
// Verify error was published to error bus
|
||||
if len(mockErrorBus.errors) == 0 {
|
||||
t.Error("Expected error to be published to error bus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPanicRecoveryMiddleware_ErrorPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
mockErrorBus := &mockErrorBusMiddleware{}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(PanicRecoveryMiddleware(mockErrorBus))
|
||||
|
||||
router.GET("/panic-error", func(c *gin.Context) {
|
||||
panic(errors.New("test error"))
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/panic-error", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("Expected status 500, got %d", w.Code)
|
||||
}
|
||||
|
||||
if len(mockErrorBus.errors) == 0 {
|
||||
t.Error("Expected error to be published to error bus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(CORSMiddleware())
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "test"})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Verify CORS headers
|
||||
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
|
||||
t.Error("Expected CORS header Access-Control-Allow-Origin")
|
||||
}
|
||||
|
||||
if w.Header().Get("Access-Control-Allow-Credentials") != "true" {
|
||||
t.Error("Expected CORS header Access-Control-Allow-Credentials")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware_OPTIONS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(CORSMiddleware())
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "test"})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodOptions, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Errorf("Expected status 204 for OPTIONS, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutMiddleware(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(TimeoutMiddleware(100 * time.Millisecond))
|
||||
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "test"})
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// mockLogger implements logger.Logger for testing.
|
||||
type mockLogger struct {
|
||||
infoLogs []string
|
||||
errors []string
|
||||
}
|
||||
|
||||
func (m *mockLogger) Debug(msg string, fields ...logger.Field) {}
|
||||
func (m *mockLogger) Info(msg string, fields ...logger.Field) {
|
||||
m.infoLogs = append(m.infoLogs, msg)
|
||||
}
|
||||
func (m *mockLogger) Warn(msg string, fields ...logger.Field) {}
|
||||
func (m *mockLogger) Error(msg string, fields ...logger.Field) {
|
||||
m.errors = append(m.errors, msg)
|
||||
}
|
||||
func (m *mockLogger) With(fields ...logger.Field) logger.Logger {
|
||||
return m
|
||||
}
|
||||
func (m *mockLogger) WithContext(ctx context.Context) logger.Logger {
|
||||
return m
|
||||
}
|
||||
|
||||
// mockErrorBusMiddleware implements errorbus.ErrorPublisher for testing middleware.
|
||||
type mockErrorBusMiddleware struct {
|
||||
errors []error
|
||||
ctxs []context.Context
|
||||
}
|
||||
|
||||
func (m *mockErrorBusMiddleware) 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