Files
ohm_streaming/tests/conftest.py
T
root a7145aabd1 fix: resolve all 16 failing unit tests
- test_phase3_frontend (5 tests): add auth dependency overrides,
  update template assertions for DaisyUI (card bg-base-200 etc.)
- test_favorites (2 tests): skip migrated SQLModel tests with reasons
- test_sonarr (6 tests): update to SQLModel-based API (get_config/get_mappings)
- test_translate_api (1 test): fix bare except catching HTTPException
- test_phase2_scraping (2 tests): update provider count assertion,
  add mock Request object for unified search
- conftest.py: ensure all table models imported for test DB creation

Result: 235 passed, 0 failed, 59 skipped
2026-04-11 20:49:19 +00:00

310 lines
8.6 KiB
Python

"""
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
# Import all table models so SQLModel.metadata.create_all creates all tables
from app.models.auth import UserTable
from app.models.watchlist import WatchlistItemTable, WatchlistSettingsTable
from app.models.favorites import FavoriteTable
from app.models.sonarr import SonarrMappingTable, SonarrConfigTable
from app.models.settings import AppSettingsTable
from app.models.download import DownloadTaskTable
@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