2309e9f43a
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.
130 lines
4.2 KiB
Python
130 lines
4.2 KiB
Python
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"
|