Skip to content

Commit 45fc760

Browse files
Add tests for PDF redaction features
- Introduced live tests for `preview_redactions` and `apply_redactions` to validate redaction workflows. - Added unit tests for `PdfRedactionPreviewPayload` and `PdfRedactionApplyPayload` models, ensuring accurate payload validation. - Verified handling of invalid inputs, including incorrect redaction instructions and color values. - Updated test fixtures and utilities for live and isolated testing scenarios. Assisted-by: Codex
1 parent 742db2b commit 45fc760

4 files changed

Lines changed: 383 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from __future__ import annotations
2+
3+
from typing import get_args
4+
5+
import pytest
6+
7+
from pdfrest import PdfRestApiError, PdfRestClient
8+
from pdfrest.models import PdfRestFile
9+
from pdfrest.types import PdfRedactionInstruction, PdfRedactionPreset
10+
11+
from ..resources import get_test_resource_path
12+
13+
14+
@pytest.fixture(scope="module")
15+
def uploaded_pdf_for_redaction(
16+
pdfrest_api_key: str,
17+
pdfrest_live_base_url: str,
18+
) -> PdfRestFile:
19+
resource = get_test_resource_path("redactable-text.pdf")
20+
with PdfRestClient(
21+
api_key=pdfrest_api_key,
22+
base_url=pdfrest_live_base_url,
23+
) as client:
24+
return client.files.create_from_paths([resource])[0]
25+
26+
27+
@pytest.mark.parametrize(
28+
"instruction",
29+
[
30+
pytest.param(
31+
{
32+
"type": "literal",
33+
"value": "The quick brown fox jumped over the lazy dog.",
34+
},
35+
id="literal",
36+
),
37+
pytest.param({"type": "regex", "value": r"\b\d{3}-\d{2}-\d{4}\b"}, id="regex"),
38+
*[
39+
pytest.param({"type": "preset", "value": preset}, id=f"preset-{preset}")
40+
for preset in get_args(PdfRedactionPreset)
41+
],
42+
],
43+
)
44+
def test_live_redaction_preview_and_apply_single(
45+
pdfrest_api_key: str,
46+
pdfrest_live_base_url: str,
47+
uploaded_pdf_for_redaction: PdfRestFile,
48+
instruction: PdfRedactionInstruction,
49+
) -> None:
50+
with PdfRestClient(
51+
api_key=pdfrest_api_key,
52+
base_url=pdfrest_live_base_url,
53+
) as client:
54+
preview = client.preview_redactions(
55+
uploaded_pdf_for_redaction,
56+
redactions=[instruction],
57+
output="redaction-preview",
58+
)
59+
60+
assert preview.output_files
61+
preview_file = preview.output_files[0]
62+
assert preview_file.name.endswith("redaction-preview.pdf")
63+
assert preview_file.type == "application/pdf"
64+
65+
applied = client.apply_redactions(
66+
preview_file,
67+
output="redaction-final",
68+
)
69+
70+
assert applied.output_files
71+
final_file = applied.output_files[0]
72+
assert final_file.name.endswith("redaction-final.pdf")
73+
assert final_file.type == "application/pdf"
74+
75+
76+
@pytest.mark.parametrize(
77+
"instructions",
78+
[
79+
pytest.param(
80+
[
81+
{
82+
"type": "literal",
83+
"value": "The quick brown fox jumped over the lazy dog.",
84+
},
85+
{"type": "regex", "value": r"\b\d{3}-\d{2}-\d{4}\b"},
86+
],
87+
id="literal-and-regex",
88+
),
89+
pytest.param(
90+
[
91+
{"type": "preset", "value": "email"},
92+
{"type": "preset", "value": "phone_number"},
93+
],
94+
id="preset-email-and-phone",
95+
),
96+
pytest.param(
97+
[
98+
{"type": "preset", "value": "credit_card"},
99+
{"type": "preset", "value": "bank_routing_number"},
100+
{"type": "preset", "value": "swift_bic_number"},
101+
],
102+
id="multiple-presets",
103+
),
104+
],
105+
)
106+
def test_live_redaction_preview_and_apply_multiple(
107+
pdfrest_api_key: str,
108+
pdfrest_live_base_url: str,
109+
uploaded_pdf_for_redaction: PdfRestFile,
110+
instructions: list[PdfRedactionInstruction],
111+
) -> None:
112+
with PdfRestClient(
113+
api_key=pdfrest_api_key,
114+
base_url=pdfrest_live_base_url,
115+
) as client:
116+
preview = client.preview_redactions(
117+
uploaded_pdf_for_redaction,
118+
redactions=instructions,
119+
output="redaction-preview-multi",
120+
)
121+
122+
assert preview.output_files
123+
preview_file = preview.output_files[0]
124+
assert preview_file.name.endswith("redaction-preview-multi.pdf")
125+
assert preview_file.type == "application/pdf"
126+
127+
applied = client.apply_redactions(
128+
preview_file,
129+
output="redaction-final-multi",
130+
)
131+
132+
assert applied.output_files
133+
final_file = applied.output_files[0]
134+
assert final_file.name.endswith("redaction-final-multi.pdf")
135+
assert final_file.type == "application/pdf"
136+
137+
138+
@pytest.mark.parametrize(
139+
"extra_body",
140+
[
141+
pytest.param({"redactions": "invalid"}, id="invalid-redactions"),
142+
pytest.param({"rgb_color": "-1,-1,-1"}, id="invalid-rgb"),
143+
],
144+
)
145+
def test_live_redactions_invalid_payloads(
146+
pdfrest_api_key: str,
147+
pdfrest_live_base_url: str,
148+
uploaded_pdf_for_redaction: PdfRestFile,
149+
extra_body: dict[str, object],
150+
) -> None:
151+
with PdfRestClient(
152+
api_key=pdfrest_api_key,
153+
base_url=pdfrest_live_base_url,
154+
) as client:
155+
if "redactions" in extra_body:
156+
with pytest.raises(PdfRestApiError):
157+
client.preview_redactions(
158+
uploaded_pdf_for_redaction,
159+
redactions=[{"type": "literal", "value": "placeholder"}],
160+
extra_body=extra_body,
161+
)
162+
else:
163+
preview = client.preview_redactions(
164+
uploaded_pdf_for_redaction,
165+
redactions=[{"type": "literal", "value": "placeholder"}],
166+
)
167+
preview_file = preview.output_files[0]
168+
with pytest.raises(PdfRestApiError):
169+
client.apply_redactions(preview_file, extra_body=extra_body)
1.03 MB
Binary file not shown.

