deltacast-sdi-recorder/backend/tests/test_main.py

287 lines
9.2 KiB
Python
Raw Normal View History

2026-04-14 09:21:12 -04:00
"""Integration tests for the main FastAPI application."""
import asyncio
import json
import pytest
from unittest.mock import MagicMock, AsyncMock, patch, Mock
from fastapi.testclient import TestClient
from datetime import datetime
# Import after mocking
from app.models import PortStatus, CodecType
@pytest.fixture
def mock_recorder_manager():
"""Create a mock RecorderManager."""
manager = MagicMock()
# Mock get_all_status to return sample port statuses (synchronous)
mock_status = PortStatus(
port_index=0,
is_recording=False,
frame_count=0,
fps=29.97,
bitrate_mbps=50.0,
uptime_seconds=0,
current_file="/recordings/port0.mov",
codec=CodecType.PRORES
)
manager.get_all_status.return_value = [mock_status]
manager.stop_recording = AsyncMock()
return manager
@pytest.fixture
def mock_scte35_manager():
"""Create a mock SCTE35Manager."""
manager = MagicMock()
return manager
@pytest.fixture
def mock_hls_manager():
"""Create a mock HLSPreviewManager."""
manager = MagicMock()
manager.stop_preview = AsyncMock()
return manager
@pytest.fixture
def mock_settings():
"""Create mock Settings."""
settings = MagicMock()
settings.deltacast_port_count = 4
settings.ffmpeg_path = "/usr/bin/ffmpeg"
settings.recording_dir = "/recordings"
return settings
@pytest.fixture
def test_app(mock_recorder_manager, mock_scte35_manager, mock_hls_manager, mock_settings):
"""Create a test FastAPI app with mocked managers."""
# Patch the lifespan and create app without running startup
with patch('app.main.RecorderManager', return_value=mock_recorder_manager), \
patch('app.main.SCTE35Manager', return_value=mock_scte35_manager), \
patch('app.main.HLSPreviewManager', return_value=mock_hls_manager), \
patch('app.main.settings', mock_settings):
from app.main import app
# Manually inject the mocked managers for routes
from app.api import routes as routes_module
routes_module.recorder_manager = mock_recorder_manager
routes_module.scte35_manager = mock_scte35_manager
return app
@pytest.fixture
def client(test_app):
"""Create a TestClient for the test app."""
return TestClient(test_app)
class TestAppInitialization:
"""Tests for app initialization and configuration."""
def test_app_has_health_endpoint(self, client):
"""App responds to GET /api/health"""
response = client.get("/api/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert "ports" in data
assert "recording_count" in data
def test_app_has_ports_endpoint(self, client):
"""App responds to GET /api/ports"""
response = client.get("/api/ports")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
assert "port_index" in data[0]
def test_cors_middleware_enabled(self, test_app):
"""CORS middleware is configured in the app"""
# Check that CORSMiddleware is in the middleware stack
from fastapi.middleware.cors import CORSMiddleware
has_cors = any(
isinstance(middleware.cls, type)
and issubclass(middleware.cls, CORSMiddleware)
for middleware in test_app.user_middleware
)
assert has_cors
def test_hls_endpoint_404_for_missing_file(self, client):
"""GET /hls/nonexistent.m3u8 returns 404"""
response = client.get("/hls/nonexistent.m3u8")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
class TestBroadcastPortStatus:
"""Tests for broadcast_port_status background task."""
@pytest.mark.asyncio
async def test_broadcast_port_status_sends_data(self, mock_recorder_manager, mock_settings):
"""broadcast_port_status sends correct data structure"""
# Mock connection manager to capture broadcast
broadcast_called = asyncio.Event()
broadcast_data = {}
async def mock_broadcast(data):
broadcast_data.update(data)
broadcast_called.set()
# Create the task
from app.main import broadcast_port_status
with patch('app.main.recorder_manager', mock_recorder_manager), \
patch('app.main.connection_manager') as mock_conn_manager:
mock_conn_manager.broadcast = mock_broadcast
# Run broadcast once
task = asyncio.create_task(broadcast_port_status())
# Wait for broadcast
try:
await asyncio.wait_for(broadcast_called.wait(), timeout=2.0)
except asyncio.TimeoutError:
pytest.fail("Broadcast did not complete within timeout")
finally:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# Verify broadcast data structure
assert "type" in broadcast_data
assert broadcast_data["type"] == "port_status"
assert "ports" in broadcast_data
assert isinstance(broadcast_data["ports"], list)
if broadcast_data["ports"]:
assert "port_index" in broadcast_data["ports"][0]
class TestWebSocketIntegration:
"""Tests for WebSocket functionality."""
def test_websocket_endpoint_exists(self, client):
"""WebSocket endpoint is defined in app routes"""
from app.main import app
ws_routes = [
route for route in app.routes
if hasattr(route, 'path') and route.path == "/ws"
]
assert len(ws_routes) > 0
def test_connection_manager_initialization(self):
"""ConnectionManager is initialized with empty connections"""
from app.api.websocket import ConnectionManager
manager = ConnectionManager()
assert manager.active_connections == []
class TestHLSEndpoint:
"""Tests for HLS file serving endpoint."""
def test_hls_endpoint_returns_404_for_missing_file(self, client):
"""HLS endpoint returns 404 for non-existent file"""
response = client.get("/hls/missing-segment.ts")
assert response.status_code == 404
def test_hls_endpoint_handles_various_filenames(self, client):
"""HLS endpoint correctly handles various filename patterns"""
test_files = [
"stream.m3u8",
"segment-001.ts",
"playlist_index.m3u8",
"../../../etc/passwd", # Path traversal attempt
]
for filename in test_files:
response = client.get(f"/hls/{filename}")
# Should all return 404 since files don't exist
assert response.status_code == 404
class TestAPIRouter:
"""Tests to verify API router is properly included."""
def test_api_router_is_included(self, client):
"""API router is included in app"""
response = client.get("/api/health")
assert response.status_code == 200
def test_api_prefix_is_correct(self, client):
"""API routes have /api prefix"""
response = client.get("/api/health")
assert response.status_code == 200
# Routes without /api should not work
response = client.get("/health")
assert response.status_code == 404
class TestErrorHandling:
"""Tests for error handling in the application."""
def test_404_for_undefined_route(self, client):
"""Undefined routes return 404"""
response = client.get("/api/nonexistent")
assert response.status_code == 404
def test_404_error_is_dict(self, client):
"""404 errors return JSON response"""
response = client.get("/api/nonexistent")
assert isinstance(response.json(), dict)
assert "detail" in response.json()
class TestAppMetadata:
"""Tests for app metadata and OpenAPI documentation."""
def test_app_has_title(self, client):
"""App has correct title"""
from app.main import app
assert app.title == "Deltacast SDI Recorder"
def test_app_has_version(self, client):
"""App has version information"""
from app.main import app
assert app.version == "1.0.0"
def test_openapi_schema_available(self, client):
"""OpenAPI schema is available"""
response = client.get("/openapi.json")
assert response.status_code == 200
schema = response.json()
assert "openapi" in schema
assert "paths" in schema
class TestManagerInjection:
"""Tests for proper manager injection into routes module."""
def test_managers_injected_into_routes(self, mock_recorder_manager, mock_scte35_manager):
"""Managers are correctly injected into routes module"""
from app.api import routes as routes_module
# Managers should be set by the fixture
assert routes_module.recorder_manager is not None
assert routes_module.scte35_manager is not None
if __name__ == "__main__":
pytest.main([__file__, "-v"])