|
| 1 | +import json |
| 2 | + |
| 3 | +import pytest |
| 4 | + |
| 5 | +from elevenlabs.music_custom import MusicClient, _find_audio_start |
| 6 | + |
| 7 | +SAMPLE_JSON = {"compositionPlan": {"sections": []}, "songMetadata": {"title": "Test"}} |
| 8 | +SAMPLE_AUDIO = bytes(range(256)) * 4 |
| 9 | + |
| 10 | + |
| 11 | +def _build_multipart(line_ending: bytes) -> bytes: |
| 12 | + """Build a realistic multipart response with the given line ending.""" |
| 13 | + boundary = b"--boundary123" |
| 14 | + nl = line_ending |
| 15 | + |
| 16 | + parts = [] |
| 17 | + parts.append(boundary + nl) |
| 18 | + parts.append(b"Content-Type: application/json" + nl) |
| 19 | + parts.append(nl) |
| 20 | + parts.append(json.dumps(SAMPLE_JSON).encode("utf-8") + nl) |
| 21 | + parts.append(boundary + nl) |
| 22 | + parts.append(b"Content-Type: audio/mpeg" + nl) |
| 23 | + parts.append(b'Content-Disposition: attachment; filename="test_song.mp3"' + nl) |
| 24 | + parts.append(nl) |
| 25 | + parts.append(SAMPLE_AUDIO) |
| 26 | + parts.append(nl + boundary + b"--" + nl) |
| 27 | + |
| 28 | + return b"".join(parts) |
| 29 | + |
| 30 | + |
| 31 | +class TestFindAudioStart: |
| 32 | + def test_crlf(self): |
| 33 | + data = b"Header: value\r\n\r\naudio data here" |
| 34 | + assert _find_audio_start(data, 0) == data.index(b"audio") |
| 35 | + |
| 36 | + def test_lf(self): |
| 37 | + data = b"Header: value\n\naudio data here" |
| 38 | + assert _find_audio_start(data, 0) == data.index(b"audio") |
| 39 | + |
| 40 | + def test_crlf_preferred_over_lf(self): |
| 41 | + data = b"Header: value\r\n\r\naudio data here" |
| 42 | + pos = _find_audio_start(data, 0) |
| 43 | + assert data[pos : pos + 5] == b"audio" |
| 44 | + |
| 45 | + def test_raises_when_no_separator(self): |
| 46 | + with pytest.raises(ValueError, match="header/body separator"): |
| 47 | + _find_audio_start(b"no blank line here", 0) |
| 48 | + |
| 49 | + def test_start_offset_respected(self): |
| 50 | + data = b"skip\n\nHeader: value\r\n\r\naudio" |
| 51 | + pos = _find_audio_start(data, 6) |
| 52 | + assert data[pos : pos + 5] == b"audio" |
| 53 | + |
| 54 | + |
| 55 | +class TestParseMultipart: |
| 56 | + @staticmethod |
| 57 | + def _parse(data: bytes): |
| 58 | + return MusicClient._parse_multipart(None, iter([data])) # type: ignore[arg-type] |
| 59 | + |
| 60 | + def test_crlf_line_endings(self): |
| 61 | + data = _build_multipart(b"\r\n") |
| 62 | + result = self._parse(data) |
| 63 | + assert result.audio.startswith(SAMPLE_AUDIO) |
| 64 | + assert result.json == SAMPLE_JSON |
| 65 | + assert result.filename == "test_song.mp3" |
| 66 | + |
| 67 | + def test_lf_line_endings(self): |
| 68 | + data = _build_multipart(b"\n") |
| 69 | + result = self._parse(data) |
| 70 | + assert result.audio.startswith(SAMPLE_AUDIO) |
| 71 | + assert result.json == SAMPLE_JSON |
| 72 | + assert result.filename == "test_song.mp3" |
| 73 | + |
| 74 | + def test_audio_bytes_not_corrupted(self): |
| 75 | + for nl in [b"\r\n", b"\n"]: |
| 76 | + data = _build_multipart(nl) |
| 77 | + result = self._parse(data) |
| 78 | + assert result.audio[:4] == SAMPLE_AUDIO[:4], ( |
| 79 | + f"First 4 bytes mismatch with {nl!r} line endings — possible byte offset error" |
| 80 | + ) |
| 81 | + assert result.audio[: len(SAMPLE_AUDIO)] == SAMPLE_AUDIO |
0 commit comments