91 lines
5.8 KiB
Python
91 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from govoplan_core.core.templates import TemplateRenderError
|
|
from govoplan_templates.backend import rendering
|
|
|
|
|
|
class TemplateRenderLimitTests(unittest.TestCase):
|
|
def test_many_items_stop_before_rendering_the_entire_oversized_bundle(self):
|
|
with patch.object(rendering, "MAX_OUTPUT_BYTES", 128), patch.object(rendering, "_substitute", wraps=rendering._substitute) as substitute:
|
|
with self.assertRaisesRegex(TemplateRenderError, "bounded-download limit"):
|
|
rendering._render_items("x" * 64, {}, [{}] * 5000, html=False)
|
|
self.assertEqual(3, substitute.call_count)
|
|
|
|
def test_repeated_token_expansion_stops_before_allocating_the_whole_row(self):
|
|
with patch.object(rendering, "MAX_OUTPUT_BYTES", 128), patch.object(rendering, "_display_value", wraps=rendering._display_value) as display:
|
|
with self.assertRaisesRegex(TemplateRenderError, "bounded-download limit"):
|
|
rendering._substitute("{{value}}" * 5000, {"value": "x" * 64}, html=False)
|
|
self.assertEqual(3, display.call_count)
|
|
|
|
def test_utf8_byte_limit_is_not_a_character_limit(self):
|
|
with patch.object(rendering, "MAX_OUTPUT_BYTES", 4):
|
|
self.assertEqual("üü", rendering._substitute("{{value}}", {"value": "üü"}, html=False))
|
|
with self.assertRaises(TemplateRenderError):
|
|
rendering._substitute("{{value}}", {"value": "üüü"}, html=False)
|
|
|
|
def test_html_escaping_counts_expanded_bytes_and_preserves_valid_output(self):
|
|
with patch.object(rendering, "MAX_OUTPUT_BYTES", 9):
|
|
self.assertEqual("<&", rendering._substitute("{{value}}", {"value": "<&"}, html=True))
|
|
with self.assertRaises(TemplateRenderError):
|
|
rendering._substitute("{{value}}!", {"value": "<&"}, html=True)
|
|
|
|
def test_text_separator_is_part_of_the_same_budget(self):
|
|
with patch.object(rendering, "MAX_OUTPUT_BYTES", 5):
|
|
self.assertEqual(["ab", "cd"], rendering._render_items("{{value}}", {}, [{"value": "ab"}, {"value": "cd"}], html=False, separator="\n"))
|
|
with self.assertRaises(TemplateRenderError):
|
|
rendering._render_items("{{value}}", {}, [{"value": "ab"}, {"value": "cd"}], html=False, separator="\n\n")
|
|
|
|
def test_html_wrapper_is_also_subject_to_output_limit(self):
|
|
definition = SimpleNamespace(name="Example")
|
|
revision = SimpleNamespace(content_html="<p>Ada</p>", content_text=None, template_type="list_layout", layout={}, output_profiles=[])
|
|
request = SimpleNamespace(output_format="html", parameters={})
|
|
with patch.object(rendering, "MAX_OUTPUT_BYTES", 32):
|
|
with self.assertRaises(TemplateRenderError):
|
|
rendering._render_payload(definition, revision, request=request, items=[{}])
|
|
|
|
def test_fresh_worker_preserves_exact_text_bytes_and_page_count(self):
|
|
definition = SimpleNamespace(name="Example")
|
|
revision = SimpleNamespace(content_html=None, content_text="Hi {{name}}: {{flag}}", template_type="serial_letter", layout={}, output_profiles=[])
|
|
request = SimpleNamespace(output_format="text", parameters={"flag": True})
|
|
with patch.object(rendering, "_render_payload_content", side_effect=AssertionError("parent renderer ran")):
|
|
result = rendering._render_payload(definition, revision, request=request, items=[{"name": "Äda"}, {"name": "Grace"}])
|
|
self.assertEqual(("Hi Äda: true\n\n---\n\nHi Grace: true".encode(), "text/plain; charset=utf-8", 2), result)
|
|
|
|
def test_real_worker_timeout_fails_without_parent_fallback(self):
|
|
definition = SimpleNamespace(name="Example")
|
|
revision = SimpleNamespace(content_html=None, content_text="Hello", template_type="serial_letter", layout={}, output_profiles=[])
|
|
request = SimpleNamespace(output_format="text", parameters={})
|
|
limits = replace(rendering.RENDER_PROCESS_LIMITS, wall_seconds=0.001)
|
|
with patch.object(rendering, "RENDER_PROCESS_LIMITS", limits), patch.object(
|
|
rendering, "_render_payload_content", side_effect=AssertionError("parent renderer ran")
|
|
):
|
|
with self.assertRaisesRegex(TemplateRenderError, "timeout"):
|
|
rendering._render_payload(definition, revision, request=request, items=[{}])
|
|
|
|
def test_worker_html_bytes_match_existing_composition(self):
|
|
definition = SimpleNamespace(name="Letters <archive>")
|
|
revision = SimpleNamespace(content_html="<p>{{name}}: {{settings}}</p>", content_text=None, template_type="label_sheet", layout={"page_size": "A5", "columns": 2, "rows": 2}, output_profiles=[])
|
|
request = SimpleNamespace(output_format="html", parameters={"settings": {"b": True, "a": "ü"}})
|
|
items = [{"name": "<Ada>"}, {"name": "Grace & Co"}]
|
|
expected = rendering._render_payload_content(definition, revision, request=request, items=items)
|
|
actual = rendering._render_payload(definition, revision, request=request, items=items)
|
|
self.assertEqual(expected, actual)
|
|
|
|
def test_real_worker_output_transport_limit_is_enforced(self):
|
|
definition = SimpleNamespace(name="Example")
|
|
revision = SimpleNamespace(content_html=None, content_text="x" * 4096, template_type="serial_letter", layout={}, output_profiles=[])
|
|
request = SimpleNamespace(output_format="text", parameters={})
|
|
limits = replace(rendering.RENDER_PROCESS_LIMITS, output_bytes=1024)
|
|
with patch.object(rendering, "RENDER_PROCESS_LIMITS", limits):
|
|
with self.assertRaisesRegex(TemplateRenderError, "output_limit"):
|
|
rendering._render_payload(definition, revision, request=request, items=[{}])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|