From ca8a27fee70f9b1ec5bcdf0271d3106dd1e9b229 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 20 May 2026 18:34:23 -0400 Subject: [PATCH] feat(fc3b): /api/platforms blueprint (read-only registry view) Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/api/__init__.py | 2 ++ backend/app/api/platforms.py | 12 ++++++++ tests/test_api_platforms.py | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 backend/app/api/platforms.py create mode 100644 tests/test_api_platforms.py diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 90b7468..1060622 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -22,6 +22,7 @@ def all_blueprints() -> list[Blueprint]: from .gallery import gallery_bp from .import_admin import import_admin_bp from .ml_admin import ml_admin_bp + from .platforms import platforms_bp from .provenance import provenance_bp from .settings import settings_bp from .showcase import showcase_bp @@ -44,4 +45,5 @@ def all_blueprints() -> list[Blueprint]: aliases_bp, ml_admin_bp, sources_bp, + platforms_bp, ] diff --git a/backend/app/api/platforms.py b/backend/app/api/platforms.py new file mode 100644 index 0000000..d22834e --- /dev/null +++ b/backend/app/api/platforms.py @@ -0,0 +1,12 @@ +"""FC-3b: /api/platforms — informational, no auth, matches GS shape.""" + +from quart import Blueprint, jsonify + +from ..services.platforms import PLATFORMS, to_dict + +platforms_bp = Blueprint("platforms", __name__, url_prefix="/api/platforms") + + +@platforms_bp.route("", methods=["GET"]) +async def list_platforms(): + return jsonify({"platforms": {k: to_dict(v) for k, v in PLATFORMS.items()}}) diff --git a/tests/test_api_platforms.py b/tests/test_api_platforms.py new file mode 100644 index 0000000..3f6c624 --- /dev/null +++ b/tests/test_api_platforms.py @@ -0,0 +1,53 @@ +import re + +import pytest + +from backend.app import create_app + +pytestmark = pytest.mark.integration + + +@pytest.fixture +async def app(): + return create_app() + + +@pytest.fixture +async def client(app): + async with app.test_client() as c: + yield c + + +@pytest.mark.asyncio +async def test_platforms_returns_gs_six(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", + } + assert "fanbox" not in platforms + + +@pytest.mark.asyncio +async def test_platforms_record_shape(client): + body = await (await client.get("/api/platforms")).get_json() + patreon = body["platforms"]["patreon"] + for key in ( + "key", "name", "description", "auth_type", "requires_auth", + "url_pattern", "url_examples", "default_config", "notes", + ): + assert key in patreon + assert patreon["auth_type"] == "cookies" + re.compile(patreon["url_pattern"]) + assert patreon["default_config"]["sleep"] == 3.0 + + +@pytest.mark.asyncio +async def test_platform_auth_types_match_gs(client): + body = await (await client.get("/api/platforms")).get_json() + assert body["platforms"]["discord"]["auth_type"] == "token" + assert body["platforms"]["pixiv"]["auth_type"] == "token" + assert body["platforms"]["patreon"]["auth_type"] == "cookies"