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>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Unit tests for Pydantic models
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.models import (
|
||||
DownloadTask,
|
||||
DownloadStatus,
|
||||
DownloadRequest,
|
||||
HostType,
|
||||
AnimeMetadata,
|
||||
AnimeSearchResult
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadStatus:
|
||||
"""Tests for DownloadStatus enum"""
|
||||
|
||||
def test_status_values(self):
|
||||
"""Test that all status values are correct"""
|
||||
assert DownloadStatus.PENDING == "pending"
|
||||
assert DownloadStatus.DOWNLOADING == "downloading"
|
||||
assert DownloadStatus.PAUSED == "paused"
|
||||
assert DownloadStatus.COMPLETED == "completed"
|
||||
assert DownloadStatus.FAILED == "failed"
|
||||
assert DownloadStatus.CANCELLED == "cancelled"
|
||||
|
||||
def test_status_comparison(self):
|
||||
"""Test status comparison"""
|
||||
status1 = DownloadStatus.PENDING
|
||||
status2 = DownloadStatus.DOWNLOADING
|
||||
assert status1 != status2
|
||||
assert status1 == "pending"
|
||||
|
||||
|
||||
class TestHostType:
|
||||
"""Tests for HostType enum"""
|
||||
|
||||
def test_host_values(self):
|
||||
"""Test that all host type values are correct"""
|
||||
assert HostType.RAPIDFILE == "rapidfile"
|
||||
assert HostType.UNFICHIER == "1fichier"
|
||||
assert HostType.DOODSTREAM == "doodstream"
|
||||
assert HostType.OTHER == "other"
|
||||
|
||||
|
||||
class TestDownloadTask:
|
||||
"""Tests for DownloadTask model"""
|
||||
|
||||
def test_create_download_task_valid(self, sample_download_task):
|
||||
"""Test creating a valid download task"""
|
||||
task = sample_download_task
|
||||
assert task.id == "test-task-123"
|
||||
assert task.url == "https://example.com/file.mp4"
|
||||
assert task.filename == "test_video.mp4"
|
||||
assert task.host == HostType.OTHER
|
||||
assert task.status == DownloadStatus.PENDING
|
||||
assert task.progress == 0.0
|
||||
assert task.downloaded_bytes == 0
|
||||
assert task.total_bytes is None
|
||||
assert task.speed == 0.0
|
||||
|
||||
def test_download_task_with_optional_fields(self):
|
||||
"""Test download task with all optional fields"""
|
||||
now = datetime.now()
|
||||
task = DownloadTask(
|
||||
id="task-456",
|
||||
url="https://example.com/file2.mp4",
|
||||
filename="file2.mp4",
|
||||
host=HostType.DOODSTREAM,
|
||||
status=DownloadStatus.DOWNLOADING,
|
||||
progress=50.0,
|
||||
downloaded_bytes=5000000,
|
||||
total_bytes=10000000,
|
||||
speed=1000000.0,
|
||||
error="Test error",
|
||||
created_at=now,
|
||||
started_at=now,
|
||||
completed_at=None,
|
||||
file_path="/downloads/file2.mp4"
|
||||
)
|
||||
assert task.progress == 50.0
|
||||
assert task.downloaded_bytes == 5000000
|
||||
assert task.total_bytes == 10000000
|
||||
assert task.speed == 1000000.0
|
||||
assert task.error == "Test error"
|
||||
assert task.started_at is not None
|
||||
assert task.completed_at is None
|
||||
assert task.file_path == "/downloads/file2.mp4"
|
||||
|
||||
def test_download_task_default_values(self):
|
||||
"""Test download task with default values"""
|
||||
task = DownloadTask(
|
||||
id="task-default",
|
||||
url="https://example.com/file.mp4",
|
||||
filename="file.mp4",
|
||||
host=HostType.OTHER,
|
||||
status=DownloadStatus.PENDING,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
assert task.progress == 0.0
|
||||
assert task.downloaded_bytes == 0
|
||||
assert task.speed == 0.0
|
||||
assert task.total_bytes is None
|
||||
assert task.error is None
|
||||
assert task.started_at is None
|
||||
assert task.completed_at is None
|
||||
assert task.file_path is None
|
||||
|
||||
def test_download_task_invalid_url(self):
|
||||
"""Test that invalid URL raises ValidationError"""
|
||||
with pytest.raises(ValidationError):
|
||||
DownloadTask(
|
||||
id="task-invalid",
|
||||
url="not-a-valid-url",
|
||||
filename="file.mp4",
|
||||
host=HostType.OTHER,
|
||||
status=DownloadStatus.PENDING,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
def test_download_task_negative_progress(self):
|
||||
"""Test that negative progress is invalid"""
|
||||
with pytest.raises(ValidationError):
|
||||
DownloadTask(
|
||||
id="task-negative",
|
||||
url="https://example.com/file.mp4",
|
||||
filename="file.mp4",
|
||||
host=HostType.OTHER,
|
||||
status=DownloadStatus.PENDING,
|
||||
progress=-10.0, # Invalid
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
def test_download_task_progress_over_100(self):
|
||||
"""Test that progress over 100 is invalid"""
|
||||
with pytest.raises(ValidationError):
|
||||
DownloadTask(
|
||||
id="task-over100",
|
||||
url="https://example.com/file.mp4",
|
||||
filename="file.mp4",
|
||||
host=HostType.OTHER,
|
||||
status=DownloadStatus.PENDING,
|
||||
progress=150.0, # Invalid
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadRequest:
|
||||
"""Tests for DownloadRequest model"""
|
||||
|
||||
def test_create_request_with_filename(self, sample_download_request):
|
||||
"""Test creating download request with filename"""
|
||||
request = sample_download_request
|
||||
assert request.url == "https://example.com/file.mp4"
|
||||
assert request.filename == "test_video.mp4"
|
||||
|
||||
def test_create_request_without_filename(self):
|
||||
"""Test creating download request without filename"""
|
||||
request = DownloadRequest(url="https://example.com/file.mp4")
|
||||
assert request.url == "https://example.com/file.mp4"
|
||||
assert request.filename is None
|
||||
|
||||
def test_request_invalid_url(self):
|
||||
"""Test that invalid URL raises ValidationError"""
|
||||
with pytest.raises(ValidationError):
|
||||
DownloadRequest(url="not-a-url")
|
||||
|
||||
def test_request_empty_url(self):
|
||||
"""Test that empty URL raises ValidationError"""
|
||||
with pytest.raises(ValidationError):
|
||||
DownloadRequest(url="")
|
||||
|
||||
|
||||
class TestAnimeMetadata:
|
||||
"""Tests for AnimeMetadata model"""
|
||||
|
||||
def test_create_metadata_all_fields(self):
|
||||
"""Test creating metadata with all fields"""
|
||||
metadata = AnimeMetadata(
|
||||
synopsis="Test anime about adventure",
|
||||
genres=["Action", "Adventure", "Fantasy"],
|
||||
rating="8.5/10",
|
||||
release_year=2023,
|
||||
studio="Test Studio",
|
||||
poster_image="https://example.com/poster.jpg",
|
||||
banner_image="https://example.com/banner.jpg",
|
||||
total_episodes=12,
|
||||
status="Completed",
|
||||
alternative_titles=["Test Anime 1", "Test Anime 2"]
|
||||
)
|
||||
assert metadata.synopsis == "Test anime about adventure"
|
||||
assert len(metadata.genres) == 3
|
||||
assert metadata.rating == "8.5/10"
|
||||
assert metadata.release_year == 2023
|
||||
assert metadata.studio == "Test Studio"
|
||||
assert metadata.poster_image is not None
|
||||
assert metadata.banner_image is not None
|
||||
assert metadata.total_episodes == 12
|
||||
assert metadata.status == "Completed"
|
||||
assert len(metadata.alternative_titles) == 2
|
||||
|
||||
def test_create_metadata_minimal(self):
|
||||
"""Test creating metadata with minimal fields"""
|
||||
metadata = AnimeMetadata()
|
||||
assert metadata.synopsis is None
|
||||
assert metadata.genres == []
|
||||
assert metadata.rating is None
|
||||
assert metadata.release_year is None
|
||||
assert metadata.studio is None
|
||||
assert metadata.poster_image is None
|
||||
assert metadata.banner_image is None
|
||||
assert metadata.total_episodes is None
|
||||
assert metadata.status is None
|
||||
assert metadata.alternative_titles == []
|
||||
|
||||
def test_metadata_with_some_fields(self):
|
||||
"""Test creating metadata with only some fields"""
|
||||
metadata = AnimeMetadata(
|
||||
genres=["Action"],
|
||||
rating="PG-13",
|
||||
release_year=2020
|
||||
)
|
||||
assert len(metadata.genres) == 1
|
||||
assert metadata.genres == ["Action"]
|
||||
assert metadata.rating == "PG-13"
|
||||
assert metadata.release_year == 2020
|
||||
assert metadata.synopsis is None
|
||||
assert metadata.studio is None
|
||||
|
||||
def test_metadata_empty_genres_list(self):
|
||||
"""Test metadata with empty genres list"""
|
||||
metadata = AnimeMetadata(genres=[])
|
||||
assert metadata.genres == []
|
||||
|
||||
def test_metadata_negative_year(self):
|
||||
"""Test that negative year is handled"""
|
||||
# Pydantic doesn't validate range by default
|
||||
metadata = AnimeMetadata(release_year=-2023)
|
||||
assert metadata.release_year == -2023
|
||||
|
||||
def test_metadata_zero_episodes(self):
|
||||
"""Test metadata with zero episodes"""
|
||||
metadata = AnimeMetadata(total_episodes=0)
|
||||
assert metadata.total_episodes == 0
|
||||
|
||||
|
||||
class TestAnimeSearchResult:
|
||||
"""Tests for AnimeSearchResult model"""
|
||||
|
||||
def test_create_search_result_full(self):
|
||||
"""Test creating search result with all fields"""
|
||||
metadata = AnimeMetadata(
|
||||
synopsis="Test",
|
||||
genres=["Action"],
|
||||
rating="8.0/10"
|
||||
)
|
||||
result = AnimeSearchResult(
|
||||
title="Test Anime",
|
||||
url="https://example.com/anime",
|
||||
cover_image="https://example.com/cover.jpg",
|
||||
type="search_result",
|
||||
metadata=metadata
|
||||
)
|
||||
assert result.title == "Test Anime"
|
||||
assert result.url == "https://example.com/anime"
|
||||
assert result.cover_image == "https://example.com/cover.jpg"
|
||||
assert result.type == "search_result"
|
||||
assert result.metadata is not None
|
||||
assert result.metadata.synopsis == "Test"
|
||||
|
||||
def test_create_search_result_minimal(self):
|
||||
"""Test creating minimal search result"""
|
||||
result = AnimeSearchResult(
|
||||
title="Minimal Anime",
|
||||
url="https://example.com/minimal",
|
||||
type="direct"
|
||||
)
|
||||
assert result.title == "Minimal Anime"
|
||||
assert result.url == "https://example.com/minimal"
|
||||
assert result.type == "direct"
|
||||
assert result.cover_image is None
|
||||
assert result.metadata is None
|
||||
|
||||
def test_search_result_invalid_type(self):
|
||||
"""Test that invalid type raises ValidationError"""
|
||||
with pytest.raises(ValidationError):
|
||||
AnimeSearchResult(
|
||||
title="Test",
|
||||
url="https://example.com",
|
||||
type="invalid_type" # Must be specific types
|
||||
)
|
||||
|
||||
def test_search_result_empty_title(self):
|
||||
"""Test that empty title raises ValidationError"""
|
||||
with pytest.raises(ValidationError):
|
||||
AnimeSearchResult(
|
||||
title="",
|
||||
url="https://example.com",
|
||||
type="search_result"
|
||||
)
|
||||
|
||||
def test_search_result_invalid_url(self):
|
||||
"""Test that invalid URL raises ValidationError"""
|
||||
with pytest.raises(ValidationError):
|
||||
AnimeSearchResult(
|
||||
title="Test",
|
||||
url="not-a-url",
|
||||
type="search_result"
|
||||
)
|
||||
Reference in New Issue
Block a user