""" Pytest configuration and fixtures for Ohm Stream Downloader tests """ import pytest import asyncio import tempfile import shutil from pathlib import Path from datetime import datetime from unittest.mock import Mock, AsyncMock, patch import sys import os import sys import os # Ensure the project root is in the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # FORCE DATABASE_URL to in-memory for ALL tests before ANY app imports os.environ["DATABASE_URL"] = "sqlite://" from app.models import DownloadTask, DownloadStatus, DownloadRequest, HostType from app.favorites import FavoritesManager from app.download_manager import DownloadManager from sqlmodel import SQLModel, create_engine, Session @pytest.fixture(scope="session", autouse=True) def init_db(): """Initialize the in-memory database once for the test session""" from app.database import engine SQLModel.metadata.create_all(engine) return engine @pytest.fixture(name="engine") def engine_fixture(): """Returns the global test engine""" from app.database import engine return engine @pytest.fixture(name="session") def session_fixture(engine): """Create a temporary database session for testing""" # Clear and recreate tables for each test to ensure isolation SQLModel.metadata.drop_all(engine) SQLModel.metadata.create_all(engine) with Session(engine) as session: yield session @pytest.fixture(autouse=True) def mock_db(engine): """Ensure each test starts with fresh tables""" SQLModel.metadata.drop_all(engine) SQLModel.metadata.create_all(engine) yield engine @pytest.fixture def user_manager(): """Create a UserManager instance""" from app.auth import UserManager return UserManager() @pytest.fixture def watchlist_manager(): """Create a WatchlistManager instance""" from app.watchlist import WatchlistManager return WatchlistManager() def pytest_configure(config): """Configure pytest with custom markers""" config.addinivalue_line( "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')" ) config.addinivalue_line( "markers", "integration: marks tests as integration tests" ) config.addinivalue_line( "markers", "unit: marks tests as unit tests" ) config.addinivalue_line( "markers", "network: marks tests that require network access" ) def pytest_collection_modifyitems(config, items): """Modify test collection to add markers automatically""" for item in items: # Mark async tests if asyncio.iscoroutinefunction(item.obj): item.add_marker("asyncio") # Mark tests in test_api.py as integration tests if "test_api.py" in str(item.fspath): item.add_marker("integration") # Mark other tests as unit tests else: item.add_marker("unit") def pytest_report_header(config): """Add custom header to pytest report""" return [ "Ohm Stream Downloader - Test Suite", f"Python: {sys.version}", ] @pytest.fixture(scope="session") def event_loop(): """Create an instance of the default event loop for the test session.""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @pytest.fixture def temp_dir(): """Create a temporary directory for test files""" temp_path = Path(tempfile.mkdtemp()) yield temp_path # Cleanup after test shutil.rmtree(temp_path, ignore_errors=True) @pytest.fixture def temp_download_dir(temp_dir): """Create a temporary download directory""" download_dir = temp_dir / "downloads" download_dir.mkdir(exist_ok=True) return download_dir @pytest.fixture def sample_download_task(): """Create a sample download task""" return DownloadTask( id="test-task-123", url="https://example.com/file.mp4", filename="test_video.mp4", host=HostType.OTHER, status=DownloadStatus.PENDING, progress=0.0, downloaded_bytes=0, total_bytes=None, speed=0.0, created_at=datetime.now() ) @pytest.fixture def sample_download_request(): """Create a sample download request""" return DownloadRequest( url="https://example.com/file.mp4", filename="test_video.mp4" ) @pytest.fixture def download_manager(temp_download_dir): """Create a DownloadManager instance with temporary directory""" manager = DownloadManager(download_dir=str(temp_download_dir), max_parallel=2) yield manager # Cleanup for task_id in list(manager.tasks.keys()): if task_id in manager.active_downloads: manager.active_downloads[task_id].cancel() @pytest.fixture async def favorites_manager(temp_dir): """Create a FavoritesManager instance with temporary storage""" storage_path = temp_dir / "test_favorites.json" manager = FavoritesManager(storage_path=str(storage_path)) # Initialize asynchronously await manager._load() yield manager # Cleanup if storage_path.exists(): storage_path.unlink() @pytest.fixture def mock_httpx_client(): """Mock httpx.AsyncClient for network requests""" mock_client = AsyncMock() mock_response = AsyncMock() mock_response.status_code = 200 mock_response.headers = {"content-length": "1000000"} mock_response.raise_for_status = Mock() mock_client.get.return_value = mock_response mock_client.stream.return_value.__aenter__.return_value = mock_response return mock_client @pytest.fixture def sample_anime_metadata(): """Sample anime metadata for testing""" return { "synopsis": "Test anime synopsis", "genres": ["Action", "Adventure"], "rating": "8.5/10", "release_year": 2023, "studio": "Test Studio", "poster_image": "https://example.com/poster.jpg", "total_episodes": 12, "status": "Completed" } @pytest.fixture def sample_favorite_data(): """Sample favorite anime data""" return { "anime_id": "anime-sama-test-anime", "title": "Test Anime", "url": "https://anime-sama.si/test/", "provider": "anime-sama", "metadata": { "synopsis": "Test synopsis", "genres": ["Action", "Adventure"], "rating": "8.5/10", "release_year": 2023 }, "poster_url": "https://example.com/poster.jpg" } @pytest.fixture def mock_async_context_manager(): """Mock async context manager for streaming responses""" class MockAsyncContextManager: def __init__(self, mock_response): self.mock_response = mock_response async def __aenter__(self): return self.mock_response async def __aexit__(self, *args): pass return MockAsyncContextManager # Mock data for API testing @pytest.fixture def mock_anime_search_results(): """Mock anime search results""" return [ { "title": "Naruto Shippuden", "url": "https://anime-sama.si/catalogue/naruto/saison1/vostfr/", "cover_image": "https://example.com/naruto.jpg", "type": "search_result" }, { "title": "One Piece", "url": "https://anime-sama.si/catalogue/one-piece/saison1/vostfr/", "cover_image": "https://example.com/onepiece.jpg", "type": "search_result" } ] @pytest.fixture def mock_episode_list(): """Mock episode list""" return [ {"episode": 1, "url": "https://anime-sama.si/catalogue/test/ep1/vostfr/"}, {"episode": 2, "url": "https://anime-sama.si/catalogue/test/ep2/vostfr/"}, {"episode": 3, "url": "https://anime-sama.si/catalogue/test/ep3/vostfr/"} ] # Patch fixtures @pytest.fixture def patch_httpx_client(): """Patch httpx.AsyncClient to avoid real network calls""" with patch('httpx.AsyncClient') as mock: yield mock @pytest.fixture def patch_aiofiles(): """Patch aiofiles for file operations""" with patch('aiofiles.open', create=True) as mock: yield mock # Test data generators def generate_chunk_data(size: int = 1024 * 1024) -> bytes: """Generate mock chunk data for downloads""" return b"x" * size