Initial commit — StreamStack v1
Five-service streaming platform: auth, catalogue, streaming, ingest, thumbnailer. Includes React frontend served by nginx, NATS JetStream event bus, aiobotocore async S3, PyAV video metadata + thumbnail extraction, service-to-service JWT auth, and a full unit + e2e test suite.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from streamstack.catalogue.models import Base, MediaItem, Movie, TvSeries, YoutubeShow
|
||||
|
||||
|
||||
@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.mark.asyncio
|
||||
async def test_create_base_media_item(db_session: AsyncSession):
|
||||
item = MediaItem(title="Generic Media", s3_key="media/generic.mp4")
|
||||
db_session.add(item)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(item)
|
||||
assert item.id is not None
|
||||
assert item.media_type == "media"
|
||||
assert item.is_published is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_movie(db_session: AsyncSession):
|
||||
movie = Movie(
|
||||
title="Inception",
|
||||
s3_key="media/inception.mp4",
|
||||
director="Christopher Nolan",
|
||||
release_year=2010,
|
||||
mpaa_rating="PG-13",
|
||||
imdb_id="tt1375666",
|
||||
)
|
||||
db_session.add(movie)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(movie)
|
||||
assert movie.media_type == "movie"
|
||||
assert movie.director == "Christopher Nolan"
|
||||
assert movie.release_year == 2010
|
||||
assert movie.mpaa_rating == "PG-13"
|
||||
assert movie.imdb_id == "tt1375666"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tv_series(db_session: AsyncSession):
|
||||
episode = TvSeries(
|
||||
title="Breaking Bad S01E01",
|
||||
s3_key="media/bb-s01e01.mp4",
|
||||
show_name="Breaking Bad",
|
||||
season=1,
|
||||
episode=1,
|
||||
episode_title="Pilot",
|
||||
network="AMC",
|
||||
)
|
||||
db_session.add(episode)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(episode)
|
||||
assert episode.media_type == "tv_series"
|
||||
assert episode.show_name == "Breaking Bad"
|
||||
assert episode.season == 1
|
||||
assert episode.episode == 1
|
||||
assert episode.episode_title == "Pilot"
|
||||
assert episode.network == "AMC"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_youtube_show(db_session: AsyncSession):
|
||||
video = YoutubeShow(
|
||||
title="Python Tutorial",
|
||||
s3_key="media/yt-python.mp4",
|
||||
youtube_video_id="dQw4w9WgXcQ",
|
||||
youtube_channel_id="UCxxxxxx",
|
||||
channel_name="TechChannel",
|
||||
)
|
||||
db_session.add(video)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(video)
|
||||
assert video.media_type == "youtube_show"
|
||||
assert video.youtube_video_id == "dQw4w9WgXcQ"
|
||||
assert video.channel_name == "TechChannel"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_polymorphic_query_returns_correct_types(db_session: AsyncSession):
|
||||
db_session.add(Movie(title="Movie A", s3_key="media/a.mp4", is_published=True))
|
||||
db_session.add(TvSeries(title="Episode B", s3_key="media/b.mp4", is_published=True))
|
||||
db_session.add(YoutubeShow(title="Video C", s3_key="media/c.mp4", is_published=True))
|
||||
await db_session.commit()
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db_session.execute(select(MediaItem))
|
||||
items = result.scalars().all()
|
||||
assert len(items) == 3
|
||||
types = {i.media_type for i in items}
|
||||
assert types == {"movie", "tv_series", "youtube_show"}
|
||||
assert any(isinstance(i, Movie) for i in items)
|
||||
assert any(isinstance(i, TvSeries) for i in items)
|
||||
assert any(isinstance(i, YoutubeShow) for i in items)
|
||||
@@ -0,0 +1,192 @@
|
||||
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
|
||||
Reference in New Issue
Block a user