Files
ComfyUI-Lora-Manager/tests/services/test_downloader_error_paths.py
Will Miao 25e6d72c4f test(backend): Phase 1 - Improve testing infrastructure and add error path tests
## Changes

### pytest-asyncio Integration
- Add pytest-asyncio>=0.21.0 to requirements-dev.txt
- Update pytest.ini with asyncio_mode=auto and fixture loop scope
- Remove custom pytest_pyfunc_call handler from conftest.py
- Add @pytest.mark.asyncio to 21 async test functions

### Error Path Tests
- Create test_downloader_error_paths.py with 19 new tests covering:
  - DownloadStreamControl state management (6 tests)
  - Downloader configuration and initialization (4 tests)
  - DownloadProgress dataclass validation (1 test)
  - Custom exception handling (2 tests)
  - Authentication header generation (3 tests)
  - Session management (3 tests)

### Documentation
- Update backend-testing-improvement-plan.md with Phase 1 completion status

## Test Results
- All 458 service tests pass
- No regressions introduced

Relates to backend testing improvement plan Phase 1
2026-02-11 10:29:21 +08:00

312 lines
11 KiB
Python

"""Error path tests for downloader module.
Tests HTTP error handling and network error scenarios.
"""
import asyncio
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import aiohttp
from py.services.downloader import Downloader, DownloadStalledError, DownloadRestartRequested
class TestDownloadStreamControl:
"""Test DownloadStreamControl functionality."""
def test_pause_clears_event(self):
"""Verify pause() clears the event."""
from py.services.downloader import DownloadStreamControl
control = DownloadStreamControl()
assert control.is_set() is True # Initially set
control.pause()
assert control.is_set() is False
assert control.is_paused() is True
def test_resume_sets_event(self):
"""Verify resume() sets the event."""
from py.services.downloader import DownloadStreamControl
control = DownloadStreamControl()
control.pause()
assert control.is_set() is False
control.resume()
assert control.is_set() is True
assert control.is_paused() is False
def test_reconnect_request_tracking(self):
"""Verify reconnect request tracking works correctly."""
from py.services.downloader import DownloadStreamControl
control = DownloadStreamControl()
assert control.has_reconnect_request() is False
control.request_reconnect()
assert control.has_reconnect_request() is True
# Consume the request
consumed = control.consume_reconnect_request()
assert consumed is True
assert control.has_reconnect_request() is False
def test_mark_progress_clears_reconnect(self):
"""Verify mark_progress clears reconnect requests."""
from py.services.downloader import DownloadStreamControl
control = DownloadStreamControl()
control.request_reconnect()
assert control.has_reconnect_request() is True
control.mark_progress()
assert control.has_reconnect_request() is False
assert control.last_progress_timestamp is not None
def test_time_since_last_progress(self):
"""Verify time_since_last_progress calculation."""
from py.services.downloader import DownloadStreamControl
import time
control = DownloadStreamControl()
# Initially None
assert control.time_since_last_progress() is None
# After marking progress
now = time.time()
control.mark_progress(timestamp=now)
elapsed = control.time_since_last_progress(now=now + 5)
assert elapsed == 5.0
@pytest.mark.asyncio
async def test_wait_for_resume(self):
"""Verify wait() blocks until resumed."""
from py.services.downloader import DownloadStreamControl
import asyncio
control = DownloadStreamControl()
control.pause()
# Start a task that will wait
wait_task = asyncio.create_task(control.wait())
# Give it a moment to start waiting
await asyncio.sleep(0.01)
assert not wait_task.done()
# Resume should unblock
control.resume()
await asyncio.wait_for(wait_task, timeout=0.1)
class TestDownloaderConfiguration:
"""Test downloader configuration and initialization."""
def test_downloader_singleton_pattern(self):
"""Verify Downloader follows singleton pattern."""
# Reset first
Downloader._instance = None
# Both should return same instance
async def get_instances():
instance1 = await Downloader.get_instance()
instance2 = await Downloader.get_instance()
return instance1, instance2
import asyncio
instance1, instance2 = asyncio.run(get_instances())
assert instance1 is instance2
# Cleanup
Downloader._instance = None
def test_default_configuration_values(self):
"""Verify default configuration values are set correctly."""
Downloader._instance = None
downloader = Downloader()
assert downloader.chunk_size == 4 * 1024 * 1024 # 4MB
assert downloader.max_retries == 5
assert downloader.base_delay == 2.0
assert downloader.session_timeout == 300
# Cleanup
Downloader._instance = None
def test_default_headers_include_user_agent(self):
"""Verify default headers include User-Agent."""
Downloader._instance = None
downloader = Downloader()
assert 'User-Agent' in downloader.default_headers
assert 'ComfyUI-LoRA-Manager' in downloader.default_headers['User-Agent']
assert downloader.default_headers['Accept-Encoding'] == 'identity'
# Cleanup
Downloader._instance = None
def test_stall_timeout_resolution(self):
"""Verify stall timeout is resolved correctly."""
Downloader._instance = None
downloader = Downloader()
timeout = downloader._resolve_stall_timeout()
# Should be at least 30 seconds
assert timeout >= 30.0
# Cleanup
Downloader._instance = None
class TestDownloadProgress:
"""Test DownloadProgress dataclass."""
def test_download_progress_creation(self):
"""Verify DownloadProgress can be created with correct values."""
from py.services.downloader import DownloadProgress
from datetime import datetime
progress = DownloadProgress(
percent_complete=50.0,
bytes_downloaded=500,
total_bytes=1000,
bytes_per_second=100.5,
timestamp=datetime.now().timestamp(),
)
assert progress.percent_complete == 50.0
assert progress.bytes_downloaded == 500
assert progress.total_bytes == 1000
assert progress.bytes_per_second == 100.5
assert progress.timestamp is not None
class TestDownloaderExceptions:
"""Test custom exception classes."""
def test_download_stalled_error(self):
"""Verify DownloadStalledError can be raised and caught."""
with pytest.raises(DownloadStalledError) as exc_info:
raise DownloadStalledError("Download stalled for 120 seconds")
assert "stalled" in str(exc_info.value).lower()
def test_download_restart_requested_error(self):
"""Verify DownloadRestartRequested can be raised and caught."""
with pytest.raises(DownloadRestartRequested) as exc_info:
raise DownloadRestartRequested("Reconnect requested after resume")
assert "reconnect" in str(exc_info.value).lower() or "restart" in str(exc_info.value).lower()
class TestDownloaderAuthHeaders:
"""Test authentication header generation."""
def test_get_auth_headers_without_auth(self):
"""Verify auth headers without authentication."""
Downloader._instance = None
downloader = Downloader()
headers = downloader._get_auth_headers(use_auth=False)
assert 'User-Agent' in headers
assert 'Authorization' not in headers
Downloader._instance = None
def test_get_auth_headers_with_auth_no_api_key(self, monkeypatch):
"""Verify auth headers with auth but no API key configured."""
Downloader._instance = None
downloader = Downloader()
# Mock settings manager to return no API key
mock_settings = MagicMock()
mock_settings.get.return_value = None
with patch('py.services.downloader.get_settings_manager', return_value=mock_settings):
headers = downloader._get_auth_headers(use_auth=True)
# Should still have User-Agent but no Authorization
assert 'User-Agent' in headers
assert 'Authorization' not in headers
Downloader._instance = None
def test_get_auth_headers_with_auth_and_api_key(self, monkeypatch):
"""Verify auth headers with auth and API key configured."""
Downloader._instance = None
downloader = Downloader()
# Mock settings manager to return API key
mock_settings = MagicMock()
mock_settings.get.return_value = "test-api-key-12345"
with patch('py.services.downloader.get_settings_manager', return_value=mock_settings):
headers = downloader._get_auth_headers(use_auth=True)
# Should have both User-Agent and Authorization
assert 'User-Agent' in headers
assert 'Authorization' in headers
assert 'test-api-key-12345' in headers['Authorization']
assert headers['Content-Type'] == 'application/json'
Downloader._instance = None
class TestDownloaderSessionManagement:
"""Test session management functionality."""
@pytest.mark.asyncio
async def test_should_refresh_session_when_none(self):
"""Verify session refresh is needed when session is None."""
Downloader._instance = None
downloader = Downloader()
# Initially should need refresh
assert downloader._should_refresh_session() is True
Downloader._instance = None
def test_should_not_refresh_new_session(self):
"""Verify new session doesn't need refresh."""
Downloader._instance = None
downloader = Downloader()
# Simulate a fresh session
downloader._session_created_at = MagicMock()
downloader._session = MagicMock()
# Mock datetime to return current time
from datetime import datetime, timedelta
current_time = datetime.now()
downloader._session_created_at = current_time
# Should not need refresh for new session
assert downloader._should_refresh_session() is False
Downloader._instance = None
def test_should_refresh_old_session(self):
"""Verify old session needs refresh."""
Downloader._instance = None
downloader = Downloader()
# Simulate an old session (older than timeout)
from datetime import datetime, timedelta
old_time = datetime.now() - timedelta(seconds=downloader.session_timeout + 1)
downloader._session_created_at = old_time
downloader._session = MagicMock()
# Should need refresh for old session
assert downloader._should_refresh_session() is True
Downloader._instance = None