"""Unit tests for database service layer. Tests CRUD operations for all database services using in-memory SQLite. """ import asyncio from datetime import datetime, timedelta import pytest from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from src.server.database.base import Base from src.server.database.models import DownloadPriority, DownloadStatus from src.server.database.service import ( AnimeSeriesService, DownloadQueueService, EpisodeService, UserSessionService, ) @pytest.fixture async def db_engine(): """Create in-memory database engine for testing.""" engine = create_async_engine( "sqlite+aiosqlite:///:memory:", echo=False, ) # Create all tables async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine # Cleanup await engine.dispose() @pytest.fixture async def db_session(db_engine): """Create database session for testing.""" async_session = sessionmaker( db_engine, class_=AsyncSession, expire_on_commit=False, ) async with async_session() as session: yield session await session.rollback() # ============================================================================ # AnimeSeriesService Tests # ============================================================================ @pytest.mark.asyncio async def test_create_anime_series(db_session): """Test creating an anime series.""" series = await AnimeSeriesService.create( db_session, key="test-anime-1", name="Test Anime", site="https://example.com", folder="/path/to/anime", description="A test anime", status="ongoing", total_episodes=12, cover_url="https://example.com/cover.jpg", ) assert series.id is not None assert series.key == "test-anime-1" assert series.name == "Test Anime" assert series.description == "A test anime" assert series.total_episodes == 12 @pytest.mark.asyncio async def test_get_anime_series_by_id(db_session): """Test retrieving anime series by ID.""" # Create series series = await AnimeSeriesService.create( db_session, key="test-anime-2", name="Test Anime 2", site="https://example.com", folder="/path/to/anime2", ) await db_session.commit() # Retrieve series retrieved = await AnimeSeriesService.get_by_id(db_session, series.id) assert retrieved is not None assert retrieved.id == series.id assert retrieved.key == "test-anime-2" @pytest.mark.asyncio async def test_get_anime_series_by_key(db_session): """Test retrieving anime series by provider key.""" # Create series await AnimeSeriesService.create( db_session, key="unique-key", name="Test Anime", site="https://example.com", folder="/path/to/anime", ) await db_session.commit() # Retrieve by key retrieved = await AnimeSeriesService.get_by_key(db_session, "unique-key") assert retrieved is not None assert retrieved.key == "unique-key" @pytest.mark.asyncio async def test_get_all_anime_series(db_session): """Test retrieving all anime series.""" # Create multiple series await AnimeSeriesService.create( db_session, key="anime-1", name="Anime 1", site="https://example.com", folder="/path/1", ) await AnimeSeriesService.create( db_session, key="anime-2", name="Anime 2", site="https://example.com", folder="/path/2", ) await db_session.commit() # Retrieve all all_series = await AnimeSeriesService.get_all(db_session) assert len(all_series) == 2 @pytest.mark.asyncio async def test_update_anime_series(db_session): """Test updating anime series.""" # Create series series = await AnimeSeriesService.create( db_session, key="anime-update", name="Original Name", site="https://example.com", folder="/path/original", ) await db_session.commit() # Update series updated = await AnimeSeriesService.update( db_session, series.id, name="Updated Name", total_episodes=24, ) await db_session.commit() assert updated is not None assert updated.name == "Updated Name" assert updated.total_episodes == 24 @pytest.mark.asyncio async def test_delete_anime_series(db_session): """Test deleting anime series.""" # Create series series = await AnimeSeriesService.create( db_session, key="anime-delete", name="To Delete", site="https://example.com", folder="/path/delete", ) await db_session.commit() # Delete series deleted = await AnimeSeriesService.delete(db_session, series.id) await db_session.commit() assert deleted is True # Verify deletion retrieved = await AnimeSeriesService.get_by_id(db_session, series.id) assert retrieved is None @pytest.mark.asyncio async def test_search_anime_series(db_session): """Test searching anime series by name.""" # Create series await AnimeSeriesService.create( db_session, key="naruto", name="Naruto Shippuden", site="https://example.com", folder="/path/naruto", ) await AnimeSeriesService.create( db_session, key="bleach", name="Bleach", site="https://example.com", folder="/path/bleach", ) await db_session.commit() # Search results = await AnimeSeriesService.search(db_session, "naruto") assert len(results) == 1 assert results[0].name == "Naruto Shippuden" # ============================================================================ # EpisodeService Tests # ============================================================================ @pytest.mark.asyncio async def test_create_episode(db_session): """Test creating an episode.""" # Create series first series = await AnimeSeriesService.create( db_session, key="test-series", name="Test Series", site="https://example.com", folder="/path/test", ) await db_session.commit() # Create episode episode = await EpisodeService.create( db_session, series_id=series.id, season=1, episode_number=1, title="Episode 1", ) assert episode.id is not None assert episode.series_id == series.id assert episode.season == 1 assert episode.episode_number == 1 @pytest.mark.asyncio async def test_get_episodes_by_series(db_session): """Test retrieving episodes for a series.""" # Create series series = await AnimeSeriesService.create( db_session, key="test-series-2", name="Test Series 2", site="https://example.com", folder="/path/test2", ) # Create episodes await EpisodeService.create( db_session, series_id=series.id, season=1, episode_number=1, ) await EpisodeService.create( db_session, series_id=series.id, season=1, episode_number=2, ) await db_session.commit() # Retrieve episodes episodes = await EpisodeService.get_by_series(db_session, series.id) assert len(episodes) == 2 @pytest.mark.asyncio async def test_mark_episode_downloaded(db_session): """Test marking episode as downloaded.""" # Create series and episode series = await AnimeSeriesService.create( db_session, key="test-series-3", name="Test Series 3", site="https://example.com", folder="/path/test3", ) episode = await EpisodeService.create( db_session, series_id=series.id, season=1, episode_number=1, ) await db_session.commit() # Mark as downloaded updated = await EpisodeService.mark_downloaded( db_session, episode.id, file_path="/path/to/file.mp4", file_size=1024000, ) await db_session.commit() assert updated is not None assert updated.is_downloaded is True assert updated.file_path == "/path/to/file.mp4" assert updated.download_date is not None # ============================================================================ # DownloadQueueService Tests # ============================================================================ @pytest.mark.asyncio async def test_create_download_queue_item(db_session): """Test adding item to download queue.""" # Create series series = await AnimeSeriesService.create( db_session, key="test-series-4", name="Test Series 4", site="https://example.com", folder="/path/test4", ) await db_session.commit() # Add to queue item = await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=1, priority=DownloadPriority.HIGH, ) assert item.id is not None assert item.status == DownloadStatus.PENDING assert item.priority == DownloadPriority.HIGH @pytest.mark.asyncio async def test_get_pending_downloads(db_session): """Test retrieving pending downloads.""" # Create series series = await AnimeSeriesService.create( db_session, key="test-series-5", name="Test Series 5", site="https://example.com", folder="/path/test5", ) # Add pending items await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=1, ) await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=2, ) await db_session.commit() # Retrieve pending pending = await DownloadQueueService.get_pending(db_session) assert len(pending) == 2 @pytest.mark.asyncio async def test_update_download_status(db_session): """Test updating download status.""" # Create series and queue item series = await AnimeSeriesService.create( db_session, key="test-series-6", name="Test Series 6", site="https://example.com", folder="/path/test6", ) item = await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=1, ) await db_session.commit() # Update status updated = await DownloadQueueService.update_status( db_session, item.id, DownloadStatus.DOWNLOADING, ) await db_session.commit() assert updated is not None assert updated.status == DownloadStatus.DOWNLOADING assert updated.started_at is not None @pytest.mark.asyncio async def test_update_download_progress(db_session): """Test updating download progress.""" # Create series and queue item series = await AnimeSeriesService.create( db_session, key="test-series-7", name="Test Series 7", site="https://example.com", folder="/path/test7", ) item = await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=1, ) await db_session.commit() # Update progress updated = await DownloadQueueService.update_progress( db_session, item.id, progress_percent=50.0, downloaded_bytes=500000, total_bytes=1000000, download_speed=50000.0, ) await db_session.commit() assert updated is not None assert updated.progress_percent == 50.0 assert updated.downloaded_bytes == 500000 assert updated.total_bytes == 1000000 @pytest.mark.asyncio async def test_clear_completed_downloads(db_session): """Test clearing completed downloads.""" # Create series and completed items series = await AnimeSeriesService.create( db_session, key="test-series-8", name="Test Series 8", site="https://example.com", folder="/path/test8", ) item1 = await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=1, ) item2 = await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=2, ) # Mark items as completed await DownloadQueueService.update_status( db_session, item1.id, DownloadStatus.COMPLETED, ) await DownloadQueueService.update_status( db_session, item2.id, DownloadStatus.COMPLETED, ) await db_session.commit() # Clear completed count = await DownloadQueueService.clear_completed(db_session) await db_session.commit() assert count == 2 @pytest.mark.asyncio async def test_retry_failed_downloads(db_session): """Test retrying failed downloads.""" # Create series and failed item series = await AnimeSeriesService.create( db_session, key="test-series-9", name="Test Series 9", site="https://example.com", folder="/path/test9", ) item = await DownloadQueueService.create( db_session, series_id=series.id, season=1, episode_number=1, ) # Mark as failed await DownloadQueueService.update_status( db_session, item.id, DownloadStatus.FAILED, error_message="Network error", ) await db_session.commit() # Retry retried = await DownloadQueueService.retry_failed(db_session) await db_session.commit() assert len(retried) == 1 assert retried[0].status == DownloadStatus.PENDING assert retried[0].error_message is None # ============================================================================ # UserSessionService Tests # ============================================================================ @pytest.mark.asyncio async def test_create_user_session(db_session): """Test creating a user session.""" expires_at = datetime.utcnow() + timedelta(hours=24) session = await UserSessionService.create( db_session, session_id="test-session-1", token_hash="hashed-token", expires_at=expires_at, user_id="user123", ip_address="127.0.0.1", ) assert session.id is not None assert session.session_id == "test-session-1" assert session.is_active is True @pytest.mark.asyncio async def test_get_session_by_id(db_session): """Test retrieving session by ID.""" expires_at = datetime.utcnow() + timedelta(hours=24) session = await UserSessionService.create( db_session, session_id="test-session-2", token_hash="hashed-token", expires_at=expires_at, ) await db_session.commit() # Retrieve retrieved = await UserSessionService.get_by_session_id( db_session, "test-session-2", ) assert retrieved is not None assert retrieved.session_id == "test-session-2" @pytest.mark.asyncio async def test_get_active_sessions(db_session): """Test retrieving active sessions.""" expires_at = datetime.utcnow() + timedelta(hours=24) # Create active session await UserSessionService.create( db_session, session_id="active-session", token_hash="hashed-token", expires_at=expires_at, ) # Create expired session await UserSessionService.create( db_session, session_id="expired-session", token_hash="hashed-token", expires_at=datetime.utcnow() - timedelta(hours=1), ) await db_session.commit() # Retrieve active sessions active = await UserSessionService.get_active_sessions(db_session) assert len(active) == 1 assert active[0].session_id == "active-session" @pytest.mark.asyncio async def test_revoke_session(db_session): """Test revoking a session.""" expires_at = datetime.utcnow() + timedelta(hours=24) session = await UserSessionService.create( db_session, session_id="test-session-3", token_hash="hashed-token", expires_at=expires_at, ) await db_session.commit() # Revoke revoked = await UserSessionService.revoke(db_session, "test-session-3") await db_session.commit() assert revoked is True # Verify retrieved = await UserSessionService.get_by_session_id( db_session, "test-session-3", ) assert retrieved.is_active is False @pytest.mark.asyncio async def test_cleanup_expired_sessions(db_session): """Test cleaning up expired sessions.""" # Create expired sessions await UserSessionService.create( db_session, session_id="expired-1", token_hash="hashed-token", expires_at=datetime.utcnow() - timedelta(hours=1), ) await UserSessionService.create( db_session, session_id="expired-2", token_hash="hashed-token", expires_at=datetime.utcnow() - timedelta(hours=2), ) await db_session.commit() # Cleanup count = await UserSessionService.cleanup_expired(db_session) await db_session.commit() assert count == 2 @pytest.mark.asyncio async def test_update_session_activity(db_session): """Test updating session last activity.""" expires_at = datetime.utcnow() + timedelta(hours=24) session = await UserSessionService.create( db_session, session_id="test-session-4", token_hash="hashed-token", expires_at=expires_at, ) await db_session.commit() original_activity = session.last_activity # Wait a bit await asyncio.sleep(0.1) # Update activity updated = await UserSessionService.update_activity( db_session, "test-session-4", ) await db_session.commit() assert updated is not None assert updated.last_activity > original_activity