feat(credentials+downloads): real credential Verify button + live download-activity polling
Operator-flagged 2026-05-28, two asks.
**1. Credential Verify (was missing vs GS — and now actually verifies).**
GS's Verify was a stub (`TODO: implement actual verification` — just
stamped last_verified). FC does a real check, which matters given the
recent auth pain (subscribestar age cookie, HF host-only PHPSESSID):
- GalleryDLService.verify(url, platform, cookies_path, auth_token) runs
gallery-dl in `--simulate --range 1-1` mode (no download) against the
URL with the materialized credentials, then reuses _categorize_error:
returncode 0 / NO_NEW_CONTENT → valid; AUTH_ERROR → invalid; other →
inconclusive (reason surfaced). 45s timeout.
- POST /api/credentials/<platform>/verify picks an enabled Source for
the platform to probe, runs verify, and on success stamps
credential.last_verified (new CredentialService.mark_verified).
Returns {valid: bool|null, reason, last_verified?}. valid=null means
untestable (no credential, or no enabled source to point at).
- CredentialCard gains a Verify button (on credentialed cards) + a
result chip (Verified ✓ / Failed / Untestable) and a toast with the
reason. SettingsTab reloads on @verified so last_verified refreshes.
**2. Live download-activity feedback.** The Downloads tab was static —
no way to tell if downloads were succeeding without manually hitting
Refresh. It now auto-polls: stats every 4s, and the event list too
while anything is queued/running. Polling pauses when the tab is
backgrounded (document.hidden) and the list reload is skipped on idle
ticks to stay light. A pulsing "● live" indicator next to the stat
chips shows when auto-refresh is active (queued+running > 0); honors
prefers-reduced-motion.
Tests: verify endpoint — untestable with no credential, untestable with
no enabled source, valid+stamped on success (gallery-dl mocked), and
auth-failure reported without stamping.
This commit is contained in:
@@ -143,3 +143,84 @@ async def test_extension_key_wrong_rejects(client, ext_key):
|
||||
headers={"X-Extension-Key": "WRONG"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_no_credential_is_untestable(client):
|
||||
resp = await client.post("/api/credentials/patreon/verify")
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["valid"] is None
|
||||
assert "No credential" in body["reason"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_no_enabled_source_is_untestable(client):
|
||||
await client.post("/api/credentials", json={
|
||||
"platform": "patreon", "credential_type": "cookies", "data": _NETSCAPE,
|
||||
})
|
||||
resp = await client.post("/api/credentials/patreon/verify")
|
||||
body = await resp.get_json()
|
||||
assert body["valid"] is None
|
||||
assert "no enabled source" in body["reason"].lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_runs_gallery_dl_and_stamps_on_success(client, db, monkeypatch):
|
||||
from backend.app.models import Artist, Source
|
||||
from backend.app.services import gallery_dl as gdl_mod
|
||||
|
||||
# Stub the gallery-dl probe so the test doesn't shell out / hit network.
|
||||
async def _fake_verify(self, *args, **kwargs):
|
||||
return (True, "Credentials valid — the feed authenticated.")
|
||||
monkeypatch.setattr(gdl_mod.GalleryDLService, "verify", _fake_verify)
|
||||
|
||||
await client.post("/api/credentials", json={
|
||||
"platform": "patreon", "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="patreon",
|
||||
url="https://www.patreon.com/maewix", enabled=True, config_overrides={},
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
resp = await client.post("/api/credentials/patreon/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/patreon")).get_json()
|
||||
assert rec["last_verified"] is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_reports_auth_failure(client, db, monkeypatch):
|
||||
from backend.app.models import Artist, Source
|
||||
from backend.app.services import gallery_dl as gdl_mod
|
||||
|
||||
async def _fake_verify(self, *args, **kwargs):
|
||||
return (False, "Authentication failed — cookies may be expired or invalid")
|
||||
monkeypatch.setattr(gdl_mod.GalleryDLService, "verify", _fake_verify)
|
||||
|
||||
await client.post("/api/credentials", json={
|
||||
"platform": "hentaifoundry", "credential_type": "cookies", "data": _NETSCAPE,
|
||||
})
|
||||
artist = Artist(name="HolyMeh", slug="holymeh")
|
||||
db.add(artist)
|
||||
await db.flush()
|
||||
db.add(Source(
|
||||
artist_id=artist.id, platform="hentaifoundry",
|
||||
url="https://www.hentai-foundry.com/user/HolyMeh", enabled=True,
|
||||
config_overrides={},
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
resp = await client.post("/api/credentials/hentaifoundry/verify")
|
||||
body = await resp.get_json()
|
||||
assert body["valid"] is False
|
||||
assert "auth" in body["reason"].lower()
|
||||
assert body["last_verified"] is None
|
||||
|
||||
Reference in New Issue
Block a user