"""Integration tests for the Patreon campaign-ID retry hook in downloads.py. These tests exercise _execute_download_with_retry — the pure async helper that owns the download → resolver → retry logic — without standing up a full Celery + DB environment. """ from types import SimpleNamespace from unittest.mock import AsyncMock from app.tasks import downloads as downloads_module from app.tasks.downloads import _execute_download_with_retry def _make_gdl_service(results): """Build a fake GalleryDLService whose .download() returns results in order.""" service = SimpleNamespace() service.download = AsyncMock(side_effect=list(results)) return service def _success(url="https://www.patreon.com/id:12345"): return SimpleNamespace( success=True, stdout="", stderr="", files_downloaded=3, url=url, error_type=None, error_message=None, ) def _campaign_id_failure(url="https://www.patreon.com/mstsu"): return SimpleNamespace( success=False, stdout="", stderr="[patreon][error] Failed to extract campaign ID", files_downloaded=0, url=url, error_type=None, error_message="Failed to extract campaign ID", ) def _other_failure(url="https://www.patreon.com/mstsu"): return SimpleNamespace( success=False, stdout="", stderr="HTTP 401 Unauthorized", files_downloaded=0, url=url, error_type=None, error_message="Authentication failed", ) async def test_happy_path_heal(monkeypatch): """Patreon source fails with campaign-ID error, resolver returns ID, retry succeeds.""" gdl = _make_gdl_service([_campaign_id_failure(), _success()]) monkeypatch.setattr( downloads_module, "resolve_campaign_id", AsyncMock(return_value="12345"), ) result = await _execute_download_with_retry( gdl_service=gdl, platform="patreon", source_url="https://www.patreon.com/mstsu", source_metadata={}, subscription_name="MSTSU", source_config=SimpleNamespace(), cookies_path="/data/cookies/patreon_cookies.txt", auth_token=None, ) assert result.dl_result.success is True assert result.resolved_campaign_id == "12345" assert gdl.download.call_count == 2 first_call_url = gdl.download.call_args_list[0].kwargs["url"] second_call_url = gdl.download.call_args_list[1].kwargs["url"] assert first_call_url == "https://www.patreon.com/mstsu" assert second_call_url == "https://www.patreon.com/id:12345" async def test_resolver_returns_none(monkeypatch): """Campaign-ID failure + resolver returns None → no retry, no metadata, original failure surfaced.""" first_failure = _campaign_id_failure() gdl = _make_gdl_service([first_failure]) monkeypatch.setattr( downloads_module, "resolve_campaign_id", AsyncMock(return_value=None), ) result = await _execute_download_with_retry( gdl_service=gdl, platform="patreon", source_url="https://www.patreon.com/mstsu", source_metadata={}, subscription_name="MSTSU", source_config=SimpleNamespace(), cookies_path="/data/cookies/patreon_cookies.txt", auth_token=None, ) assert result.dl_result is first_failure assert result.resolved_campaign_id is None assert gdl.download.call_count == 1 async def test_already_cached_campaign_id(monkeypatch): """Source has cached campaign_id → first download uses id-URL, no resolver call.""" gdl = _make_gdl_service([_success()]) resolver_mock = AsyncMock(return_value="should-not-be-called") monkeypatch.setattr(downloads_module, "resolve_campaign_id", resolver_mock) result = await _execute_download_with_retry( gdl_service=gdl, platform="patreon", source_url="https://www.patreon.com/mstsu", source_metadata={"patreon_campaign_id": "99999"}, subscription_name="MSTSU", source_config=SimpleNamespace(), cookies_path="/data/cookies/patreon_cookies.txt", auth_token=None, ) assert result.dl_result.success is True assert result.resolved_campaign_id is None assert gdl.download.call_count == 1 assert gdl.download.call_args.kwargs["url"] == "https://www.patreon.com/id:99999" resolver_mock.assert_not_called() async def test_non_patreon_failure_skips_retry(monkeypatch): """Non-Patreon source with similar-looking output → retry hook doesn't fire.""" failure = SimpleNamespace( success=False, stdout="", stderr="some error that contains failed to extract campaign id text", files_downloaded=0, url="https://subscribestar.adult/foo", error_type=None, error_message="unknown", ) gdl = _make_gdl_service([failure]) resolver_mock = AsyncMock(return_value="should-not-be-called") monkeypatch.setattr(downloads_module, "resolve_campaign_id", resolver_mock) result = await _execute_download_with_retry( gdl_service=gdl, platform="subscribestar", source_url="https://subscribestar.adult/foo", source_metadata={}, subscription_name="Foo", source_config=SimpleNamespace(), cookies_path=None, auth_token=None, ) assert result.dl_result is failure assert result.resolved_campaign_id is None resolver_mock.assert_not_called() assert gdl.download.call_count == 1 async def test_patreon_different_failure_skips_retry(monkeypatch): """Patreon source with non-campaign-ID error → no resolver, no retry.""" failure = _other_failure() gdl = _make_gdl_service([failure]) resolver_mock = AsyncMock(return_value="should-not-be-called") monkeypatch.setattr(downloads_module, "resolve_campaign_id", resolver_mock) result = await _execute_download_with_retry( gdl_service=gdl, platform="patreon", source_url="https://www.patreon.com/mstsu", source_metadata={}, subscription_name="MSTSU", source_config=SimpleNamespace(), cookies_path="/data/cookies/patreon_cookies.txt", auth_token=None, ) assert result.dl_result is failure assert result.resolved_campaign_id is None resolver_mock.assert_not_called() assert gdl.download.call_count == 1 async def test_unextractable_vanity_skips_retry(monkeypatch): """Patreon source whose URL can't yield a vanity → no resolver call.""" failure = _campaign_id_failure(url="https://www.patreon.com/") gdl = _make_gdl_service([failure]) resolver_mock = AsyncMock(return_value="should-not-be-called") monkeypatch.setattr(downloads_module, "resolve_campaign_id", resolver_mock) result = await _execute_download_with_retry( gdl_service=gdl, platform="patreon", source_url="https://www.patreon.com/", source_metadata={}, subscription_name="?", source_config=SimpleNamespace(), cookies_path=None, auth_token=None, ) assert result.dl_result is failure assert result.resolved_campaign_id is None resolver_mock.assert_not_called() assert gdl.download.call_count == 1 async def test_retry_itself_fails(monkeypatch): """Resolver succeeds but retry fails → cache id, surface retry's failure.""" first_failure = _campaign_id_failure() retry_failure = _other_failure() gdl = _make_gdl_service([first_failure, retry_failure]) monkeypatch.setattr( downloads_module, "resolve_campaign_id", AsyncMock(return_value="12345") ) result = await _execute_download_with_retry( gdl_service=gdl, platform="patreon", source_url="https://www.patreon.com/mstsu", source_metadata={}, subscription_name="MSTSU", source_config=SimpleNamespace(), cookies_path="/data/cookies/patreon_cookies.txt", auth_token=None, ) assert result.dl_result is retry_failure # retry's result wins assert result.resolved_campaign_id == "12345" # ID still cached assert gdl.download.call_count == 2