|
| 1 | +"""Regression: extensionless image/audio uploads must get a valid MIME subtype. |
| 2 | +
|
| 3 | +The data-URL subtype was derived only from the stored file's extension |
| 4 | +(`image_format = ext[1:]`). A pasted screenshot or any file whose stored id |
| 5 | +carries no extension yields `ext == ""`, so the emitted URL was |
| 6 | +`data:image/;base64,...` — an empty MIME subtype (invalid per RFC 2046) that |
| 7 | +vision/audio endpoints reject, silently dropping the attachment. When the |
| 8 | +extension is missing, fall back to the resolved MIME subtype. Extensions that |
| 9 | +are present are unchanged. |
| 10 | +""" |
| 11 | + |
| 12 | + |
| 13 | +class _Handler: |
| 14 | + def __init__(self, uploads, image=False, audio=False): |
| 15 | + self.uploads = uploads |
| 16 | + self._image = image |
| 17 | + self._audio = audio |
| 18 | + |
| 19 | + def resolve_upload(self, fid, owner=None): |
| 20 | + return self.uploads.get(fid) |
| 21 | + |
| 22 | + def _inside_upload_dir(self, path): |
| 23 | + return True |
| 24 | + |
| 25 | + def is_image_file(self, name, mime): |
| 26 | + return self._image and (mime or "").startswith("image/") |
| 27 | + |
| 28 | + def is_audio_file(self, name, mime): |
| 29 | + return self._audio and (mime or "").startswith("audio/") |
| 30 | + |
| 31 | + def is_document_file(self, name, mime): |
| 32 | + return False |
| 33 | + |
| 34 | + |
| 35 | +def _blocks(content, block_type): |
| 36 | + return [b for b in content if isinstance(b, dict) and b.get("type") == block_type] |
| 37 | + |
| 38 | + |
| 39 | +def test_extensionless_image_uses_mime_subtype(tmp_path): |
| 40 | + import src.document_processor as dp |
| 41 | + |
| 42 | + p = tmp_path / ("a" * 32) # bare id, no extension |
| 43 | + p.write_bytes(b"\x89PNG\r\n\x1a\nfake") |
| 44 | + uploads = {"img": {"path": str(p), "name": "screenshot", "mime": "image/png"}} |
| 45 | + |
| 46 | + content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t") |
| 47 | + imgs = _blocks(content, "image_url") |
| 48 | + assert imgs, content |
| 49 | + assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,") |
| 50 | + |
| 51 | + |
| 52 | +def test_extensionless_audio_uses_mime_subtype(tmp_path): |
| 53 | + import src.document_processor as dp |
| 54 | + |
| 55 | + p = tmp_path / ("b" * 32) |
| 56 | + p.write_bytes(b"fakeaudio") |
| 57 | + uploads = {"aud": {"path": str(p), "name": "recording", "mime": "audio/mpeg"}} |
| 58 | + |
| 59 | + content = dp.build_user_content("listen", ["aud"], str(tmp_path), _Handler(uploads, audio=True), owner="t") |
| 60 | + auds = _blocks(content, "audio") |
| 61 | + assert auds, content |
| 62 | + assert auds[0]["audio"]["url"].startswith("data:audio/mpeg;base64,") |
| 63 | + |
| 64 | + |
| 65 | +def test_extension_present_is_unchanged(tmp_path): |
| 66 | + import src.document_processor as dp |
| 67 | + |
| 68 | + p = tmp_path / "pic.png" |
| 69 | + p.write_bytes(b"\x89PNG\r\n\x1a\n") |
| 70 | + uploads = {"img": {"path": str(p), "name": "pic.png", "mime": "image/png"}} |
| 71 | + |
| 72 | + content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t") |
| 73 | + imgs = _blocks(content, "image_url") |
| 74 | + assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,") |
0 commit comments