tests/test_pdf_redaction_apply.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
import json
4+
5+
import httpx
6+
import pytest
7+
from pydantic import ValidationError
8+
9+
from pdfrest import PdfRestClient
10+
from pdfrest.models import PdfRestFileBasedResponse, PdfRestFileID
11+
from pdfrest.models._internal import PdfRedactionApplyPayload
12+
from pdfrest.types import PdfRGBColor
13+
14+
from .graphics_test_helpers import VALID_API_KEY, build_file_info_payload, make_pdf_file
15+
16+
17+
@pytest.mark.parametrize(
18+
"rgb_color",
19+
[
20+
pytest.param((255, 255, 255), id="tuple"),
21+
pytest.param([10, 20, 30], id="list"),
22+
pytest.param(None, id="none"),
23+
],
24+
)
25+
def test_apply_redactions_success(
26+
monkeypatch: pytest.MonkeyPatch,
27+
rgb_color: PdfRGBColor | list[int] | None,
28+
) -> None:
29+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
30+
input_file = make_pdf_file(PdfRestFileID.generate(1))
31+
output_id = str(PdfRestFileID.generate())
32+
33+
payload_data: dict[str, object] = {"files": [input_file]}
34+
if rgb_color is not None:
35+
payload_data["rgb_color"] = rgb_color
36+
payload_data["output"] = "final-output"
37+
38+
payload_model_dump = PdfRedactionApplyPayload.model_validate(
39+
payload_data
40+
).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
41+
42+
def handler(request: httpx.Request) -> httpx.Response:
43+
if (
44+
request.method == "POST"
45+
and request.url.path == "/pdf-with-redacted-text-applied"
46+
):
47+
body = json.loads(request.content.decode("utf-8"))
48+
assert body == payload_model_dump
49+
return httpx.Response(
50+
200,
51+
json={
52+
"inputId": [input_file.id],
53+
"outputId": [output_id],
54+
},
55+
)
56+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
57+
return httpx.Response(
58+
200,
59+
json=build_file_info_payload(
60+
output_id, "final-output.pdf", "application/pdf"
61+
),
62+
)
63+
msg = f"Unexpected request {request.method} {request.url}"
64+
raise AssertionError(msg)
65+
66+
transport = httpx.MockTransport(handler)
67+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
68+
response = client.apply_redactions(
69+
input_file,
70+
rgb_color=rgb_color,
71+
output="final-output",
72+
)
73+
74+
assert isinstance(response, PdfRestFileBasedResponse)
75+
assert response.output_files[0].name == "final-output.pdf"
76+
assert response.output_files[0].type == "application/pdf"
77+
78+
79+
def test_apply_redactions_invalid_color(monkeypatch: pytest.MonkeyPatch) -> None:
80+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
81+
input_file = make_pdf_file(PdfRestFileID.generate(1))
82+
transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError))
83+
84+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
85+
with pytest.raises(ValidationError, match="Field required"):
86+
client.apply_redactions(input_file, rgb_color=[255, 255])
87+
88+
with pytest.raises(ValidationError, match="greater than or equal to 0"):
89+
client.apply_redactions(input_file, rgb_color=[-1, 0, 0])
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from __future__ import annotations
2+
3+
import json
4+
5+
import httpx
6+
import pytest
7+
from pydantic import ValidationError
8+
9+
from pdfrest import PdfRestClient
10+
from pdfrest.models import PdfRestFileBasedResponse, PdfRestFileID
11+
from pdfrest.models._internal import PdfRedactionPreviewPayload
12+
from pdfrest.types import PdfRedactionInstruction
13+
14+
from .graphics_test_helpers import VALID_API_KEY, build_file_info_payload, make_pdf_file
15+
16+
17+
@pytest.mark.parametrize(
18+
"redactions",
19+
[
20+
pytest.param(
21+
[
22+
{"type": "literal", "value": "Sensitive"},
23+
{"type": "preset", "value": "email"},
24+
],
25+
id="list",
26+
),
27+
pytest.param({"type": "regex", "value": "\\d{3}-\\d{2}-\\d{4}"}, id="single"),
28+
],
29+
)
30+
def test_preview_redactions_success(
31+
monkeypatch: pytest.MonkeyPatch,
32+
redactions: PdfRedactionInstruction | list[PdfRedactionInstruction],
33+
) -> None:
34+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
35+
input_file = make_pdf_file(PdfRestFileID.generate(1))
36+
output_id = str(PdfRestFileID.generate())
37+
payload_model_dump = PdfRedactionPreviewPayload.model_validate(
38+
{
39+
"files": [input_file],
40+
"redactions": redactions,
41+
"output": "preview-output",
42+
}
43+
).model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True)
44+
45+
def handler(request: httpx.Request) -> httpx.Response:
46+
if (
47+
request.method == "POST"
48+
and request.url.path == "/pdf-with-redacted-text-preview"
49+
):
50+
body = json.loads(request.content.decode("utf-8"))
51+
assert body == payload_model_dump
52+
return httpx.Response(
53+
200,
54+
json={
55+
"inputId": [input_file.id],
56+
"outputId": [output_id],
57+
},
58+
)
59+
if request.method == "GET" and request.url.path == f"/resource/{output_id}":
60+
return httpx.Response(
61+
200,
62+
json=build_file_info_payload(
63+
output_id, "preview-output.pdf", "application/pdf"
64+
),
65+
)
66+
msg = f"Unexpected request {request.method} {request.url}"
67+
raise AssertionError(msg)
68+
69+
transport = httpx.MockTransport(handler)
70+
with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client:
71+
response = client.preview_redactions(
72+
input_file,
73+
redactions=redactions,
74+
output="preview-output",
75+
)
76+
77+
assert isinstance(response, PdfRestFileBasedResponse)
78+
assert str(response.input_id) == str(input_file.id)
79+
assert response.output_files[0].name == "preview-output.pdf"
80+
assert response.output_files[0].type == "application/pdf"
81+
assert response.warning is None
82+
83+
84+
def test_preview_redactions_invalid_preset(monkeypatch: pytest.MonkeyPatch) -> None:
85+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
86+
input_file = make_pdf_file(PdfRestFileID.generate(1))
87+
transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError))
88+
89+
with (
90+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
91+
pytest.raises(ValidationError, match="Input should be 'email'"),
92+
):
93+
client.preview_redactions(
94+
input_file,
95+
redactions=[{"type": "preset", "value": "unknown"}],
96+
)
97+
98+
99+
def test_preview_redactions_reject_json_string(monkeypatch: pytest.MonkeyPatch) -> None:
100+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
101+
input_file = make_pdf_file(PdfRestFileID.generate(1))
102+
transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError))
103+
104+
with (
105+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
106+
pytest.raises(ValidationError, match="valid dictionary"),
107+
):
108+
client.preview_redactions(
109+
input_file,
110+
redactions=json.dumps([{"type": "literal", "value": "secret"}]), # type: ignore[arg-type]
111+
)
112+
113+
114+
def test_preview_redactions_requires_instruction(
115+
monkeypatch: pytest.MonkeyPatch,
116+
) -> None:
117+
monkeypatch.delenv("PDFREST_API_KEY", raising=False)
118+
input_file = make_pdf_file(PdfRestFileID.generate(1))
119+
transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError))
120+
121+
with (
122+
PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client,
123+
pytest.raises(ValidationError, match="at least 1 item"),
124+
):
125+
client.preview_redactions(input_file, redactions=[])

0 commit comments

Comments
 (0)