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.
106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from streamstack.streaming.app import app
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_nats_client():
|
|
nc = AsyncMock()
|
|
js = AsyncMock()
|
|
kv = AsyncMock()
|
|
nc.jetstream.return_value = js
|
|
js.key_value.return_value = kv
|
|
return nc, kv
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_resolve_token_valid():
|
|
with patch(
|
|
"streamstack.streaming.service.resolve_stream_token",
|
|
new=AsyncMock(return_value=("media-uuid-abc", "user-1")),
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_resolve_token_invalid():
|
|
with patch(
|
|
"streamstack.streaming.service.resolve_stream_token",
|
|
new=AsyncMock(return_value=None),
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_s3_full():
|
|
s3 = MagicMock()
|
|
s3.head_object.return_value = {
|
|
"ContentLength": 1024,
|
|
"ContentType": "video/mp4",
|
|
}
|
|
body = MagicMock()
|
|
body.read.side_effect = [b"x" * 512, b"x" * 512, b""]
|
|
s3.get_object.return_value = {"Body": body}
|
|
with patch("streamstack.streaming.service.get_s3_client", return_value=s3):
|
|
yield s3
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_nats_global(mock_nats_client):
|
|
nc, _ = mock_nats_client
|
|
with patch("streamstack.streaming.router.get_nats", return_value=nc):
|
|
yield nc
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_endpoint():
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
resp = await ac.get("/v1/health")
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {"status": "ok"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_not_found(mock_resolve_token_invalid, mock_nats_global):
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
resp = await ac.get("/v1/stream/invalid-token")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_full_response(mock_resolve_token_valid, mock_nats_global, mock_s3_full):
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
resp = await ac.get("/v1/stream/valid-token")
|
|
assert resp.status_code == 200
|
|
assert resp.headers["accept-ranges"] == "bytes"
|
|
assert resp.headers["content-length"] == "1024"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_range_request(mock_resolve_token_valid, mock_nats_global):
|
|
s3 = MagicMock()
|
|
s3.head_object.return_value = {"ContentLength": 10000, "ContentType": "video/mp4"}
|
|
body = MagicMock()
|
|
body.read.side_effect = [b"y" * 500, b""]
|
|
s3.get_object.return_value = {"Body": body}
|
|
with patch("streamstack.streaming.service.get_s3_client", return_value=s3):
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
resp = await ac.get("/v1/stream/valid-token", headers={"Range": "bytes=0-499"})
|
|
assert resp.status_code == 206
|
|
assert "content-range" in resp.headers
|
|
assert resp.headers["content-range"] == "bytes 0-499/10000"
|
|
assert resp.headers["content-length"] == "500"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stream_invalid_range_header(mock_resolve_token_valid, mock_nats_global):
|
|
s3 = MagicMock()
|
|
s3.head_object.return_value = {"ContentLength": 10000, "ContentType": "video/mp4"}
|
|
with patch("streamstack.streaming.service.get_s3_client", return_value=s3):
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
resp = await ac.get("/v1/stream/valid-token", headers={"Range": "bytes=abc-def"})
|
|
assert resp.status_code == 400
|