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,129 @@
|
||||
import io
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from streamstack.core.middleware import UserClaims, verify_jwt
|
||||
from streamstack.ingest.app import app
|
||||
|
||||
ADMIN = UserClaims(sub="admin-uuid", email="admin@test.com", roles=["admin"], jti="jti")
|
||||
VIEWER = UserClaims(sub="viewer-uuid", email="viewer@test.com", roles=["viewer"], jti="jti")
|
||||
|
||||
MOCK_CATALOGUE_ITEM = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"media_type": "movie",
|
||||
"title": "Test Movie",
|
||||
"s3_key": "media/test.mp4",
|
||||
"bucket": "media",
|
||||
"content_type": "video/mp4",
|
||||
"duration_seconds": 10.0,
|
||||
"size_bytes": 1024,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30.0,
|
||||
"codec": "h264",
|
||||
"thumbnail_s3_key": "thumbnails/test.jpg",
|
||||
"is_published": True,
|
||||
"tags": [],
|
||||
"director": None,
|
||||
"release_year": None,
|
||||
"mpaa_rating": None,
|
||||
"imdb_id": None,
|
||||
"description": None,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
MOCK_META = {
|
||||
"duration": 10.0,
|
||||
"streams": [
|
||||
{"type": "video", "codec": "h264", "width": 1920, "height": 1080, "fps": 30.0},
|
||||
{"type": "audio", "codec": "aac", "sample_rate": 44100, "channels": 2},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_s3():
|
||||
s3 = MagicMock()
|
||||
s3.upload_fileobj.return_value = None
|
||||
s3.head_object.return_value = {"ContentLength": 1024}
|
||||
s3.put_object.return_value = None
|
||||
with patch("streamstack.ingest.service.get_s3_client", return_value=s3):
|
||||
yield s3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_metadata():
|
||||
with patch("streamstack.ingest.service.extract_metadata", return_value=MOCK_META):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_thumbnail():
|
||||
with patch("streamstack.ingest.service._extract_thumbnail", return_value="thumbnails/test.jpg"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_catalogue():
|
||||
http_response = MagicMock()
|
||||
http_response.json.return_value = MOCK_CATALOGUE_ITEM
|
||||
http_response.raise_for_status.return_value = None
|
||||
|
||||
async_client = MagicMock()
|
||||
async_client.__aenter__ = AsyncMock(return_value=async_client)
|
||||
async_client.__aexit__ = AsyncMock(return_value=False)
|
||||
async_client.post = AsyncMock(return_value=http_response)
|
||||
|
||||
with patch("streamstack.ingest.service.httpx.AsyncClient", return_value=async_client):
|
||||
yield async_client
|
||||
|
||||
|
||||
async def _post_upload(ac, user):
|
||||
app.dependency_overrides[verify_jwt] = lambda: user
|
||||
try:
|
||||
return await ac.post(
|
||||
"/v1/ingest/upload",
|
||||
files={"file": ("test.mp4", io.BytesIO(b"fake video"), "video/mp4")},
|
||||
data={"media_type": "movie", "title": "Test Movie", "is_published": "true"},
|
||||
headers={"Authorization": "Bearer fake"},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(verify_jwt, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_success(mock_s3, mock_metadata, mock_thumbnail, mock_catalogue):
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
||||
resp = await _post_upload(ac, ADMIN)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["media_type"] == "movie"
|
||||
assert body["title"] == "Test Movie"
|
||||
assert body["duration_seconds"] == 10.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_requires_admin(mock_s3, mock_metadata, mock_thumbnail, mock_catalogue):
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
||||
resp = await _post_upload(ac, VIEWER)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_calls_catalogue_api(mock_s3, mock_metadata, mock_thumbnail, mock_catalogue):
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
||||
await _post_upload(ac, ADMIN)
|
||||
mock_catalogue.post.assert_called_once()
|
||||
call_kwargs = mock_catalogue.post.call_args
|
||||
payload = call_kwargs.kwargs["json"]
|
||||
assert payload["s3_key"].startswith("media/")
|
||||
assert payload["s3_key"].endswith(".mp4")
|
||||
assert payload["duration_seconds"] == 10.0
|
||||
assert payload["width"] == 1920
|
||||
assert payload["height"] == 1080
|
||||
assert payload["codec"] == "h264"
|
||||
assert payload["thumbnail_s3_key"] == "thumbnails/test.jpg"
|
||||
Reference in New Issue
Block a user