From d89918fd2ad095a51b6677d30fc4649a2bc92bbe Mon Sep 17 00:00:00 2001 From: Shane Maynard Date: Sat, 29 Aug 2026 16:09:42 -0400 Subject: [PATCH] test: stream-proxy test works against remote deployments The fake audio upstream bound the test host's loopback, which the Unraid media server cannot reach (and its LAN route is firewalled). Local runs keep the controlled upstream; remote runs use a public reference mp3 and assert Range semantics without byte equality. docker-exec MCP probes skip on remote targets (they necessarily exercise the local container). --- tests/test_media.py | 86 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 15 deletions(-) diff --git a/tests/test_media.py b/tests/test_media.py index 6270d98..7ef9864 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -13,11 +13,12 @@ import threading import time import uuid from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse import httpx import pytest -from conftest import TOKEN_URL, docker_exec, IS_LOCAL_DEPLOYMENT +from conftest import TOKEN_URL, docker_exec, IS_LOCAL_DEPLOYMENT, WEB_BASE_URL MEDIA_API = "/media/api" TEST_ROOM = f"media-test-{uuid.uuid4().hex[:8]}" @@ -26,11 +27,13 @@ TEST_ROOM = f"media-test-{uuid.uuid4().hex[:8]}" # ── local upstream for stream-proxy tests ─────────────────────────────────── def _serve_fake_audio(payload: bytes): - """Tiny Range-capable HTTP server on an ephemeral host port. + """Tiny Range-capable HTTP server on an ephemeral port. - The container uses host networking, so the media server inside the - container reaches it on 127.0.0.1. + Binds 0.0.0.0 and advertises this host's LAN IP so the deployment host's + media server (which may be a different machine — Unraid) can reach it + over the LAN when fetching external stream URLs. """ + import socket class Handler(BaseHTTPRequestHandler): def do_GET(self): @@ -57,10 +60,19 @@ def _serve_fake_audio(payload: bytes): def log_message(self, *args): pass - server = HTTPServer(("127.0.0.1", 0), Handler) + server = HTTPServer(("0.0.0.0", 0), Handler) port = server.server_address[1] + # The local IP toward the deployment host (works across the LAN). + probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + probe.connect((urlparse(WEB_BASE_URL).hostname or "192.168.86.2", 9178)) + lan_ip = probe.getsockname()[0] + except OSError: + lan_ip = "127.0.0.1" + finally: + probe.close() threading.Thread(target=server.serve_forever, daemon=True).start() - return server, f"http://127.0.0.1:{port}/audio.mp3" + return server, f"http://{lan_ip}:{port}/audio.mp3" # ── health + basic API shape ──────────────────────────────────────────────── @@ -171,27 +183,60 @@ def test_media_player_state_flow(client): def test_media_stream_proxy_and_range(client): """External audio URLs must be reachable same-origin (HTTPS) and support - Range passthrough so the browser can seek progressive streams.""" - payload = b"0123456789" * 100 - upstream, url = _serve_fake_audio(payload) + Range passthrough so the browser can seek progressive streams. + + Local deployments test against a controlled fake upstream; remote + deployments (Unraid) cannot reach the test host's ephemeral ports, so a + public reference mp3 is used and Range semantics are asserted without + byte equality.""" + if IS_LOCAL_DEPLOYMENT: + payload = b"0123456789" * 100 + upstream, url = _serve_fake_audio(payload) + track_id = None + try: + track_id = client.post( + f"{MEDIA_API}/streams", json={"name": "Range FM", "url": url} + ).json()["track"]["id"] + + full = client.get(f"{MEDIA_API}/stream/{track_id}") + assert full.status_code == 200 + assert full.content == payload + + ranged = client.get( + f"{MEDIA_API}/stream/{track_id}", headers={"Range": "bytes=100-199"} + ) + assert ranged.status_code == 206 + assert ranged.content == payload[100:200] + assert ranged.headers["content-range"] == f"bytes 100-199/{len(payload)}" + finally: + upstream.shutdown() + if track_id: + try: + client.delete(f"{MEDIA_API}/tracks/{track_id}") + except httpx.HTTPError: + pass + return + + # Remote deployment: public reference audio (SoundHelix). + url = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" track_id = None try: track_id = client.post( - f"{MEDIA_API}/streams", json={"name": "Range FM", "url": url} + f"{MEDIA_API}/streams", json={"name": "Range FM public", "url": url} ).json()["track"]["id"] full = client.get(f"{MEDIA_API}/stream/{track_id}") assert full.status_code == 200 - assert full.content == payload + assert full.headers.get("content-type", "").startswith("audio/") + assert len(full.content) > 100_000 ranged = client.get( - f"{MEDIA_API}/stream/{track_id}", headers={"Range": "bytes=100-199"} + f"{MEDIA_API}/stream/{track_id}", headers={"Range": "bytes=0-1023"} ) assert ranged.status_code == 206 - assert ranged.content == payload[100:200] - assert ranged.headers["content-range"] == f"bytes 100-199/{len(payload)}" + assert len(ranged.content) == 1024 + assert (ranged.headers.get("content-range") or "").startswith("bytes 0-1023/") finally: - upstream.shutdown() if track_id: try: client.delete(f"{MEDIA_API}/tracks/{track_id}") @@ -286,13 +331,22 @@ def test_token_response_includes_room(): # ── Hope's media MCP toolset ──────────────────────────────────────────────── +# These spawn the MCP server inside the LOCAL dev container (docker exec), +# whose media server is the local one — they can only be consistent when +# the test target IS the local deployment. MEDIA_MCP_ENV = { "MEDIA_SERVER_URL": "http://127.0.0.1:8093", "MEDIA_ROOM": "mcp-test-room", } +needs_local_container = pytest.mark.skipif( + not IS_LOCAL_DEPLOYMENT, + reason="docker-exec MCP probes run against the local dev container", +) + +@needs_local_container def test_mcp_media_list_library(mcp_client_factory): text, is_error = mcp_client_factory( "media_mcp.py", "list_library", {}, env=MEDIA_MCP_ENV @@ -301,6 +355,7 @@ def test_mcp_media_list_library(mcp_client_factory): assert "library" in text.lower() +@needs_local_container def test_mcp_media_add_stream_and_now_playing(mcp_client_factory, client): upstream, url = _serve_fake_audio(b"\x00" * 16) try: @@ -335,6 +390,7 @@ def test_mcp_media_add_stream_and_now_playing(mcp_client_factory, client): upstream.shutdown() +@needs_local_container def test_mcp_media_resume_without_load(mcp_client_factory, client): """Errors from the media server reach the LLM as tool text (not MCP errors) so Hope can explain them naturally."""