import base64 from datetime import datetime, timezone import os import tempfile import unittest from pdf_report import ( ReportValidationError, report_from_calculation_result, render_roll_report, safe_report_filename, validate_report_payload, ) from roll_calculation import calculate_roll try: import app as rollcalc_app except ModuleNotFoundError as error: if error.name not in {"flask", "flask_httpauth", "werkzeug"}: raise rollcalc_app = None def normal_payload(*, include_weight=True, include_warning=True): payload = { "report_type": "roll_diameter", "article": {"number": "146900", "name": "Stex R 1801, 5.80 x 50 m"}, "inputs": [ {"label": "Core diameter", "value": "150.0 mm"}, {"label": "Core type", "value": "150mm PVC"}, {"label": "Product thickness", "value": "10.885 mm"}, {"label": "Thickness stddev", "value": "0.427 mm"}, {"label": "Product length", "value": "50.00 m"}, {"label": "Roll width", "value": "5.80 m"}, {"label": "Area weight", "value": "1767.2 g/m²"}, ], "diameters": { "minimum": {"value": 612.3, "display": "612.3 mm"}, "average": {"value": 635.7, "display": "635.7 mm"}, "maximum": {"value": 658.1, "display": "658.1 mm"}, }, "roll_length": {"value": 50.0, "display": "50.00 m"}, "roll_weight": ( {"value": 512.5, "display": "512.5 kg"} if include_weight else None ), "warnings": [], "notes": [], } if include_warning: payload["warnings"].append( { "severity": "warning", "title": "Heavy Roll - Special Equipment Required", "message": "Special equipment required. Core diameter too small.", } ) return payload def normal_request(*, include_weight=True): return { "intent": "calculate_roll_diameter", "article_number": "146900", "roll_length_m": 50.0, "width_m": 5.8 if include_weight else None, "core_diameter_mm": 150.0, "category": "bentofix", "include_roll_weight": include_weight, } @unittest.skipUnless(rollcalc_app, "Flask dependencies are not installed") class PdfEndpointTests(unittest.TestCase): def setUp(self): self.temp_dir = tempfile.TemporaryDirectory() self.original_log_file = rollcalc_app.LOG_FILE rollcalc_app.LOG_FILE = os.path.join(self.temp_dir.name, "access.json") rollcalc_app.app.config.update(TESTING=True) self.client = rollcalc_app.app.test_client() credentials = base64.b64encode(b"mtazl:rollcalc").decode("ascii") self.auth_headers = {"Authorization": f"Basic {credentials}"} def tearDown(self): rollcalc_app.LOG_FILE = self.original_log_file self.temp_dir.cleanup() def post_report(self, payload): return self.client.post( "/api/reports/roll-calculation.pdf", json=payload, headers=self.auth_headers, ) def test_normal_request_is_recalculated_into_one_page_pdf(self): response = self.post_report(normal_request()) self.assertEqual(response.status_code, 200) self.assertEqual(response.mimetype, "application/pdf") self.assertTrue(response.data.startswith(b"%PDF-1.4")) self.assertEqual(response.data.count(b"/Type /Page "), 1) self.assertIn(b"CALCULATION RESULTS", response.data) self.assertNotIn(b"CALCULATED DIAMETERS", response.data) for expected in ( b"813.1 mm", b"845.9 mm", b"877.4 mm", b"50.00 m", b"512.5 kg", b"within standard limits", ): self.assertIn(expected, response.data) self.assertGreaterEqual(response.data.count(b"INTERNAL USE ONLY"), 3) self.assertIn("roll-calculation_146900_", response.headers["Content-Disposition"]) def test_report_without_optional_weight_still_succeeds(self): response = self.post_report( normal_request(include_weight=False) ) self.assertEqual(response.status_code, 200) self.assertNotIn(b"CALCULATED ROLL WEIGHT", response.data) self.assertIn(b"845.9 mm", response.data) def test_incomplete_or_caller_supplied_results_are_rejected(self): incomplete = normal_request() del incomplete["roll_length_m"] response = self.post_report(incomplete) self.assertEqual(response.status_code, 400) self.assertEqual(response.mimetype, "application/json") caller_result = normal_request() caller_result["diameters"] = {"average": 1} response = self.post_report(caller_result) self.assertEqual(response.status_code, 400) def test_endpoint_requires_authentication(self): response = self.client.post( "/api/reports/roll-calculation.pdf", json=normal_payload() ) self.assertEqual(response.status_code, 401) class PdfReportModelTests(unittest.TestCase): def test_low_level_renderer_consumes_validated_layout_model(self): report = validate_report_payload(normal_payload()) pdf = render_roll_report( report, generated_at=datetime(2026, 8, 28, 12, 30, tzinfo=timezone.utc), build_info={"version": "v0.4", "commit": "fe267b8"}, ) self.assertIn(b"612.3 mm", pdf) self.assertIn(b"635.7 mm", pdf) self.assertIn(b"658.1 mm", pdf) self.assertIn(b"50.00 m", pdf) def test_non_numeric_diameter_is_rejected(self): payload = normal_payload() payload["diameters"]["average"]["value"] = "635.7" with self.assertRaises(ReportValidationError): validate_report_payload(payload) def test_display_value_mismatch_is_rejected(self): payload = normal_payload() payload["diameters"]["average"]["display"] = "999.9 mm" with self.assertRaises(ReportValidationError): validate_report_payload(payload) def test_effective_roll_length_is_required_and_must_match_display(self): missing = normal_payload() del missing["roll_length"] with self.assertRaises(ReportValidationError): validate_report_payload(missing) mismatched = normal_payload() mismatched["roll_length"]["display"] = "60.00 m" with self.assertRaises(ReportValidationError): validate_report_payload(mismatched) def test_filename_is_filesystem_safe(self): filename = safe_report_filename( "Article 12/../../unsafe", datetime(2026, 8, 28, tzinfo=timezone.utc), ) self.assertEqual( filename, "roll-calculation_Article-12-..-..-unsafe_2026-08-28_000000.pdf", ) self.assertNotIn(":", filename) def test_pdf_adapter_consumes_authoritative_calculation_result(self): result = calculate_roll(normal_request()) report = report_from_calculation_result(result) pdf = render_roll_report( report, generated_at=datetime(2026, 8, 28, 12, 30, tzinfo=timezone.utc), build_info={"version": "v0.4", "commit": "fe267b8"}, ) self.assertIn(b"845.9 mm", pdf) self.assertIn(b"50.00 m", pdf) self.assertIn(b"512.5 kg", pdf) def test_multiple_warnings_are_wrapped_without_creating_a_second_page(self): payload = normal_payload() payload["warnings"] = [ { "severity": "warning", "title": f"Warning {index}", "message": ( "This complete engineering warning remains in the report and " "wraps when the available line width is reached." ), } for index in range(1, 7) ] report = validate_report_payload(payload) pdf = render_roll_report( report, generated_at=datetime(2026, 8, 28, 12, 30, tzinfo=timezone.utc), build_info={"version": "v0.4", "commit": "fe267b8"}, ) self.assertEqual(pdf.count(b"/Type /Page "), 1) for index in range(1, 7): self.assertIn(f"Warning {index}".encode("ascii"), pdf) if __name__ == "__main__": unittest.main()