232 lines
8.3 KiB
Python
232 lines
8.3 KiB
Python
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()
|