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:
parent
348089d635
commit
a7a038beea
2 changed files with 127 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue