feat(downloads): native Patreon verify + uniform backend dispatch (plan #697)
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 12s
CI / frontend-build (push) Successful in 19s
CI / integration (push) Successful in 2m59s

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:
2026-06-05 22:49:43 -04:00
parent ec43e823e1
commit 218bfebb92
12 changed files with 371 additions and 50 deletions
+41 -4
View File
@@ -155,6 +155,8 @@ async def test_verify_no_enabled_source_is_untestable(client):
@pytest.mark.asyncio
async def test_verify_runs_gallery_dl_and_stamps_on_success(client, db, monkeypatch):
"""A gallery-dl platform routes through GalleryDLService.verify; success
stamps last_verified (platform-agnostic endpoint behavior)."""
from backend.app.models import Artist, Source
from backend.app.services import gallery_dl as gdl_mod
@@ -163,6 +165,45 @@ async def test_verify_runs_gallery_dl_and_stamps_on_success(client, db, monkeypa
return (True, "Credentials valid — the feed authenticated.")
monkeypatch.setattr(gdl_mod.GalleryDLService, "verify", _fake_verify)
await client.post("/api/credentials", json={
"platform": "subscribestar", "credential_type": "cookies", "data": _NETSCAPE,
})
artist = Artist(name="Maewix", slug="maewix")
db.add(artist)
await db.flush()
db.add(Source(
artist_id=artist.id, platform="subscribestar",
url="https://www.subscribestar.com/maewix", enabled=True, config_overrides={},
))
await db.commit()
resp = await client.post("/api/credentials/subscribestar/verify")
body = await resp.get_json()
assert body["valid"] is True
assert body["last_verified"] is not None
# The stamp is persisted on the credential record.
rec = await (await client.get("/api/credentials/subscribestar")).get_json()
assert rec["last_verified"] is not None
@pytest.mark.asyncio
async def test_verify_patreon_uses_native_ingester_not_gallery_dl(client, db, monkeypatch):
"""plan #697 cutover: Patreon credential verify routes through the native
ingester (verify_patreon_credential), NOT gallery-dl --simulate."""
from backend.app.models import Artist, Source
from backend.app.services import gallery_dl as gdl_mod
from backend.app.services import patreon_ingester as pi_mod
async def _native_verify(url, cookies_path, overrides):
return (True, "Credentials valid — the Patreon feed authenticated.")
monkeypatch.setattr(pi_mod, "verify_patreon_credential", _native_verify)
# gallery-dl must NOT be consulted for Patreon.
async def _boom(self, *args, **kwargs):
raise AssertionError("gallery-dl verify must not run for patreon")
monkeypatch.setattr(gdl_mod.GalleryDLService, "verify", _boom)
await client.post("/api/credentials", json={
"platform": "patreon", "credential_type": "cookies", "data": _NETSCAPE,
})
@@ -180,10 +221,6 @@ async def test_verify_runs_gallery_dl_and_stamps_on_success(client, db, monkeypa
assert body["valid"] is True
assert body["last_verified"] is not None
# The stamp is persisted on the credential record.
rec = await (await client.get("/api/credentials/patreon")).get_json()
assert rec["last_verified"] is not None
@pytest.mark.asyncio
async def test_verify_reports_auth_failure(client, db, monkeypatch):