package tests import ( "bytes" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/api" "veza-backend-api/internal/config" "veza-backend-api/internal/database" "veza-backend-api/internal/metrics" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // Helper function to create a test Gin engine with routes set up func setupTestRouter(t *testing.T) (*gin.Engine, func()) { gin.SetMode(gin.TestMode) router := gin.New() logger := zaptest.NewLogger(t) // Create a minimal mock *gorm.DB instance // This avoids AutoMigrate failures with PostgreSQL-specific DDL in SQLite. // We're essentially mocking the DB connection for the routes that don't need real persistence. mockGormDB, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) assert.NoError(t, err) mockDB := &database.Database{ GormDB: mockGormDB, Logger: logger, } // Mock Config // Note: Pass nil for RedisClient and RabbitMQEventBus to avoid connection attempts // Health checks will handle nil gracefully (return "error" status but don't block) mockConfig := &config.Config{ AppPort: 8080, CORSOrigins: []string{"*"}, JWTSecret: "test-secret", UploadDir: "uploads/test", StreamServerURL: "http://localhost:8000", StreamServerInternalAPIKey: "test-internal-api-key", Database: mockDB, // Corrected from testDB Logger: logger, // Pass the logger to the config RedisClient: nil, // nil = not configured, health checks handle this gracefully RabbitMQEventBus: nil, // nil = not configured, health checks handle this gracefully ErrorMetrics: metrics.NewErrorMetrics(), // Initialize ErrorMetrics HandlerTimeout: 30 * time.Second, // Set reasonable timeout for tests } apiRouter := api.NewAPIRouter(mockDB, mockConfig) apiRouter.Setup(router) cleanup := func() { // No specific cleanup needed for in-memory SQLite in this setup // GORM closes the DB when the gormDB object is garbage collected. } return router, cleanup } func TestPublicCoreRoutes(t *testing.T) { // MetricsProtection middleware (added in 7b2f87373) reads METRICS_BEARER_TOKEN // at construction time. Set it before setupTestRouter so the protected // /metrics, /metrics/aggregated, /system/metrics routes are reachable in tests // when the request carries the matching bearer header. const metricsToken = "test-metrics-token" t.Setenv("METRICS_BEARER_TOKEN", metricsToken) router, cleanup := setupTestRouter(t) defer cleanup() // Define test cases for public core routes testCases := []struct { name string method string legacyPath string modernPath string expectedStatus int expectDeprecatedHeader bool needsMetricsAuth bool }{ { name: "Health Check", method: http.MethodGet, legacyPath: "/health", modernPath: "/api/v1/health", expectedStatus: http.StatusOK, expectDeprecatedHeader: true, }, { name: "Liveness Check", method: http.MethodGet, legacyPath: "/healthz", modernPath: "/api/v1/healthz", expectedStatus: http.StatusOK, expectDeprecatedHeader: true, }, { name: "Readiness Check", method: http.MethodGet, legacyPath: "/readyz", modernPath: "/api/v1/readyz", expectedStatus: http.StatusOK, expectDeprecatedHeader: true, }, // Metrics endpoints are protected by MetricsProtection middleware. // We pass a bearer token to verify they're reachable when authenticated. { name: "Metrics", method: http.MethodGet, legacyPath: "/metrics", modernPath: "/api/v1/metrics", expectedStatus: http.StatusOK, expectDeprecatedHeader: true, needsMetricsAuth: true, }, { name: "Aggregated Metrics", method: http.MethodGet, legacyPath: "/metrics/aggregated", modernPath: "/api/v1/metrics/aggregated", expectedStatus: http.StatusOK, expectDeprecatedHeader: true, needsMetricsAuth: true, }, { name: "System Metrics", method: http.MethodGet, legacyPath: "/system/metrics", modernPath: "/api/v1/system/metrics", expectedStatus: http.StatusOK, expectDeprecatedHeader: true, needsMetricsAuth: true, }, } for _, tc := range testCases { t.Run("Legacy "+tc.name, func(t *testing.T) { req, _ := http.NewRequest(tc.method, tc.legacyPath, nil) if tc.needsMetricsAuth { req.Header.Set("Authorization", "Bearer "+metricsToken) } w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tc.expectedStatus, w.Code) if tc.expectDeprecatedHeader { assert.Contains(t, w.Header().Get("Deprecated"), "true") } }) t.Run("Modern "+tc.name, func(t *testing.T) { req, _ := http.NewRequest(tc.method, tc.modernPath, nil) if tc.needsMetricsAuth { req.Header.Set("Authorization", "Bearer "+metricsToken) } w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tc.expectedStatus, w.Code) assert.NotContains(t, w.Header().Get("Deprecated"), "true") // Modern routes should NOT be deprecated }) } } func TestInternalTrackStreamCallbackRoutes(t *testing.T) { router, cleanup := setupTestRouter(t) defer cleanup() // Test case for internal track stream callback // Note: The handler requires a valid JSON body with "status" field (oneof: completed, failed, processing) // Sending {} will result in 400 BadRequest due to validation failure testCases := []struct { name string method string legacyPath string modernPath string body string expectedStatus int expectDeprecatedHeader bool }{ { name: "Track Stream Ready Callback - Invalid JSON", method: http.MethodPost, legacyPath: "", // v0.941: legacy removed modernPath: "/api/v1/internal/tracks/123e4567-e89b-12d3-a456-426614174000/stream-ready", body: "{}", // Missing required "status" field expectedStatus: http.StatusBadRequest, // 400 because validation fails (status field required) expectDeprecatedHeader: false, }, { name: "Track Stream Ready Callback - Valid JSON", method: http.MethodPost, legacyPath: "", // v0.941: legacy removed modernPath: "/api/v1/internal/tracks/123e4567-e89b-12d3-a456-426614174000/stream-ready", body: `{"status": "completed"}`, // Valid JSON with required status expectedStatus: http.StatusInternalServerError, // 500 because track doesn't exist in test DB (UpdateStreamStatus fails with "no such table: tracks") expectDeprecatedHeader: false, }, } apiKey := "test-internal-api-key" for _, tc := range testCases { if tc.legacyPath != "" { t.Run("Legacy "+tc.name, func(t *testing.T) { req, _ := http.NewRequest(tc.method, tc.legacyPath, bytes.NewBufferString(tc.body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Internal-API-Key", apiKey) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tc.expectedStatus, w.Code) }) } t.Run("Modern "+tc.name, func(t *testing.T) { req, _ := http.NewRequest(tc.method, tc.modernPath, bytes.NewBufferString(tc.body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Internal-API-Key", apiKey) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tc.expectedStatus, w.Code) }) } // A01/A04: Reject stream-ready without X-Internal-API-Key (v0.941: only modern path exists) t.Run("Modern stream-ready without API key returns 401", func(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, "/api/v1/internal/tracks/123e4567-e89b-12d3-a456-426614174000/stream-ready", bytes.NewBufferString(`{"status": "completed"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) }) }