Files
vllm-omni/tests/entrypoints/openai_api/test_decode_audio_url.py
T

272 lines
9.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for decode_audio_url helper and _AudioFileMediaIO."""
import asyncio
import base64
import os
import pytest
from vllm.multimodal.media import MediaConnector
from vllm_omni.entrypoints.openai.errors import InvalidInputReferenceError
from vllm_omni.entrypoints.openai.video_api_utils import (
_AudioFileMediaIO,
decode_audio_url,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _run(coro):
return asyncio.run(coro)
def _cleanup(path):
if path and os.path.exists(path):
os.unlink(path)
def _permissive_connector():
return MediaConnector(
allowed_local_media_path="",
allowed_media_domains=None,
)
def _restricted_connector(allowed_domains=None):
return MediaConnector(
allowed_local_media_path="",
allowed_media_domains=allowed_domains or [],
)
# ---------------------------------------------------------------------------
# _AudioFileMediaIO direct tests
# ---------------------------------------------------------------------------
class TestAudioFileMediaIO:
"""Direct tests for _AudioFileMediaIO methods."""
def test_load_bytes_writes_temp_file(self):
audio_bytes = b"\xff\xfb\x90\x00" * 50
io = _AudioFileMediaIO()
path = io.load_bytes(audio_bytes)
try:
assert os.path.isfile(path)
assert path.endswith(".wav")
with open(path, "rb") as f:
assert f.read() == audio_bytes
finally:
_cleanup(path)
def test_load_bytes_empty_raises(self):
io = _AudioFileMediaIO()
with pytest.raises(InvalidInputReferenceError, match="audio data is empty"):
io.load_bytes(b"")
def test_load_base64_mp3_suffix(self):
audio_bytes = b"\xff\xfb\x90\x00" * 50
b64 = base64.b64encode(audio_bytes).decode()
io = _AudioFileMediaIO()
path = io.load_base64("audio/mp3", b64)
try:
assert path.endswith(".mp3")
with open(path, "rb") as f:
assert f.read() == audio_bytes
finally:
_cleanup(path)
def test_load_base64_mpeg_uses_mp3(self):
io = _AudioFileMediaIO()
path = io.load_base64("audio/mpeg", base64.b64encode(b"\x00" * 10).decode())
try:
assert path.endswith(".mp3")
finally:
_cleanup(path)
def test_load_base64_wav_suffix(self):
io = _AudioFileMediaIO()
path = io.load_base64("audio/wav", base64.b64encode(b"\x00" * 10).decode())
try:
assert path.endswith(".wav")
finally:
_cleanup(path)
def test_load_base64_flac_suffix(self):
io = _AudioFileMediaIO()
path = io.load_base64("audio/flac", base64.b64encode(b"\x00" * 10).decode())
try:
assert path.endswith(".flac")
finally:
_cleanup(path)
def test_load_base64_empty_media_type_uses_wav(self):
io = _AudioFileMediaIO()
path = io.load_base64("", base64.b64encode(b"\x00" * 10).decode())
try:
assert path.endswith(".wav")
finally:
_cleanup(path)
def test_load_base64_non_alnum_falls_back_to_wav(self):
io = _AudioFileMediaIO()
path = io.load_base64("audio/foo-bar", base64.b64encode(b"\x00" * 10).decode())
try:
assert path.endswith(".wav")
finally:
_cleanup(path)
def test_load_base64_too_long_extension_falls_back_to_wav(self):
io = _AudioFileMediaIO()
path = io.load_base64("audio/verylongextension", base64.b64encode(b"\x00" * 10).decode())
try:
assert path.endswith(".wav")
finally:
_cleanup(path)
def test_load_file_returns_path_string(self, tmp_path):
audio_file = tmp_path / "test.wav"
audio_file.write_bytes(b"\x00" * 100)
io = _AudioFileMediaIO()
result = io.load_file(audio_file)
assert result == str(audio_file)
# ---------------------------------------------------------------------------
# decode_audio_url with data URLs (exercises real MediaConnector + MediaIO)
# ---------------------------------------------------------------------------
class TestDecodeAudioUrlDataUrl:
"""Tests for base64 data URL decoding — no mocks, real code path."""
def test_valid_mp3_data_url(self):
audio_bytes = b"\xff\xfb\x90\x00" * 100
b64 = base64.b64encode(audio_bytes).decode()
url = f"data:audio/mp3;base64,{b64}"
path = _run(decode_audio_url(url, _permissive_connector()))
try:
assert os.path.isfile(path)
assert path.endswith(".mp3")
with open(path, "rb") as f:
assert f.read() == audio_bytes
finally:
_cleanup(path)
def test_valid_wav_data_url(self):
audio_bytes = b"RIFF" + b"\x00" * 100
b64 = base64.b64encode(audio_bytes).decode()
url = f"data:audio/wav;base64,{b64}"
path = _run(decode_audio_url(url, _permissive_connector()))
try:
assert path.endswith(".wav")
with open(path, "rb") as f:
assert f.read() == audio_bytes
finally:
_cleanup(path)
def test_invalid_base64_raises(self):
url = "data:audio/mp3;base64,!!!not-valid-base64!!!"
with pytest.raises(InvalidInputReferenceError, match="Incorrect padding"):
_run(decode_audio_url(url, _permissive_connector()))
def test_empty_audio_data_raises(self):
url = "data:audio/mp3;base64,"
with pytest.raises(InvalidInputReferenceError, match="audio data is empty"):
_run(decode_audio_url(url, _permissive_connector()))
# ---------------------------------------------------------------------------
# decode_audio_url with file:// URIs (exercises load_file through connector)
# ---------------------------------------------------------------------------
class TestDecodeAudioUrlFileUri:
"""Tests using file:// URIs — exercises MediaConnector + load_file."""
def test_file_uri_with_allowed_path(self, tmp_path):
audio_file = tmp_path / "test.wav"
audio_file.write_bytes(b"\xff\xfb\x90\x00" * 50)
file_uri = audio_file.as_uri()
connector = MediaConnector(
allowed_local_media_path=str(tmp_path),
allowed_media_domains=None,
)
path = _run(decode_audio_url(file_uri, connector))
assert path == str(audio_file)
def test_file_uri_without_allowed_path_raises(self, tmp_path):
audio_file = tmp_path / "test.wav"
audio_file.write_bytes(b"\x00" * 50)
file_uri = audio_file.as_uri()
connector = MediaConnector(
allowed_local_media_path="",
allowed_media_domains=None,
)
with pytest.raises(InvalidInputReferenceError):
_run(decode_audio_url(file_uri, connector))
def test_file_uri_outside_allowed_path_raises(self, tmp_path):
audio_file = tmp_path / "test.wav"
audio_file.write_bytes(b"\x00" * 50)
file_uri = audio_file.as_uri()
other_dir = tmp_path / "other"
other_dir.mkdir()
connector = MediaConnector(
allowed_local_media_path=str(other_dir),
allowed_media_domains=None,
)
with pytest.raises(InvalidInputReferenceError):
_run(decode_audio_url(file_uri, connector))
# ---------------------------------------------------------------------------
# decode_audio_url — invalid schemes and SSRF
# ---------------------------------------------------------------------------
class TestDecodeAudioUrlInvalid:
"""Tests for invalid URL schemes."""
def test_local_file_path_raises(self):
with pytest.raises(InvalidInputReferenceError, match="must be either a HTTP, data or file URL"):
_run(decode_audio_url("/path/to/local/file.mp3", _permissive_connector()))
def test_ftp_url_raises(self):
with pytest.raises(InvalidInputReferenceError, match="must be either a HTTP, data or file URL"):
_run(decode_audio_url("ftp://example.com/file.mp3", _permissive_connector()))
def test_empty_string_raises(self):
with pytest.raises(InvalidInputReferenceError, match="must be either a HTTP, data or file URL"):
_run(decode_audio_url("", _permissive_connector()))
class TestDecodeAudioUrlSSRF:
"""Tests for SSRF protection via MediaConnector domain restrictions."""
def test_restricted_domain_rejects_disallowed_url(self):
connector = _restricted_connector(allowed_domains=["trusted.example.com"])
with pytest.raises(InvalidInputReferenceError, match="allowed domains"):
_run(
decode_audio_url(
"https://evil.internal/admin/audio.mp3",
connector,
)
)
def test_data_url_bypasses_domain_restriction(self):
audio_bytes = b"\xff\xfb\x90\x00" * 50
b64 = base64.b64encode(audio_bytes).decode()
url = f"data:audio/wav;base64,{b64}"
connector = _restricted_connector(allowed_domains=["trusted.example.com"])
path = _run(decode_audio_url(url, connector))
try:
assert os.path.isfile(path)
finally:
_cleanup(path)