"""Unit tests for analytics service. Tests analytics service functionality including download statistics, series popularity tracking, storage analysis, and performance reporting. """ import json from datetime import datetime from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from sqlalchemy.ext.asyncio import AsyncSession from src.server.services.analytics_service import ( AnalyticsService, DownloadStats, PerformanceReport, StorageAnalysis, ) @pytest.fixture def analytics_service(tmp_path): """Create analytics service with temp directory.""" with patch("src.server.services.analytics_service.ANALYTICS_FILE", tmp_path / "analytics.json"): service = AnalyticsService() yield service @pytest.fixture async def mock_db(): """Create mock database session.""" db = AsyncMock(spec=AsyncSession) return db @pytest.mark.asyncio async def test_analytics_service_initialization(analytics_service): """Test analytics service initializes with default data.""" assert analytics_service.analytics_file.exists() data = json.loads(analytics_service.analytics_file.read_text()) assert "created_at" in data assert "download_stats" in data assert "series_popularity" in data assert data["download_stats"]["total_downloads"] == 0 @pytest.mark.asyncio async def test_get_download_stats_no_data( analytics_service, mock_db ): """Test download statistics with no download data.""" mock_db.execute = AsyncMock(return_value=MagicMock( scalars=MagicMock(return_value=MagicMock(all=MagicMock( return_value=[] ))) )) stats = await analytics_service.get_download_stats(mock_db) assert isinstance(stats, DownloadStats) assert stats.total_downloads == 0 assert stats.successful_downloads == 0 assert stats.success_rate == 0.0 @pytest.mark.asyncio async def test_get_download_stats_with_data( analytics_service, mock_db ): """Test download statistics with download data.""" # Mock downloads download1 = MagicMock() download1.status = "completed" download1.size_bytes = 1024 * 1024 * 100 # 100 MB download1.duration_seconds = 60 download2 = MagicMock() download2.status = "failed" download2.size_bytes = 0 download2.duration_seconds = 0 mock_db.execute = AsyncMock(return_value=MagicMock( scalars=MagicMock(return_value=MagicMock(all=MagicMock( return_value=[download1, download2] ))) )) stats = await analytics_service.get_download_stats(mock_db) assert stats.total_downloads == 2 assert stats.successful_downloads == 1 assert stats.failed_downloads == 1 assert stats.success_rate == 50.0 assert stats.total_bytes_downloaded == 1024 * 1024 * 100 @pytest.mark.asyncio async def test_get_series_popularity_empty( analytics_service, mock_db ): """Test series popularity with no data.""" mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) popularity = await analytics_service.get_series_popularity( mock_db, limit=10 ) assert isinstance(popularity, list) assert len(popularity) == 0 @pytest.mark.asyncio async def test_get_series_popularity_with_data( analytics_service, mock_db ): """Test series popularity with data.""" row = MagicMock() row.series_name = "Test Anime" row.download_count = 5 row.total_size = 1024 * 1024 * 500 row.last_download = datetime.now() row.successful = 4 mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[row]) )) popularity = await analytics_service.get_series_popularity( mock_db, limit=10 ) assert len(popularity) == 1 assert popularity[0].series_name == "Test Anime" assert popularity[0].download_count == 5 assert popularity[0].success_rate == 80.0 @pytest.mark.asyncio async def test_get_storage_analysis(analytics_service): """Test storage analysis retrieval.""" with patch("psutil.disk_usage") as mock_disk: mock_disk.return_value = MagicMock( total=1024 * 1024 * 1024 * 1024, used=512 * 1024 * 1024 * 1024, free=512 * 1024 * 1024 * 1024, percent=50.0, ) analysis = analytics_service.get_storage_analysis() assert isinstance(analysis, StorageAnalysis) assert analysis.total_storage_bytes > 0 assert analysis.storage_percent_used == 50.0 @pytest.mark.asyncio async def test_get_performance_report_no_data( analytics_service, mock_db ): """Test performance report with no data.""" mock_db.execute = AsyncMock(return_value=MagicMock( scalars=MagicMock(return_value=MagicMock(all=MagicMock( return_value=[] ))) )) with patch("psutil.Process") as mock_process: mock_process.return_value = MagicMock( memory_info=MagicMock( return_value=MagicMock(rss=100 * 1024 * 1024) ), cpu_percent=MagicMock(return_value=10.0), ) report = await analytics_service.get_performance_report( mock_db, hours=24 ) assert isinstance(report, PerformanceReport) assert report.downloads_per_hour == 0.0 @pytest.mark.asyncio async def test_record_performance_sample(analytics_service): """Test recording performance samples.""" analytics_service.record_performance_sample( queue_size=5, active_downloads=2, cpu_percent=25.0, memory_mb=512.0, ) data = json.loads( analytics_service.analytics_file.read_text() ) assert len(data["performance_samples"]) == 1 sample = data["performance_samples"][0] assert sample["queue_size"] == 5 assert sample["active_downloads"] == 2 @pytest.mark.asyncio async def test_record_multiple_performance_samples( analytics_service ): """Test recording multiple performance samples.""" for i in range(5): analytics_service.record_performance_sample( queue_size=i, active_downloads=i % 2, cpu_percent=10.0 + i, memory_mb=256.0 + i * 50, ) data = json.loads( analytics_service.analytics_file.read_text() ) assert len(data["performance_samples"]) == 5 @pytest.mark.asyncio async def test_generate_summary_report( analytics_service, mock_db ): """Test generating comprehensive summary report.""" mock_db.execute = AsyncMock(return_value=MagicMock( scalars=MagicMock(return_value=MagicMock(all=MagicMock( return_value=[] ))), all=MagicMock(return_value=[]), )) with patch("psutil.disk_usage") as mock_disk: mock_disk.return_value = MagicMock( total=1024 * 1024 * 1024, used=512 * 1024 * 1024, free=512 * 1024 * 1024, percent=50.0, ) with patch("psutil.Process"): report = await analytics_service.generate_summary_report( mock_db ) assert "timestamp" in report assert "download_stats" in report assert "series_popularity" in report assert "storage_analysis" in report assert "performance_report" in report @pytest.mark.asyncio async def test_get_dir_size(analytics_service, tmp_path): """Test directory size calculation.""" # Create test files (tmp_path / "file1.txt").write_text("test content") (tmp_path / "file2.txt").write_text("more test content") subdir = tmp_path / "subdir" subdir.mkdir() (subdir / "file3.txt").write_text("nested content") size = analytics_service._get_dir_size(tmp_path) assert size > 0 @pytest.mark.asyncio async def test_get_dir_size_nonexistent(analytics_service): """Test directory size for nonexistent directory.""" size = analytics_service._get_dir_size( Path("/nonexistent/directory") ) assert size == 0 @pytest.mark.asyncio async def test_analytics_persistence(analytics_service): """Test analytics data persistence.""" analytics_service.record_performance_sample( queue_size=10, active_downloads=3, cpu_percent=50.0, memory_mb=1024.0, ) # Create new service instance analytics_service2 = AnalyticsService() analytics_service2.analytics_file = analytics_service.analytics_file data = json.loads( analytics_service2.analytics_file.read_text() ) assert len(data["performance_samples"]) == 1 @pytest.mark.asyncio async def test_analytics_service_singleton(analytics_service): """Test analytics service singleton pattern.""" from src.server.services.analytics_service import get_analytics_service service1 = get_analytics_service() service2 = get_analytics_service() assert service1 is service2