feat(gallery): add admin gallery curation endpoints and management page (Task 6)

Add super-admin-only backend endpoints for toggling is_gallery_featured and
gallery_sort_order on flows and scripts, plus a frontend GalleryManagementPage
with toggle switches, editable sort order fields, and name/featured filters.
13 integration tests; all pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 20:04:40 +00:00
parent 2b657fc4ac
commit 12a373a2a2
6 changed files with 1036 additions and 0 deletions

View File

@@ -0,0 +1,312 @@
"""Integration tests for admin gallery curation endpoints."""
import uuid
import pytest
from httpx import AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.tree import Tree
from app.models.script_template import ScriptTemplate, ScriptCategory
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
async def _create_tree(db: AsyncSession, admin_user_id: str) -> Tree:
"""Insert a minimal Tree directly into the DB."""
tree = Tree(
id=uuid.uuid4(),
name="Gallery Test Flow",
tree_type="troubleshooting",
visibility="public",
is_gallery_featured=False,
gallery_sort_order=0,
tree_structure={
"id": "root",
"type": "decision",
"question": "Test?",
"options": [],
"children": [],
},
author_id=uuid.UUID(admin_user_id),
)
db.add(tree)
await db.commit()
await db.refresh(tree)
return tree
async def _create_script(db: AsyncSession, admin_user_id: str) -> ScriptTemplate:
"""Insert a minimal ScriptTemplate directly into the DB."""
# Need a category first
category = ScriptCategory(
id=uuid.uuid4(),
name="Test Category",
slug=f"test-category-{uuid.uuid4().hex[:6]}",
)
db.add(category)
await db.flush()
script = ScriptTemplate(
id=uuid.uuid4(),
category_id=category.id,
name="Gallery Test Script",
slug=f"gallery-test-script-{uuid.uuid4().hex[:6]}",
script_body="Write-Host 'Test'",
is_gallery_featured=False,
gallery_sort_order=0,
created_by=uuid.UUID(admin_user_id) if admin_user_id else None,
)
db.add(script)
await db.commit()
await db.refresh(script)
return script
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestAdminGallery:
"""Test suite for admin gallery curation endpoints."""
# -- Auth guard tests --
@pytest.mark.asyncio
async def test_featured_list_requires_admin(
self, client: AsyncClient, auth_headers: dict
):
"""Non-admin users get 403 on featured list."""
response = await client.get("/api/v1/admin/gallery/featured", headers=auth_headers)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_feature_toggle_flow_requires_admin(
self, client: AsyncClient, auth_headers: dict
):
"""Non-admin users get 403 when trying to toggle flow feature."""
fake_id = str(uuid.uuid4())
response = await client.patch(
f"/api/v1/admin/gallery/flows/{fake_id}/feature",
json={"is_gallery_featured": True},
headers=auth_headers,
)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_feature_toggle_script_requires_admin(
self, client: AsyncClient, auth_headers: dict
):
"""Non-admin users get 403 when trying to toggle script feature."""
fake_id = str(uuid.uuid4())
response = await client.patch(
f"/api/v1/admin/gallery/scripts/{fake_id}/feature",
json={"is_gallery_featured": True},
headers=auth_headers,
)
assert response.status_code == 403
# -- Feature toggle tests --
@pytest.mark.asyncio
async def test_toggle_flow_featured_on(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""Admin can feature a flow."""
tree = await _create_tree(test_db, test_admin["user_data"]["id"])
response = await client.patch(
f"/api/v1/admin/gallery/flows/{tree.id}/feature",
json={"is_gallery_featured": True},
headers=admin_auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["is_gallery_featured"] is True
assert data["id"] == str(tree.id)
@pytest.mark.asyncio
async def test_toggle_flow_featured_off(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""Admin can un-feature a previously featured flow."""
tree = await _create_tree(test_db, test_admin["user_data"]["id"])
# Feature it first
tree.is_gallery_featured = True
await test_db.commit()
response = await client.patch(
f"/api/v1/admin/gallery/flows/{tree.id}/feature",
json={"is_gallery_featured": False},
headers=admin_auth_headers,
)
assert response.status_code == 200
assert response.json()["is_gallery_featured"] is False
@pytest.mark.asyncio
async def test_toggle_flow_not_found(
self, client: AsyncClient, admin_auth_headers: dict
):
"""Returns 404 when flow ID doesn't exist."""
fake_id = str(uuid.uuid4())
response = await client.patch(
f"/api/v1/admin/gallery/flows/{fake_id}/feature",
json={"is_gallery_featured": True},
headers=admin_auth_headers,
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_toggle_script_featured_on(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""Admin can feature a script."""
script = await _create_script(test_db, test_admin["user_data"]["id"])
response = await client.patch(
f"/api/v1/admin/gallery/scripts/{script.id}/feature",
json={"is_gallery_featured": True},
headers=admin_auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["is_gallery_featured"] is True
assert data["id"] == str(script.id)
@pytest.mark.asyncio
async def test_toggle_script_not_found(
self, client: AsyncClient, admin_auth_headers: dict
):
"""Returns 404 when script ID doesn't exist."""
fake_id = str(uuid.uuid4())
response = await client.patch(
f"/api/v1/admin/gallery/scripts/{fake_id}/feature",
json={"is_gallery_featured": True},
headers=admin_auth_headers,
)
assert response.status_code == 404
# -- Sort order tests --
@pytest.mark.asyncio
async def test_update_flow_sort_order(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""Admin can update gallery sort order for a flow."""
tree = await _create_tree(test_db, test_admin["user_data"]["id"])
response = await client.patch(
f"/api/v1/admin/gallery/flows/{tree.id}/sort-order",
json={"gallery_sort_order": 42},
headers=admin_auth_headers,
)
assert response.status_code == 200
assert response.json()["gallery_sort_order"] == 42
@pytest.mark.asyncio
async def test_update_script_sort_order(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""Admin can update gallery sort order for a script."""
script = await _create_script(test_db, test_admin["user_data"]["id"])
response = await client.patch(
f"/api/v1/admin/gallery/scripts/{script.id}/sort-order",
json={"gallery_sort_order": 7},
headers=admin_auth_headers,
)
assert response.status_code == 200
assert response.json()["gallery_sort_order"] == 7
# -- Featured list tests --
@pytest.mark.asyncio
async def test_featured_list_returns_only_featured(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""GET /featured returns only items where is_gallery_featured=True."""
# Create two flows: one featured, one not
featured_tree = await _create_tree(test_db, test_admin["user_data"]["id"])
_unfeatured_tree = await _create_tree(test_db, test_admin["user_data"]["id"])
_unfeatured_tree.name = "Unfeatured Flow"
# Feature only the first
featured_tree.is_gallery_featured = True
await test_db.commit()
# Create two scripts: one featured, one not
featured_script = await _create_script(test_db, test_admin["user_data"]["id"])
_unfeatured_script = await _create_script(test_db, test_admin["user_data"]["id"])
featured_script.is_gallery_featured = True
await test_db.commit()
response = await client.get(
"/api/v1/admin/gallery/featured", headers=admin_auth_headers
)
assert response.status_code == 200
data = response.json()
flow_ids = [f["id"] for f in data["flows"]]
script_ids = [s["id"] for s in data["scripts"]]
assert str(featured_tree.id) in flow_ids
assert str(_unfeatured_tree.id) not in flow_ids
assert str(featured_script.id) in script_ids
assert str(_unfeatured_script.id) not in script_ids
@pytest.mark.asyncio
async def test_items_list_requires_admin(
self, client: AsyncClient, auth_headers: dict
):
"""Non-admin users get 403 on all items list."""
response = await client.get("/api/v1/admin/gallery/items", headers=auth_headers)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_items_list_returns_all(
self,
client: AsyncClient,
admin_auth_headers: dict,
test_admin: dict,
test_db: AsyncSession,
):
"""GET /items returns all flows and scripts regardless of featured status."""
tree = await _create_tree(test_db, test_admin["user_data"]["id"])
response = await client.get(
"/api/v1/admin/gallery/items", headers=admin_auth_headers
)
assert response.status_code == 200
data = response.json()
assert "flows" in data
assert "scripts" in data
flow_ids = [f["id"] for f in data["flows"]]
assert str(tree.id) in flow_ids