Commit 73eca4f6a wrapped /metrics, /metrics/aggregated and /system/metrics
behind a new MetricsProtection middleware. Without auth they return 403,
which broke the 6 metrics sub-tests. The middleware reads
METRICS_BEARER_TOKEN at construction time, so set it via t.Setenv before
calling setupTestRouter, and add a needsMetricsAuth flag on the test
case so the request carries the matching Authorization header.
241 lines
8.6 KiB
Go
241 lines
8.6 KiB
Go
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)
|
|
})
|
|
}
|