from unittest.mock import AsyncMock, patch import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from streamstack.catalogue.app import app from streamstack.catalogue.models import Base, MediaItem from streamstack.core.db import get_db from streamstack.core.middleware import UserClaims, verify_jwt @pytest.fixture async def db_session(): engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) session_factory = async_sessionmaker(engine, expire_on_commit=False) async with session_factory() as session: yield session await engine.dispose() @pytest.fixture def admin_user(): return UserClaims(sub="admin-uuid", email="admin@test.com", roles=["admin"], jti="test-jti") @pytest.fixture def viewer_user(): return UserClaims(sub="viewer-uuid", email="viewer@test.com", roles=["viewer"], jti="test-jti") @pytest.fixture def mock_nats(): nc = AsyncMock() with patch("streamstack.catalogue.service.get_nats", return_value=nc): yield nc @pytest.fixture async def test_client(db_session, mock_nats): app.dependency_overrides[get_db] = lambda: db_session async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: yield ac app.dependency_overrides.clear() @pytest.mark.asyncio async def test_health(test_client): resp = await test_client.get("/v1/health") assert resp.status_code == 200 @pytest.mark.asyncio async def test_list_empty(test_client): resp = await test_client.get("/v1/catalogue/") assert resp.status_code == 200 body = resp.json() assert body["items"] == [] assert body["total"] == 0 @pytest.mark.asyncio async def test_create_requires_admin(test_client, viewer_user, db_session): app.dependency_overrides[get_db] = lambda: db_session app.dependency_overrides[verify_jwt] = lambda: viewer_user try: resp = await test_client.post( "/v1/catalogue/", json={"media_type": "media", "title": "Test", "s3_key": "media/test.mp4"}, headers={"Authorization": "Bearer fake"}, ) finally: app.dependency_overrides.pop(verify_jwt, None) assert resp.status_code == 403 @pytest.mark.asyncio async def test_create_movie(test_client, admin_user, db_session): app.dependency_overrides[get_db] = lambda: db_session app.dependency_overrides[verify_jwt] = lambda: admin_user try: resp = await test_client.post( "/v1/catalogue/", json={ "media_type": "movie", "title": "Inception", "s3_key": "media/inception.mp4", "director": "Christopher Nolan", "release_year": 2010, "is_published": True, }, headers={"Authorization": "Bearer fake"}, ) finally: app.dependency_overrides.pop(verify_jwt, None) assert resp.status_code == 201 body = resp.json() assert body["media_type"] == "movie" assert body["director"] == "Christopher Nolan" assert body["release_year"] == 2010 @pytest.mark.asyncio async def test_create_tv_series(test_client, admin_user, db_session): app.dependency_overrides[get_db] = lambda: db_session app.dependency_overrides[verify_jwt] = lambda: admin_user try: resp = await test_client.post( "/v1/catalogue/", json={ "media_type": "tv_series", "title": "Breaking Bad S01E01", "s3_key": "media/bb-s01e01.mp4", "show_name": "Breaking Bad", "season": 1, "episode": 1, "is_published": True, }, headers={"Authorization": "Bearer fake"}, ) finally: app.dependency_overrides.pop(verify_jwt, None) assert resp.status_code == 201 body = resp.json() assert body["media_type"] == "tv_series" assert body["show_name"] == "Breaking Bad" assert body["season"] == 1 @pytest.mark.asyncio async def test_create_youtube_show(test_client, admin_user, db_session): app.dependency_overrides[get_db] = lambda: db_session app.dependency_overrides[verify_jwt] = lambda: admin_user try: resp = await test_client.post( "/v1/catalogue/", json={ "media_type": "youtube_show", "title": "Python Tutorial", "s3_key": "media/yt-python.mp4", "youtube_video_id": "dQw4w9WgXcQ", "channel_name": "TechChannel", "is_published": True, }, headers={"Authorization": "Bearer fake"}, ) finally: app.dependency_overrides.pop(verify_jwt, None) assert resp.status_code == 201 body = resp.json() assert body["media_type"] == "youtube_show" assert body["youtube_video_id"] == "dQw4w9WgXcQ" @pytest.mark.asyncio async def test_list_filtered_by_media_type(test_client, admin_user, db_session): app.dependency_overrides[get_db] = lambda: db_session app.dependency_overrides[verify_jwt] = lambda: admin_user try: await test_client.post( "/v1/catalogue/", json={"media_type": "movie", "title": "M1", "s3_key": "m/m1.mp4", "is_published": True}, headers={"Authorization": "Bearer fake"}, ) await test_client.post( "/v1/catalogue/", json={ "media_type": "tv_series", "title": "T1", "s3_key": "m/t1.mp4", "is_published": True, }, headers={"Authorization": "Bearer fake"}, ) finally: app.dependency_overrides.pop(verify_jwt, None) resp = await test_client.get("/v1/catalogue/?media_type=movie") assert resp.status_code == 200 body = resp.json() assert body["total"] == 1 assert body["items"][0]["media_type"] == "movie" @pytest.mark.asyncio async def test_get_unpublished_returns_404(test_client, db_session): item = MediaItem(title="Hidden", s3_key="media/hidden.mp4", is_published=False) db_session.add(item) await db_session.commit() resp = await test_client.get(f"/v1/catalogue/{item.id}") assert resp.status_code == 404