Files
ohm_streaming/tests/test_downloaders.py
T
root 785147b1b1 test: Add comprehensive unit and integration test suite
Implement a complete test suite for Ohm Stream Downloader with over 300 tests covering:

Test Files:
- tests/test_models.py: Pydantic model validation tests
  * DownloadTask, DownloadRequest, DownloadStatus, HostType
  * AnimeMetadata, AnimeSearchResult
  * Field validation, edge cases, error handling

- tests/test_downloaders.py: Downloader implementation tests
  * BaseDownloader abstract class
  * Unfichier, Doodstream, Rapidfile, Uptobox downloaders
  * Video downloaders (VidMoly, SendVid)
  * Anime provider downloaders (Anime-Sama, Neko-Sama, etc.)
  * URL detection and handling

- tests/test_download_manager.py: Core download management tests
  * Task creation and lifecycle
  * Pause/resume/cancel operations
  * Progress tracking and file handling
  * Concurrency and semaphore limits
  * Error handling and edge cases

- tests/test_favorites.py: Favorites system tests
  * Add, remove, get, list favorites
  * Sorting and filtering (by title, rating, provider, genre)
  * Toggle functionality
  * Statistics generation
  * Concurrent operations

- tests/test_api.py: FastAPI endpoint tests
  * Root, health, providers endpoints
  * Download CRUD operations
  * Anime search and metadata endpoints
  * Favorites API endpoints
  * Sorting and filtering
  * Error handling and validation
  * CORS headers

Infrastructure:
- tests/conftest.py: Pytest configuration and fixtures
  * Temporary directories for isolation
  * Sample data fixtures
  * Mock clients for network operations
  * Custom markers (unit, integration, slow, network)

- pytest.ini: Pytest configuration
  * Coverage reporting (term + HTML)
  * Verbose output with locals
  * Strict markers
  * Async test support
  * Timeout configuration

- requirements.txt: Updated with testing dependencies
  * pytest, pytest-asyncio, pytest-cov
  * pytest-mock, pytest-timeout, pytest-html

- .gitignore: Updated to ignore test artifacts
  * .pytest_cache/, coverage reports
  * Project data files (favorites.json, *.db)

- tests/README.md: Test documentation
  * How to run tests
  * Available fixtures and markers
  * Coverage reporting instructions

Test Coverage Areas:
✓ Model validation and serialization
✓ All downloader implementations
✓ Download queue management
✓ Favorites persistence and retrieval
✓ REST API endpoints
✓ Error handling and edge cases
✓ Async/await operations
✓ Concurrent operations
✓ File system operations

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-23 10:28:47 +00:00

340 lines
13 KiB
Python

"""
Unit tests for downloaders
"""
import pytest
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from bs4 import BeautifulSoup
from app.downloaders.base import BaseDownloader
class TestBaseDownloader:
"""Tests for BaseDownloader abstract class"""
def test_base_downloader_is_abstract(self):
"""Test that BaseDownloader cannot be instantiated directly"""
with pytest.raises(TypeError):
BaseDownloader()
def test_base_downloader_can_handle_not_implemented(self):
"""Test that can_handle raises NotImplementedError"""
class TestDownloader(BaseDownloader):
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
with pytest.raises(NotImplementedError):
downloader.can_handle("https://example.com")
def test_base_downloader_get_download_link_not_implemented(self):
"""Test that get_download_link raises NotImplementedError"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
downloader = TestDownloader()
with pytest.raises(NotImplementedError):
# Need to await the coroutine
import asyncio
asyncio.run(downloader.get_download_link("https://example.com"))
@pytest.mark.asyncio
async def test_base_downloader_fetch_page(self):
"""Test _fetch_page method"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
# Mock the client.get method
with patch.object(downloader.client, 'get') as mock_get:
mock_response = Mock()
mock_response.text = "<html>Test content</html>"
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
content = await downloader._fetch_page("https://example.com")
assert content == "<html>Test content</html>"
mock_get.assert_called_once_with("https://example.com")
@pytest.mark.asyncio
async def test_base_downloader_fetch_page_error(self):
"""Test _fetch_page method with HTTP error"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
with patch.object(downloader.client, 'get') as mock_get:
mock_response = Mock()
mock_response.raise_for_status.side_effect = Exception("HTTP Error")
mock_get.return_value = mock_response
with pytest.raises(Exception):
await downloader._fetch_page("https://example.com")
def test_extract_filename_from_headers(self):
"""Test _extract_filename_from_headers method"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
# Test with filename in headers
headers = {"content-disposition": 'attachment; filename="test.mp4"'}
filename = downloader._extract_filename_from_headers(headers)
assert filename == "test.mp4"
# Test without filename
headers = {}
filename = downloader._extract_filename_from_headers(headers)
assert filename is None
# Test with filename in single quotes
headers = {"content-disposition": "attachment; filename='test.mp4'"}
filename = downloader._extract_filename_from_headers(headers)
assert filename == "'test.mp4'"
@pytest.mark.asyncio
async def test_search_anime_default(self):
"""Test default search_anime returns empty list"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
results = await downloader.search_anime("naruto", "vostfr")
assert results == []
@pytest.mark.asyncio
async def test_get_episodes_default(self):
"""Test default get_episodes returns empty list"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
episodes = await downloader.get_episodes("https://example.com/anime", "vostfr")
assert episodes == []
@pytest.mark.asyncio
async def test_close(self):
"""Test close method"""
class TestDownloader(BaseDownloader):
def can_handle(self, url: str) -> bool:
return True
async def get_download_link(self, url: str):
return ("http://example.com/download", "file.mp4")
downloader = TestDownloader()
# Mock the client.aclose method
with patch.object(downloader.client, 'aclose') as mock_aclose:
mock_aclose.return_value = AsyncMock()
await downloader.close()
mock_aclose.assert_called_once()
# Test for concrete downloader implementations
class TestDownloaderRegistration:
"""Tests for downloader registration system"""
def test_get_downloader_returns_downloader(self):
"""Test that get_downloader returns appropriate downloader"""
from app.downloaders import get_downloader
# Test with 1fichier URL
downloader = get_downloader("https://1fichier.com/?abcdef")
assert downloader is not None
assert downloader.can_handle("https://1fichier.com/?abcdef")
# Test with doodstream URL
downloader = get_downloader("https://doodstream.com/d/abcdef")
assert downloader is not None
assert downloader.can_handle("https://doodstream.com/d/abcdef")
def test_get_downloader_fallback(self):
"""Test that get_downloader falls back to other for unknown hosts"""
from app.downloaders import get_downloader
downloader = get_downloader("https://unknown-host.com/file")
assert downloader is not None
def test_all_downloaders_have_required_methods(self):
"""Test that all registered downloaders implement required methods"""
from app.downloaders import get_downloader
test_urls = [
"https://1fichier.com/?test",
"https://doodstream.com/d/test",
"https://rapidfile.net/test",
"https://uptobox.com/test"
]
for url in test_urls:
downloader = get_downloader(url)
assert hasattr(downloader, 'can_handle')
assert hasattr(downloader, 'get_download_link')
assert callable(downloader.can_handle)
# get_download_link is async, so we can't test with callable()
import inspect
assert inspect.iscoroutinefunction(downloader.get_download_link)
class TestDownloaderCanHandle:
"""Tests for can_handle method in concrete downloaders"""
def test_unfichier_can_handle(self):
"""Test UnfichierDownloader.can_handle"""
from app.downloaders.unfichier import UnfichierDownloader
downloader = UnfichierDownloader()
assert downloader.can_handle("https://1fichier.com/?abc123") is True
assert downloader.can_handle("https://1fichier.fr/?abc123") is True
assert downloader.can_handle("http://1fichier.com/?abc123") is True
assert downloader.can_handle("https://doodstream.com/test") is False
assert downloader.can_handle("https://example.com/test") is False
def test_doodstream_can_handle(self):
"""Test DoodstreamDownloader.can_handle"""
from app.downloaders.doodstream import DoodstreamDownloader
downloader = DoodstreamDownloader()
assert downloader.can_handle("https://doodstream.com/d/abc123") is True
assert downloader.can_handle("https://dood.to/d/abc123") is True
assert downloader.can_handle("https://dood.lol/d/abc123") is True
assert downloader.can_handle("https://1fichier.com/?test") is False
def test_rapidfile_can_handle(self):
"""Test RapidfileDownloader.can_handle"""
from app.downloaders.rapidfile import RapidfileDownloader
downloader = RapidfileDownloader()
assert downloader.can_handle("https://rapidfile.net/abc123") is True
assert downloader.can_handle("https://rapidfile.com/abc123") is True
assert downloader.can_handle("https://doodstream.com/test") is False
def test_uptobox_can_handle(self):
"""Test UptoboxDownloader.can_handle"""
from app.downloaders.uptobox import UptoboxDownloader
downloader = UptoboxDownloader()
assert downloader.can_handle("https://uptobox.com/abc123") is True
assert downloader.can_handle("https://uptobox.fr/abc123") is True
assert downloader.can_handle("https://doodstream.com/test") is False
def test_vidmoly_can_handle(self):
"""Test VidMolyDownloader.can_handle"""
from app.downloaders.vidmoly import VidMolyDownloader
downloader = VidMolyDownloader()
assert downloader.can_handle("https://vidmoly.to/abc123") is True
assert downloader.can_handle("https://vidmoly.com/abc123") is True
assert downloader.can_handle("https://doodstream.com/test") is False
def test_sendvid_can_handle(self):
"""Test SendVidDownloader.can_handle"""
from app.downloaders.sendvid import SendVidDownloader
downloader = SendVidDownloader()
assert downloader.can_handle("https://sendvid.com/abc123") is True
assert downloader.can_handle("https://doodstream.com/test") is False
class TestAnimeDownloaders:
"""Tests for anime provider downloaders"""
@pytest.mark.asyncio
async def test_anime_sama_search(self):
"""Test AnimeSamaDownloader.search_anime"""
from app.downloaders.animesama import AnimeSamaDownloader
downloader = AnimeSamaDownloader()
with patch.object(downloader, '_fetch_page') as mock_fetch:
# Mock HTML response
mock_html = """
<html>
<div class="sa-popflex">
<a href="/anime/test-anime/" class="sa-poster">
<img src="https://example.com/poster.jpg" alt="Test Anime">
<div class="sa-poster-info">
<h2 class="entry-title">Test Anime</h2>
</div>
</a>
</div>
</html>
"""
mock_fetch.return_value = mock_html
results = await downloader.search_anime("test anime", "vostfr")
# Should return results based on the mocked HTML
assert isinstance(results, list)
@pytest.mark.asyncio
async def test_neko_sama_can_handle(self):
"""Test NekoSamaDownloader.can_handle"""
from app.downloaders.nekosama import NekoSamaDownloader
downloader = NekoSamaDownloader()
assert downloader.can_handle("https://neko-sama.franime/test") is True
assert downloader.can_handle("https://neko-sama.netanime/test") is True
assert downloader.can_handle("https://anime-sama.si/test") is False
@pytest.mark.asyncio
async def test_anime_ultime_can_handle(self):
"""Test AnimeUltimeDownloader.can_handle"""
from app.downloaders.animeultime import AnimeUltimeDownloader
downloader = AnimeUltimeDownloader()
assert downloader.can_handle("https://anime-ultime.net/test") is True
assert downloader.can_handle("https://anime-sama.si/test") is False
@pytest.mark.asyncio
async def test_vostfree_can_handle(self):
"""Test VostfreeDownloader.can_handle"""
from app.downloaders.vostfree import VostfreeDownloader
downloader = VostfreeDownloader()
assert downloader.can_handle("https://vostfree.top/test") is True
assert downloader.can_handle("https://anime-sama.si/test") is False
class TestDownloaderUrlExtraction:
"""Tests for URL extraction methods"""
@pytest.mark.asyncio
async def test_get_download_link_mock(self):
"""Test get_download_link with mocked response"""
from app.downloaders.unfichier import UnfichierDownloader
downloader = UnfichierDownloader()
with patch.object(downloader, '_fetch_page') as mock_fetch:
# Mock a simple HTML page
mock_fetch.return_value = "<html><body>Test page</body></html>"
# This should not crash
try:
download_url, filename = await downloader.get_download_link("https://1fichier.com/?test")
# Result may vary based on actual implementation
assert isinstance(download_url, str)
assert isinstance(filename, str)
except Exception as e:
# Some downloaders might fail with mock HTML
assert isinstance(e, (ValueError, AttributeError, KeyError))