veza/veza-backend-api/tests/api_routes_integration_test.go
senke 7e015f8e73
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Stream Server CI / test (push) Failing after 0s
chore(release): v0.941 — Cleanup (dead code, migrations dedup, deprecated routes)
2026-03-02 19:04:30 +01:00

224 lines
7.9 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) {
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
}{
{
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 might return different body content due to dynamic nature,
// so we primarily check status code.
{
name: "Metrics",
method: http.MethodGet,
legacyPath: "/metrics",
modernPath: "/api/v1/metrics",
expectedStatus: http.StatusOK,
expectDeprecatedHeader: true,
},
{
name: "Aggregated Metrics",
method: http.MethodGet,
legacyPath: "/metrics/aggregated",
modernPath: "/api/v1/metrics/aggregated",
expectedStatus: http.StatusOK,
expectDeprecatedHeader: true,
},
{
name: "System Metrics",
method: http.MethodGet,
legacyPath: "/system/metrics",
modernPath: "/api/v1/system/metrics",
expectedStatus: http.StatusOK,
expectDeprecatedHeader: true,
},
}
for _, tc := range testCases {
t.Run("Legacy "+tc.name, func(t *testing.T) {
req, _ := http.NewRequest(tc.method, tc.legacyPath, nil)
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)
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)
})
}