mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
## 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
312 lines
11 KiB
Python
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
|