Sporalize-api / tests /test_video_stream.py
Shoraky's picture
Stream session video ranges directly from storage
a66365f
Raw History Blame Contribute Delete
6.15 kB
import unittest
from unittest.mock import AsyncMock, patch
from uuid import UUID
import httpx
from fastapi import HTTPException
from starlette.requests import Request
import video_stream
class ProbeStream(httpx.AsyncByteStream):
def __init__(self):
self.reads = 0
self.closed = False
async def __aiter__(self):
for chunk in (b"first", b"second"):
self.reads += 1
yield chunk
async def aclose(self):
self.closed = True
def request(method="GET", headers=None):
return Request({"type": "http", "method": method, "path": "/storage/test/videos/camera_1.mp4",
"headers": [(key.encode(), value.encode()) for key, value in (headers or {}).items()]})
class VideoStreamingTest(unittest.IsolatedAsyncioTestCase):
async def test_streams_ranges_lazily_and_closes_after_client_cancels(self):
body = ProbeStream()
seen = []
def handler(req):
seen.append(req)
return httpx.Response(206, headers={"Content-Range": "bytes 0-10/1000000", "Content-Length": "11",
"Accept-Ranges": "bytes", "Content-Type": "video/mp4"}, stream=body)
client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
with patch.object(video_stream.httpx, "AsyncClient", return_value=client):
response = await video_stream.video_response(request(headers={"range": "bytes=0-10", "cookie": "private-cookie"}),
"test/storage", "sessions/id/videos/camera_1.mp4", "storage-token")
self.assertEqual(body.reads, 0)
self.assertEqual(response.status_code, 206)
self.assertEqual(response.headers["content-range"], "bytes 0-10/1000000")
self.assertEqual(seen[0].headers["range"], "bytes=0-10")
self.assertNotIn("cookie", seen[0].headers)
self.assertEqual(await anext(response.body_iterator), b"first")
await response.body_iterator.aclose()
await response.background()
self.assertEqual(body.reads, 1)
self.assertTrue(body.closed)
self.assertTrue(client.is_closed)
async def test_cdn_redirect_strips_authorization_and_keeps_range(self):
seen = []
def handler(req):
seen.append(req)
if req.url.host == "huggingface.co":
return httpx.Response(302, headers={"Location": "https://cdn.example/video?signature=test"})
return httpx.Response(206, headers={"Content-Range": "bytes 0-1/10"}, stream=ProbeStream())
client = httpx.AsyncClient(transport=httpx.MockTransport(handler), follow_redirects=True)
with patch.object(video_stream.httpx, "AsyncClient", return_value=client):
response = await video_stream.video_response(request(headers={"range": "bytes=0-1"}),
"test/storage", "video.mp4", "storage-token")
self.assertEqual(seen[0].headers["authorization"], "Bearer storage-token")
self.assertNotIn("authorization", seen[1].headers)
self.assertEqual(seen[1].headers["range"], "bytes=0-1")
await response.background()
async def test_head_preserves_size_without_reading_video(self):
body = ProbeStream()
client = httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(
200, headers={"Content-Length": "1000000", "Accept-Ranges": "bytes"}, stream=body)))
with patch.object(video_stream.httpx, "AsyncClient", return_value=client):
response = await video_stream.video_response(request("HEAD"), "test/storage", "video.mp4", "token")
self.assertEqual(response.headers["content-length"], "1000000")
self.assertEqual(response.body, b"")
self.assertEqual(body.reads, 0)
self.assertTrue(client.is_closed)
async def test_invalid_range_keeps_total_size_and_omits_storage_error_body(self):
client = httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(
416, headers={"Content-Range": "bytes */100", "Content-Length": "100"}, stream=ProbeStream())))
with patch.object(video_stream.httpx, "AsyncClient", return_value=client):
response = await video_stream.video_response(request(headers={"range": "bytes=999-"}), "test/storage", "video.mp4", "token")
self.assertEqual(response.status_code, 416)
self.assertEqual(response.headers["content-range"], "bytes */100")
self.assertEqual(response.headers["content-length"], "0")
self.assertTrue(client.is_closed)
async def test_upstream_errors_close_connections_and_hide_storage_details(self):
for status, expected in ((404, 404), (401, 502), (503, 502)):
with self.subTest(status=status):
client = httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(status, stream=ProbeStream())))
with patch.object(video_stream.httpx, "AsyncClient", return_value=client):
with self.assertRaises(HTTPException) as error:
await video_stream.video_response(request(), "test/storage", "video.mp4", "private-token")
self.assertEqual(error.exception.status_code, expected)
self.assertNotIn("private-token", error.exception.detail)
self.assertTrue(client.is_closed)
async def test_video_route_checks_ownership_before_streaming(self):
import app as analysis_app
req = request()
req.state.user_id = "11111111-1111-4111-8111-111111111111"
req.state.is_admin = False
with patch.object(analysis_app, "session_row", side_effect=HTTPException(404, "Session not found")), \
patch.object(analysis_app, "video_response", new_callable=AsyncMock) as stream:
with self.assertRaises(HTTPException):
await analysis_app.video(UUID("22222222-2222-4222-8222-222222222222"), "camera_1.mp4", req)
stream.assert_not_called()
if __name__ == "__main__":
unittest.main()