test: Added GET /topics/{category_slug}/{subtopic_slug} endpoint filter…

- "backend/routers/topics.py"
- "backend/tests/test_public_api.py"

GSD-Task: S01/T01
This commit is contained in:
jlightner 2026-03-31 05:59:36 +00:00
parent 348089d635
commit a7a038beea
2 changed files with 127 additions and 2 deletions

View file

@ -100,6 +100,61 @@ async def list_topics(
return result
@router.get("/{category_slug}/{subtopic_slug}", response_model=PaginatedResponse)
async def get_subtopic_techniques(
category_slug: str,
subtopic_slug: str,
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 50,
db: AsyncSession = Depends(get_session),
) -> PaginatedResponse:
"""Return technique pages filtered by sub-topic tag within a category.
``subtopic_slug`` is matched case-insensitively against elements of the
``topic_tags`` ARRAY column. ``category_slug`` is normalised and matched
against ``topic_category`` to scope results to the correct category.
Results are eager-loaded with the creator relation.
"""
category_name = category_slug.replace("-", " ").title()
subtopic_name = subtopic_slug.replace("-", " ")
# Filter: category matches AND subtopic_name appears in topic_tags.
# Tags are stored lowercase; slugs arrive lowercase. Use PG ARRAY contains (@>).
stmt = (
select(TechniquePage)
.where(TechniquePage.topic_category.ilike(category_name))
.where(TechniquePage.topic_tags.contains([subtopic_name]))
)
count_stmt = select(func.count()).select_from(stmt.subquery())
count_result = await db.execute(count_stmt)
total = count_result.scalar() or 0
stmt = (
stmt.options(selectinload(TechniquePage.creator))
.order_by(TechniquePage.title)
.offset(offset)
.limit(limit)
)
result = await db.execute(stmt)
pages = result.scalars().all()
items = []
for p in pages:
item = TechniquePageRead.model_validate(p)
if p.creator:
item.creator_name = p.creator.name
item.creator_slug = p.creator.slug
items.append(item)
return PaginatedResponse(
items=items,
total=total,
offset=offset,
limit=limit,
)
@router.get("/{category_slug}", response_model=PaginatedResponse)
async def get_topic_techniques(
category_slug: str,

View file

@ -65,7 +65,7 @@ async def _seed_full_data(db_engine) -> dict:
file_path="AlphaCreator/bass-tutorial.mp4",
duration_seconds=600,
content_type=ContentType.tutorial,
processing_status=ProcessingStatus.extracted,
processing_status=ProcessingStatus.complete,
)
video2 = SourceVideo(
creator_id=creator2.id,
@ -73,7 +73,7 @@ async def _seed_full_data(db_engine) -> dict:
file_path="BetaProducer/mixing-masterclass.mp4",
duration_seconds=1200,
content_type=ContentType.tutorial,
processing_status=ProcessingStatus.extracted,
processing_status=ProcessingStatus.complete,
)
session.add_all([video1, video2])
await session.flush()
@ -295,6 +295,76 @@ async def test_topics_with_no_technique_pages(client, db_engine):
assert st["creator_count"] == 0
# ── Sub-Topic Tests ──────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_get_subtopic_techniques(client, db_engine):
"""GET /topics/{category}/{subtopic} returns matching techniques with creator info."""
seed = await _seed_full_data(db_engine)
# "bass" tag appears on tp1 (Sound design) and tp3 (Synthesis).
# Filter to Sound design — only tp1 should match.
resp = await client.get(f"{TOPICS_URL}/sound-design/bass")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
assert len(data["items"]) == 1
item = data["items"][0]
assert item["slug"] == seed["tp1_slug"]
assert item["topic_category"] == "Sound design"
assert "bass" in item["topic_tags"]
# Creator relation should be populated
assert item["creator_name"] == seed["creator1_name"]
assert item["creator_slug"] == seed["creator1_slug"]
@pytest.mark.asyncio
async def test_get_subtopic_techniques_empty(client, db_engine):
"""GET /topics/{category}/{subtopic} returns empty list for nonexistent sub-topic."""
await _seed_full_data(db_engine)
resp = await client.get(f"{TOPICS_URL}/mixing/nonexistent-tag")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 0
assert data["items"] == []
@pytest.mark.asyncio
async def test_get_subtopic_techniques_pagination(client, db_engine):
"""GET /topics/{category}/{subtopic} respects offset and limit params."""
await _seed_full_data(db_engine)
# "bass" tag exists on both tp1 (Sound design) and tp3 (Synthesis).
# Synthesis has tp3 with tag "bass".
# Use Synthesis category so we only get tp3, then test pagination bounds.
# First: get all to verify baseline
resp = await client.get(f"{TOPICS_URL}/synthesis/bass")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
# Offset beyond total returns empty items but total is still correct
resp = await client.get(f"{TOPICS_URL}/synthesis/bass?offset=10&limit=10")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
assert data["items"] == []
# Limit=1 with multiple results in Sound design (bass matches tp1 only there)
# Let's test with a tag that matches multiple in one category.
# "bass" in Sound design matches tp1; only 1 result. Use limit=1 to confirm.
resp = await client.get(f"{TOPICS_URL}/sound-design/bass?limit=1")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
assert len(data["items"]) <= 1
# ── Creator Tests ────────────────────────────────────────────────────────────