import base64 import io from unittest.mock import patch from fastapi.testclient import TestClient from PIL import Image def _png_data_url() -> str: buf = io.BytesIO() Image.new("RGB", (8, 8), "white").save(buf, format="PNG") b64 = base64.b64encode(buf.getvalue()).decode("ascii") return f"data:image/png;base64,{b64}" def _client() -> TestClient: from surya.endpoint.app import app return TestClient(app) def _image_message(): return [ { "role": "user", "content": [ {"type": "image_url", "image_url": {"url": _png_data_url()}}, {"type": "text", "text": "extract the text"}, ], } ] def test_returns_openai_chat_completion_shape(): body = {"model": "surya-ocr", "messages": _image_message()} fake = {"text_lines": "hello\nworld", "ocr_text_json": {"blocks": []}, "elapsed_seconds": 1.23} with patch("surya.endpoint.service.ocr_via_batcher", return_value=fake): r = _client().post("/v1/chat/completions", json=body) assert r.status_code == 200 d = r.json() assert d["object"] == "chat.completion" assert d["model"] == "surya-ocr" assert d["choices"][0]["message"]["content"] == "hello\nworld" assert d["choices"][0]["finish_reason"] == "stop" assert d["surya"]["ocr_text_json"] == {"blocks": []} assert d["surya"]["elapsed_seconds"] == 1.23 assert "usage" in d def test_mode_full_page_skips_text_detection(): body = {"model": "m", "mode": "full_page", "messages": _image_message()} fake = {"text_lines": "x", "ocr_text_json": {}, "elapsed_seconds": None} with patch("surya.endpoint.service.ocr_via_batcher", return_value=fake) as m: r = _client().post("/v1/chat/completions", json=body) assert r.status_code == 200 assert m.call_args.kwargs["skip_text_detection"] is True def test_mode_table_routes_to_table_image(): body = {"model": "m", "mode": "table", "messages": _image_message()} fake = {"text_lines": "tbl", "ocr_text_json": {"x": 1}, "elapsed_seconds": None} with patch("surya.endpoint.service.table_image", return_value=fake) as m: r = _client().post("/v1/chat/completions", json=body) assert r.status_code == 200 assert m.called assert r.json()["choices"][0]["message"]["content"] == "tbl" def test_ocr_with_boxes_false_omits_structured_json(): body = {"model": "m", "ocr_with_boxes": False, "messages": _image_message()} fake = {"text_lines": "x", "ocr_text_json": {"a": 1}, "elapsed_seconds": None} with patch("surya.endpoint.service.ocr_via_batcher", return_value=fake): r = _client().post("/v1/chat/completions", json=body) assert r.json()["surya"]["ocr_text_json"] is None def test_stream_true_returns_400(): body = {"model": "m", "stream": True, "messages": _image_message()} r = _client().post("/v1/chat/completions", json=body) assert r.status_code == 400 assert r.json()["error"]["type"] == "invalid_request_error" def test_missing_image_returns_400(): body = {"model": "m", "messages": [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]} r = _client().post("/v1/chat/completions", json=body) assert r.status_code == 400 assert r.json()["error"]["type"] == "invalid_request_error" def test_pipeline_error_returns_500(): body = {"model": "m", "messages": _image_message()} with patch("surya.endpoint.service.ocr_via_batcher", side_effect=RuntimeError("boom")): r = _client().post("/v1/chat/completions", json=body) assert r.status_code == 500 assert r.json()["error"]["type"] == "internal_error"