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