From 300e479eecba0efe11366664355c55aa15d76175 Mon Sep 17 00:00:00 2001 From: "Kevin A. Mitchell" Date: Thu, 18 Dec 2025 10:06:35 -0600 Subject: [PATCH 1/4] tests: Add live tests for PDF/X, Word, and form flattening - Added `test_live_convert_to_pdfx.py` to test PDF to PDF/X conversion with valid and invalid output types. - Added `test_live_convert_to_word.py` to test PDF to Word conversion with support for custom and default output names. - Added `test_live_flatten_pdf_forms.py` to test flattening of PDF forms with validation for custom and default output names. - Introduced new test resource file: `form_with_data.pdf`. Assisted-by: Codex --- tests/live/test_live_convert_to_pdfx.py | 78 ++++++++++++++++++++++ tests/live/test_live_convert_to_word.py | 75 +++++++++++++++++++++ tests/live/test_live_flatten_pdf_forms.py | 72 ++++++++++++++++++++ tests/resources/form_with_data.pdf | Bin 0 -> 23027 bytes 4 files changed, 225 insertions(+) create mode 100644 tests/live/test_live_convert_to_pdfx.py create mode 100644 tests/live/test_live_convert_to_word.py create mode 100644 tests/live/test_live_flatten_pdf_forms.py create mode 100644 tests/resources/form_with_data.pdf diff --git a/tests/live/test_live_convert_to_pdfx.py b/tests/live/test_live_convert_to_pdfx.py new file mode 100644 index 00000000..a08088b0 --- /dev/null +++ b/tests/live/test_live_convert_to_pdfx.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import cast, get_args + +import pytest + +from pdfrest import PdfRestApiError, PdfRestClient +from pdfrest.models import PdfRestFile +from pdfrest.types import PdfXType + +from ..resources import get_test_resource_path + +PDFX_TYPES: tuple[PdfXType, ...] = cast(tuple[PdfXType, ...], get_args(PdfXType)) + + +@pytest.fixture(scope="module") +def uploaded_pdf_for_pdfx( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> PdfRestFile: + resource = get_test_resource_path("report.pdf") + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + return client.files.create_from_paths([resource])[0] + + +@pytest.mark.parametrize("output_type", PDFX_TYPES, ids=list(PDFX_TYPES)) +def test_live_convert_to_pdfx_success( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_pdfx: PdfRestFile, + output_type: PdfXType, +) -> None: + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.convert_to_pdfx( + uploaded_pdf_for_pdfx, + output_type=output_type, + output="pdfx-live", + ) + + assert response.output_files + output_file = response.output_file + assert output_file.type == "application/pdf" + assert str(response.input_id) == str(uploaded_pdf_for_pdfx.id) + assert output_file.name.startswith("pdfx-live") + + +@pytest.mark.parametrize( + "invalid_output_type", + [ + pytest.param("PDF/X-0", id="pdfx-0"), + pytest.param("PDF/X-99", id="pdfx-99"), + pytest.param("pdf/x-4", id="lowercase"), + ], +) +def test_live_convert_to_pdfx_invalid_output_type( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_pdfx: PdfRestFile, + invalid_output_type: str, +) -> None: + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError), + ): + client.convert_to_pdfx( + uploaded_pdf_for_pdfx, + output_type="PDF/X-1a", + extra_body={"output_type": invalid_output_type}, + ) diff --git a/tests/live/test_live_convert_to_word.py b/tests/live/test_live_convert_to_word.py new file mode 100644 index 00000000..c3c5822e --- /dev/null +++ b/tests/live/test_live_convert_to_word.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import pytest + +from pdfrest import PdfRestApiError, PdfRestClient +from pdfrest.models import PdfRestFile + +from ..resources import get_test_resource_path + + +@pytest.fixture(scope="module") +def uploaded_pdf_for_word( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> PdfRestFile: + resource = get_test_resource_path("report.pdf") + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + return client.files.create_from_paths([resource])[0] + + +@pytest.mark.parametrize( + "output_name", + [ + pytest.param(None, id="default-output"), + pytest.param("live-word", id="custom-output"), + ], +) +def test_live_convert_to_word_success( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_word: PdfRestFile, + output_name: str | None, +) -> None: + kwargs: dict[str, str] = {} + if output_name is not None: + kwargs["output"] = output_name + + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.convert_to_word(uploaded_pdf_for_word, **kwargs) + + assert response.output_files + output_file = response.output_file + assert ( + output_file.type + == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + assert str(response.input_id) == str(uploaded_pdf_for_word.id) + if output_name is not None: + assert output_file.name.startswith(output_name) + else: + assert output_file.name.endswith(".docx") + + +def test_live_convert_to_word_invalid_file_id( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_word: PdfRestFile, +) -> None: + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError), + ): + client.convert_to_word( + uploaded_pdf_for_word, + extra_body={"id": "00000000-0000-0000-0000-000000000000"}, + ) diff --git a/tests/live/test_live_flatten_pdf_forms.py b/tests/live/test_live_flatten_pdf_forms.py new file mode 100644 index 00000000..c6ad7fdb --- /dev/null +++ b/tests/live/test_live_flatten_pdf_forms.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import pytest + +from pdfrest import PdfRestApiError, PdfRestClient +from pdfrest.models import PdfRestFile + +from ..resources import get_test_resource_path + + +@pytest.fixture(scope="module") +def uploaded_pdf_with_forms( + pdfrest_api_key: str, + pdfrest_live_base_url: str, +) -> PdfRestFile: + resource = get_test_resource_path("form_with_data.pdf") + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + return client.files.create_from_paths([resource])[0] + + +@pytest.mark.parametrize( + "output_name", + [ + pytest.param(None, id="default-output"), + pytest.param("flattened-live", id="custom-output"), + ], +) +def test_live_flatten_pdf_forms( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_with_forms: PdfRestFile, + output_name: str | None, +) -> None: + kwargs: dict[str, str] = {} + if output_name is not None: + kwargs["output"] = output_name + + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.flatten_pdf_forms(uploaded_pdf_with_forms, **kwargs) + + assert response.output_files + output_file = response.output_file + assert output_file.type == "application/pdf" + assert str(response.input_id) == str(uploaded_pdf_with_forms.id) + if output_name is not None: + assert output_file.name.startswith(output_name) + else: + assert output_file.name.endswith(".pdf") + + +def test_live_flatten_pdf_forms_invalid_file_id( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_with_forms: PdfRestFile, +) -> None: + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError), + ): + client.flatten_pdf_forms( + uploaded_pdf_with_forms, + extra_body={"id": "ffffffff-ffff-ffff-ffff-ffffffffffff"}, + ) diff --git a/tests/resources/form_with_data.pdf b/tests/resources/form_with_data.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3ec8152d397c8178bc3e01df7ba94eac08117fbf GIT binary patch literal 23027 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 literal 0 HcmV?d00001 From 014162ea4d5d20076a54954ef6716973f598e554 Mon Sep 17 00:00:00 2001 From: "Kevin A. Mitchell" Date: Thu, 18 Dec 2025 10:18:33 -0600 Subject: [PATCH 2/4] tests: Add request customization tests for PDF/X, flatten, Word - Added parameterized tests for `test_convert_to_pdfx_success` and `test_async_convert_to_pdfx_success` to validate multiple output types. - Introduced sync and async tests to verify request customization for PDF to PDF/X, Word, and flattening PDF forms. - Ensured validation logic for accepted input/output types and custom metadata integrations in payload and headers. Assisted-by: Codex --- tests/test_convert_to_pdfx.py | 158 ++++++++++++++++++++++++++++++-- tests/test_convert_to_word.py | 123 +++++++++++++++++++++++++ tests/test_flatten_pdf_forms.py | 123 +++++++++++++++++++++++++ 3 files changed, 398 insertions(+), 6 deletions(-) diff --git a/tests/test_convert_to_pdfx.py b/tests/test_convert_to_pdfx.py index fd2918f0..86f10129 100644 --- a/tests/test_convert_to_pdfx.py +++ b/tests/test_convert_to_pdfx.py @@ -19,12 +19,21 @@ ) -def test_convert_to_pdfx_success(monkeypatch: pytest.MonkeyPatch) -> None: +@pytest.mark.parametrize( + "output_type", + [ + pytest.param("PDF/X-1a", id="pdfx-1a"), + pytest.param("PDF/X-3", id="pdfx-3"), + pytest.param("PDF/X-4", id="pdfx-4"), + pytest.param("PDF/X-6", id="pdfx-6"), + ], +) +def test_convert_to_pdfx_success( + monkeypatch: pytest.MonkeyPatch, output_type: PdfXType +) -> None: monkeypatch.delenv("PDFREST_API_KEY", raising=False) input_file = make_pdf_file(PdfRestFileID.generate(1)) output_id = str(PdfRestFileID.generate()) - output_type: PdfXType = "PDF/X-4" - payload_dump = PdfToPdfxPayload.model_validate( {"files": [input_file], "output_type": output_type, "output": "print-ready"} ).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) @@ -72,12 +81,21 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio -async def test_async_convert_to_pdfx_success(monkeypatch: pytest.MonkeyPatch) -> None: +@pytest.mark.parametrize( + "output_type", + [ + pytest.param("PDF/X-1a", id="async-pdfx-1a"), + pytest.param("PDF/X-3", id="async-pdfx-3"), + pytest.param("PDF/X-4", id="async-pdfx-4"), + pytest.param("PDF/X-6", id="async-pdfx-6"), + ], +) +async def test_async_convert_to_pdfx_success( + monkeypatch: pytest.MonkeyPatch, output_type: PdfXType +) -> None: monkeypatch.delenv("PDFREST_API_KEY", raising=False) input_file = make_pdf_file(PdfRestFileID.generate(2)) output_id = str(PdfRestFileID.generate()) - output_type: PdfXType = "PDF/X-1a" - payload_dump = PdfToPdfxPayload.model_validate( {"files": [input_file], "output_type": output_type} ).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) @@ -120,6 +138,123 @@ def handler(request: httpx.Request) -> httpx.Response: assert str(response.input_id) == str(input_file.id) +def test_convert_to_pdfx_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/pdfx": + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["output_type"] == "PDF/X-3" + assert payload["debug"] == "yes" + assert payload["id"] == str(input_file.id) + assert payload["output"] == "custom" + return httpx.Response( + 200, + json={"inputId": [input_file.id], "outputId": [output_id]}, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, "custom.pdf", "application/pdf" + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.convert_to_pdfx( + input_file, + output_type="PDF/X-3", + output="custom", + extra_query={"trace": "true"}, + extra_headers={"X-Debug": "sync"}, + extra_body={"debug": "yes"}, + timeout=0.33, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "custom.pdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.33) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.33) + + +@pytest.mark.asyncio +async def test_async_convert_to_pdfx_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/pdfx": + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["output_type"] == "PDF/X-6" + assert payload["id"] == str(input_file.id) + assert payload["extra"] == {"note": "async"} + return httpx.Response( + 200, + json={"inputId": [input_file.id], "outputId": [output_id]}, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, "async-custom.pdf", "application/pdf" + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.convert_to_pdfx( + input_file, + output_type="PDF/X-6", + extra_query={"trace": "async"}, + extra_headers={"X-Debug": "async"}, + extra_body={"extra": {"note": "async"}}, + timeout=0.72, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "async-custom.pdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.72) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.72) + + def test_convert_to_pdfx_validation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("PDFREST_API_KEY", raising=False) pdf_file = make_pdf_file(PdfRestFileID.generate(1)) @@ -152,3 +287,14 @@ def test_convert_to_pdfx_validation(monkeypatch: pytest.MonkeyPatch) -> None: pytest.raises(ValidationError, match="PDF/X-1a"), ): client.convert_to_pdfx(pdf_file, output_type="PDF/X-5") # type: ignore[arg-type] + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises( + ValidationError, match="List should have at most 1 item after validation" + ), + ): + client.convert_to_pdfx( + [pdf_file, make_pdf_file(PdfRestFileID.generate())], + output_type="PDF/X-3", + ) diff --git a/tests/test_convert_to_word.py b/tests/test_convert_to_word.py index a9e60cbd..ba4e0341 100644 --- a/tests/test_convert_to_word.py +++ b/tests/test_convert_to_word.py @@ -71,6 +71,68 @@ def handler(request: httpx.Request) -> httpx.Response: assert str(response.input_id) == str(input_file.id) +def test_convert_to_word_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/word": + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["debug"] is True + assert payload["id"] == str(input_file.id) + assert payload["output"] == "custom" + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "custom.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.convert_to_word( + input_file, + output="custom", + extra_query={"trace": "true"}, + extra_headers={"X-Debug": "sync"}, + extra_body={"debug": True}, + timeout=0.4, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "custom.docx" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.4) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.4) + + @pytest.mark.asyncio async def test_async_convert_to_word_success( monkeypatch: pytest.MonkeyPatch, @@ -125,6 +187,67 @@ def handler(request: httpx.Request) -> httpx.Response: assert str(response.input_id) == str(input_file.id) +@pytest.mark.asyncio +async def test_async_convert_to_word_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/word": + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["debug"] == "yes" + assert payload["id"] == str(input_file.id) + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "async-custom.docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.convert_to_word( + input_file, + extra_query={"trace": "async"}, + extra_headers={"X-Debug": "async"}, + extra_body={"debug": "yes"}, + timeout=0.55, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "async-custom.docx" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.55) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.55) + + def test_convert_to_word_validation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("PDFREST_API_KEY", raising=False) pdf_file = make_pdf_file(PdfRestFileID.generate(1)) diff --git a/tests/test_flatten_pdf_forms.py b/tests/test_flatten_pdf_forms.py index 8b22bd4e..b8f41e6d 100644 --- a/tests/test_flatten_pdf_forms.py +++ b/tests/test_flatten_pdf_forms.py @@ -67,6 +67,68 @@ def handler(request: httpx.Request) -> httpx.Response: assert response.warning is None +def test_flatten_pdf_forms_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/flattened-forms-pdf": + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["debug"] == "yes" + assert payload["id"] == str(input_file.id) + assert payload["output"] == "custom" + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "true" + assert request.headers["X-Debug"] == "sync" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "custom.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.flatten_pdf_forms( + input_file, + output="custom", + extra_query={"trace": "true"}, + extra_headers={"X-Debug": "sync"}, + extra_body={"debug": "yes"}, + timeout=0.29, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "custom.pdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.29) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.29) + + @pytest.mark.asyncio async def test_async_flatten_pdf_forms_success(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("PDFREST_API_KEY", raising=False) @@ -116,6 +178,67 @@ def handler(request: httpx.Request) -> httpx.Response: assert str(response.input_id) == str(input_file.id) +@pytest.mark.asyncio +async def test_async_flatten_pdf_forms_request_customization( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + captured_timeout: dict[str, float | dict[str, float] | None] = {} + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "POST" and request.url.path == "/flattened-forms-pdf": + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + captured_timeout["value"] = request.extensions.get("timeout") + payload = json.loads(request.content.decode("utf-8")) + assert payload["flags"] == ["a", "b"] + assert payload["id"] == str(input_file.id) + return httpx.Response( + 200, + json={ + "inputId": [input_file.id], + "outputId": [output_id], + }, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + assert request.url.params["format"] == "info" + assert request.url.params["trace"] == "async" + assert request.headers["X-Debug"] == "async" + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "async-custom.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.flatten_pdf_forms( + input_file, + extra_query={"trace": "async"}, + extra_headers={"X-Debug": "async"}, + extra_body={"flags": ["a", "b"]}, + timeout=0.58, + ) + + assert isinstance(response, PdfRestFileBasedResponse) + assert response.output_file.name == "async-custom.pdf" + timeout_value = captured_timeout["value"] + assert timeout_value is not None + if isinstance(timeout_value, dict): + assert all( + component == pytest.approx(0.58) for component in timeout_value.values() + ) + else: + assert timeout_value == pytest.approx(0.58) + + def test_flatten_pdf_forms_validation(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("PDFREST_API_KEY", raising=False) pdf_file = make_pdf_file(PdfRestFileID.generate(1)) From 277cfad24be5c7c71f41ba39518a74171f4650d2 Mon Sep 17 00:00:00 2001 From: "Kevin A. Mitchell" Date: Thu, 18 Dec 2025 10:22:06 -0600 Subject: [PATCH 3/4] AGENTS.md: Update testing guidelines with live test requirements - Added requirement for live pytest modules under `tests/live/` for all new endpoints and services. - Emphasized parameterization, coverage of accepted literals, and at least one invalid input test. - Clarified guidelines for reviewers to block changes without live tests or an explicit follow-up plan. - Enhanced recommendations for pytest parameterization to improve test case clarity and reduce duplication. Assisted-by: Codex --- AGENTS.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cd47344b..ae95a0d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,23 +111,40 @@ ## Testing Guidelines +- **Live Test Requirement (Do Not Skip):** Every new endpoint or service must + ship with a matching live pytest module under `tests/live/` before the work is + considered complete. Mirror the naming/structure used by the graphic + conversion suites: one module per endpoint, parameterized success cases that + enumerate all accepted literals, at least one invalid input that hits the + server, and coverage for any request options surfaced on the client. If an + endpoint cannot be exercised live, call that out explicitly in the PR + description with the reason and the follow-up plan; otherwise reviewers should + block the change. Treat this as a release gate on par with unit tests. + - Write pytest tests: files named `test_*.py`, test functions `test_*`, fixtures in `conftest.py` where shared. + - Ensure high-value coverage of public functions and edge cases; document intent in test docstrings when non-obvious. + - Use `uvx nox -s tests` to exercise the full interpreter matrix locally when validating compatibility. + - When writing live tests for URL uploads, first create the remote resources via `create_from_paths`, then reuse the returned URLs in `create_from_urls` to avoid relying on third-party availability. + - For parameterized tests prefer `pytest.param(..., id="short-label")` so test IDs stay readable; make assertions for every relevant response attribute (name prefix, MIME type, size, URLs, warnings). + - Avoid manual loops over test parameters; prefer `@pytest.mark.parametrize` with explicit `id=` values so each combination is visible and reproducible. + - Always couple `pytest.raises` with an explicit `match=` regex that reflects the intended validation error wording—mirror the human-readable text rather than relying on default exception formatting. + - Mirror PNG’s request/response scenarios for each graphic conversion endpoint: maintain per-endpoint test modules (`test_convert_to_png.py`, `test_convert_to_bmp.py`, etc.) covering success, parameter customization, @@ -135,6 +152,7 @@ validation (output prefix and page-range cases) in a dedicated suite (e.g., `tests/test_graphic_payload_validation.py`) that exercises every payload model. + - When introducing additional pdfRest endpoints, follow the same pattern used for graphic conversions: encapsulate shared request validation in a typed payload model, expose fully named client methods, and create a dedicated test @@ -143,15 +161,20 @@ checks (e.g., common field requirements, payload serialization) in shared helper tests so new services inherit consistent coverage with minimal duplication. + - Prefer `pytest.mark.parametrize` (with `pytest.param(..., id="...")`) over - explicit loops inside tests; nest parametrization for multi-dimensional - coverage so each case appears as an individual test item. + explicit loops or copy/paste blocks—if only the input value or expected error + changes, parameterize it so failures point to the exact case and reviewers + don’t have to diff almost-identical code. Nest parametrization for + multi-dimensional coverage so each combination appears as its own test item. + - Live tests should verify that literal enumerations match pdfRest’s accepted values. Exercise format-specific options (e.g., each image format’s `color_model`) individually, and run smoothing enumerations through every enabled endpoint to confirm consistent server behaviour. Include “wildly” invalid values (e.g., bogus literals or mixed lists) alongside boundary failures so the server-side error messaging is exercised. + - Provide live integration tests under `tests/live/` (with an `__init__.py` so pytest discovers the package) that introspect payload models to enumerate valid/invalid literal values and numeric boundaries. These tests should vary a @@ -162,11 +185,13 @@ exception surfaced by the client). When test fixtures produce deterministic results (e.g., `tests/resources/report.pdf`), assert the concrete values returned by pdfRest rather than only checking for presence or type. + - Use `tests/resources/20-pages.pdf` for high-page-count scenarios such as split and merge endpoints so boundary coverage (multi-output splits, staggered page selections) remains reproducible. Parameterize live split/merge tests to cover multiple page-group patterns, and pair each success case with an invalid input that reaches the server by overriding the JSON body via `extra_body`. + - Developers can load a pdfRest API key from `.env` during ad-hoc exploration. The repo includes `python-dotenv`; call `load_dotenv()` (optionally pointing to `.env`) in temporary scripts to drive the in-flight client against live From 80a6eea2120d1e1da18cf1a28fe23a43fa40604c Mon Sep 17 00:00:00 2001 From: "Kevin A. Mitchell" Date: Thu, 18 Dec 2025 11:55:05 -0600 Subject: [PATCH 4/4] AGENTS.md: Clarify validation and file handling conventions - Recommended using `BeforeValidator` over `@field_validator` for declarative validation across schemas. - Documented guidelines for treating `PdfRestClient` and `AsyncPdfRestClient` as context managers for deterministic resource disposal. - Established consistent behavior for handling file uploads, multipart forms, and endpoint parameters, highlighting the use of `PdfRestFile` objects. - Specified rules for serialization and restricted public APIs from exposing internal resource identifiers like `PdfRestFileID`. Assisted-by: Codex --- AGENTS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ae95a0d5..11642296 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,11 +39,25 @@ - When calling pdfRest, supply the API key via the `Api-Key` header (not `Authorization: Bearer`); keep tests and client defaults in sync with this convention. +- Avoid `@field_validator` on payload models. Prefer existing `BeforeValidator` + helpers (e.g., `_allowed_mime_types`) so validation remains declarative and + consistent across schemas. - Treat `PdfRestClient` and `AsyncPdfRestClient` as context managers in both production code and tests so transports are disposed deterministically. - When uploading content, always send the multipart field name `file`; when uploading by URL, send a JSON payload using the `url` key with a list of http/https addresses (single values are promoted to lists internally). +- Always upload local assets before invoking an endpoint helper. Public client + APIs must accept `PdfRestFile` objects (or sequences) rather than raw paths or + ids, including optional resources such as compression profiles. Never expose + `PdfRestFileID` in the interface—callers should upload the profile JSON, get + the resulting `PdfRestFile`, then pass that object into helpers like + `compress_pdf`. +- When an endpoint supports both an inline upload parameter and an `*_id` + variant, ignore the upload form and expose only the base parameter (without + `_id`) typed as `PdfRestFile`. Serialize via `_serialize_as_first_file_id` + with `serialization_alias` pointing to the server’s `*_id` field so requests + always reference already-uploaded resources. - `prepare_request` rejects mixed multipart (`files`) and JSON payloads; only URL uploads (`create_from_urls`) should combine JSON bodies with the request. - Replicate server-side safeguards when porting validation logic: the output