feat(downloads): native Patreon verify + uniform backend dispatch (plan #697)
The credential Verify button still ran gallery-dl --simulate for Patreon after the cutover — testing the wrong path (and prone to the vanity "Failed to extract campaign ID" the native resolver fixes). Wire it to the native ingester, behind a DRY dispatch so callers never branch on platform. - services/download_backends.py (new): the ONE place that knows which platforms are native vs gallery-dl. `uses_native_ingester(platform)` is the shared predicate; `verify_source_credential(...)` is the uniform probe (same (ok|None, message) contract for both backends). As a platform migrates, it moves into NATIVE_INGESTER_PLATFORMS here and BOTH download routing and verify switch together. - PatreonClient.verify_auth(campaign_id): one authenticated /api/posts fetch → True (valid) / False (401/403/HTML-login) / None (drift or network — inconclusive, not a credential verdict). - patreon_ingester.verify_patreon_credential(): resolve campaign id, then verify_auth — the verify counterpart to the download path. - patreon_resolver.resolve_campaign_id_for_source(): extracted the override / id:-URL / vanity resolution into ONE helper now shared by the download ingester and verify (download_service no longer carries its own copy + regex; −`import re`). - download_service: routes on uses_native_ingester() instead of inline `== "patreon"` (3 sites); uses the shared resolver. - api/credentials: calls verify_source_credential — no platform branch. Tests: verify_auth mapping, resolve_campaign_id_for_source (override/id:/ vanity/none), the dispatch predicate, verify_patreon_credential glue, credentials endpoint proves Patreon uses the native path (gallery-dl verify asserted not-called); repointed the gallery-dl verify test to subscribestar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,10 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.app.services.patreon_resolver import resolve_campaign_id
|
||||
from backend.app.services.patreon_resolver import (
|
||||
resolve_campaign_id,
|
||||
resolve_campaign_id_for_source,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -86,3 +89,48 @@ async def test_loads_cookies_file_when_present(tmp_path):
|
||||
assert result == "9"
|
||||
_, kwargs = mock_get.call_args
|
||||
assert kwargs.get("cookies") is not None
|
||||
|
||||
|
||||
# -- resolve_campaign_id_for_source (shared by download + verify) ----------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_for_source_uses_cached_override_without_lookup():
|
||||
with patch("backend.app.services.patreon_resolver.requests.get") as mock_get:
|
||||
cid, resolved = await resolve_campaign_id_for_source(
|
||||
"https://patreon.com/alice", None, {"patreon_campaign_id": "999"},
|
||||
)
|
||||
assert cid == "999"
|
||||
assert resolved is None # cached → no newly-resolved id to cache
|
||||
mock_get.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_for_source_extracts_id_url_without_lookup():
|
||||
with patch("backend.app.services.patreon_resolver.requests.get") as mock_get:
|
||||
cid, resolved = await resolve_campaign_id_for_source(
|
||||
"https://www.patreon.com/id:4242", None, {},
|
||||
)
|
||||
assert cid == "4242"
|
||||
assert resolved is None
|
||||
mock_get.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_for_source_resolves_vanity_and_reports_it():
|
||||
fake = MagicMock()
|
||||
fake.status_code = 200
|
||||
fake.json.return_value = {"data": [{"id": "777", "type": "campaign"}]}
|
||||
with patch("backend.app.services.patreon_resolver.requests.get", return_value=fake):
|
||||
cid, resolved = await resolve_campaign_id_for_source(
|
||||
"https://patreon.com/alice", None, {},
|
||||
)
|
||||
assert cid == "777"
|
||||
assert resolved == "777" # newly resolved → caller caches it
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_for_source_unresolvable_returns_none():
|
||||
cid, resolved = await resolve_campaign_id_for_source("not-a-patreon-url", None, {})
|
||||
assert cid is None
|
||||
assert resolved is None
|
||||
|
||||
Reference in New Issue
Block a user