refactor(platforms): retire deviantart end-to-end (#3069)
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m43s
Executes the 2026-07-05 product decision (FC downloaders = art-dedicated
services only), which removed Twitter/X and Bluesky but left deviantart
fully wired for seven weeks — the half-retired state rule 22 exists to
prevent.
Removed: the PlatformInfo module and its registry entry, the gallery-dl
extractor block, extension_service's artist-page pattern, the extension's
PLATFORMS + PLATFORM_ARTIST_PATTERNS entries, its manifest host permission
and content-script match, the frontend icon/colour/label, and the operator-
facing "supported platforms" list that still advertised it.
Two judgment calls, both recorded in migration 0088:
* existing `source` rows are DISABLED, not deleted. The row is the only
record of the artist's DeviantArt URL. Disabling is also required for
correctness rather than tidiness: with the platform unregistered the
download path falls through to gallery-dl, which carries its OWN
deviantart extractor, so an enabled row would have kept downloading
from a dropped platform.
* the `credential` row IS deleted — a live session cookie for a site FC
will never call again.
Adds the invariant whose absence is why manifest.json drifted in the first
place: nothing tied its domain lists back to the platform table. The
extension suite now asserts both directions, plus that no host permission
belongs to an unclaimed domain (`*://*/*` exempted — FC is self-hosted at
an operator-chosen URL the extension cannot enumerate).
Extension version 1.0.10 -> 1.0.11: ci.yml's guard hard-fails a packaged
extension change without a bump. No release is cut — build.yml's
sign-extension job only runs on main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,6 @@ async def test_resolve_artist_name_dispatches_per_platform(db, monkeypatch):
|
||||
("https://www.subscribestar.com/foobar", "subscribestar", "foobar"),
|
||||
("https://subscribestar.adult/foobar", "subscribestar", "foobar"),
|
||||
("https://www.hentai-foundry.com/user/Foo/profile", "hentaifoundry", "Foo"),
|
||||
("https://www.deviantart.com/baz", "deviantart", "baz"),
|
||||
("https://www.pixiv.net/users/12345", "pixiv", "12345"),
|
||||
("https://www.pixiv.net/en/users/12345", "pixiv", "12345"),
|
||||
])
|
||||
@@ -160,6 +159,23 @@ async def test_quick_add_source_unknown_url_400(client, ext_key):
|
||||
assert "known" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_source_rejects_retired_deviantart(client, ext_key):
|
||||
"""#3069: a DeviantArt creator URL used to derive cleanly. Now that the
|
||||
platform is retired, the extension's own gate should never offer the
|
||||
button — but a stale content script on an un-updated browser still can,
|
||||
so the backend has to refuse it rather than create an unusable source."""
|
||||
resp = await client.post(
|
||||
"/api/extension/quick-add-source",
|
||||
json={"url": "https://www.deviantart.com/baz"},
|
||||
headers={"X-Extension-Key": ext_key},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
body = await resp.get_json()
|
||||
assert body["error"] == "unknown_platform"
|
||||
assert "deviantart" not in body["known"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_source_invalid_url_400(client, ext_key):
|
||||
resp = await client.post(
|
||||
|
||||
@@ -6,16 +6,17 @@ pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_platforms_returns_gs_six(client):
|
||||
async def test_platforms_returns_gs_five(client):
|
||||
resp = await client.get("/api/platforms")
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
platforms = body["platforms"]
|
||||
assert set(platforms.keys()) == {
|
||||
"patreon", "subscribestar", "hentaifoundry",
|
||||
"discord", "pixiv", "deviantart",
|
||||
"discord", "pixiv",
|
||||
}
|
||||
assert "fanbox" not in platforms
|
||||
assert "deviantart" not in platforms # retired at #3069
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -154,7 +154,7 @@ async def test_list_platform_filter_excludes_no_source(db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_platform_filter_excludes_wrong_platform(db):
|
||||
a = await _seed_artist(db, "alice-wplat")
|
||||
await _seed_source(db, a.id, "deviantart", "https://d/alice-wp")
|
||||
await _seed_source(db, a.id, "discord", "https://d/alice-wp")
|
||||
await db.commit()
|
||||
|
||||
page = await ArtistDirectoryService(db).list_artists(
|
||||
|
||||
@@ -19,7 +19,7 @@ def test_native_platforms():
|
||||
def test_gallery_dl_platforms_are_not_native():
|
||||
# The platforms still served by gallery-dl must NOT route to the native
|
||||
# ingester — guards an accidental over-broad migration.
|
||||
for platform in ("hentaifoundry", "discord", "deviantart"):
|
||||
for platform in ("hentaifoundry", "discord"):
|
||||
assert uses_native_ingester(platform) is False
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ pytestmark = pytest.mark.integration
|
||||
|
||||
def test_non_serialized_platform_has_no_lock():
|
||||
# gallery-dl platforms aren't capped — they get no lock at all.
|
||||
assert platform_lock("deviantart", ttl_seconds=60) is None
|
||||
assert platform_lock("hentaifoundry", ttl_seconds=60) is None
|
||||
assert platform_lock("discord", ttl_seconds=60) is None
|
||||
|
||||
|
||||
def test_subscribestar_is_serialized():
|
||||
|
||||
@@ -11,10 +11,10 @@ from backend.app.services.platforms import (
|
||||
)
|
||||
|
||||
|
||||
def test_known_platform_keys_is_gs_six():
|
||||
def test_known_platform_keys_is_gs_five():
|
||||
assert known_platform_keys() == frozenset({
|
||||
"patreon", "subscribestar", "hentaifoundry",
|
||||
"discord", "pixiv", "deviantart",
|
||||
"discord", "pixiv",
|
||||
})
|
||||
|
||||
|
||||
@@ -23,6 +23,15 @@ def test_fanbox_not_in_registry():
|
||||
assert "fanbox" not in PLATFORMS
|
||||
|
||||
|
||||
def test_deviantart_is_retired():
|
||||
# #3069 executed the 2026-07-05 drop decision (FC downloaders = ART-
|
||||
# DEDICATED services only). The registry is what /api/platforms, the
|
||||
# source validator and the credential validator all read, so its absence
|
||||
# here is what actually retires the platform everywhere else.
|
||||
assert "deviantart" not in PLATFORMS
|
||||
assert auth_type_for("deviantart") is None
|
||||
|
||||
|
||||
def test_auth_type_for_known_and_unknown():
|
||||
assert auth_type_for("patreon") == "cookies"
|
||||
assert auth_type_for("discord") == "token"
|
||||
|
||||
@@ -169,7 +169,7 @@ async def test_scroll_filters_by_artist(db):
|
||||
async def test_scroll_filters_by_platform(db):
|
||||
artist = await _seed_artist(db, "alice-platf")
|
||||
src_p = await _seed_source(db, artist.id, "patreon", "https://p/alice-pp")
|
||||
src_d = await _seed_source(db, artist.id, "deviantart", "https://d/alice-dd")
|
||||
src_d = await _seed_source(db, artist.id, "discord", "https://d/alice-dd")
|
||||
now = datetime.now(UTC)
|
||||
pp = await _seed_post(db, src_p.id, external_id="PP", post_date=now)
|
||||
await _seed_post(db, src_d.id, external_id="PD", post_date=now)
|
||||
@@ -234,7 +234,7 @@ async def test_scroll_combined_artist_and_platform(db):
|
||||
alice = await _seed_artist(db, "alice-combo")
|
||||
bob = await _seed_artist(db, "bob-combo")
|
||||
src_alice_patreon = await _seed_source(db, alice.id, "patreon", "https://p/alice-c")
|
||||
src_alice_da = await _seed_source(db, alice.id, "deviantart", "https://d/alice-c")
|
||||
src_alice_da = await _seed_source(db, alice.id, "discord", "https://d/alice-c")
|
||||
src_bob_patreon = await _seed_source(db, bob.id, "patreon", "https://p/bob-c")
|
||||
now = datetime.now(UTC)
|
||||
target = await _seed_post(
|
||||
|
||||
@@ -23,12 +23,14 @@ async def _artist(db, name="Alice"):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_known_platforms_is_gs_six(db):
|
||||
async def test_known_platforms_is_gs_five(db):
|
||||
assert KNOWN_PLATFORMS == frozenset({
|
||||
"patreon", "subscribestar", "hentaifoundry",
|
||||
"discord", "pixiv", "deviantart",
|
||||
"discord", "pixiv",
|
||||
})
|
||||
assert "fanbox" not in KNOWN_PLATFORMS
|
||||
# Retired at #3069 — a source can no longer be created on it.
|
||||
assert "deviantart" not in KNOWN_PLATFORMS
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user