"""Dependency-free synthetic tests for manifest and PPTX security helpers.""" from __future__ import annotations import contextlib import io import json import sys import tempfile import unittest import zipfile from pathlib import Path SKILL_ROOT = Path(__file__).resolve().parents[2] / "skills" / "pptx-posters" SCRIPTS = SKILL_ROOT / "scripts" TESTS = Path(__file__).resolve().parent sys.path.insert(0, str(SCRIPTS)) sys.path.insert(0, str(TESTS)) from _common import CliError, atomic_write_bytes, load_json_file # noqa: E402 from _manifest import ( # noqa: E402 manifest_content_hash, validate_manifest_document, ) from _pptx import MAX_MEMBERS, inspect_pptx, require_safe_pptx # noqa: E402 from check_palette import audit_palette, main as palette_main # noqa: E402 from plan_export import build_export_plan # noqa: E402 from synthetic_posters import build_manifest # noqa: E402 CONTENT_TYPES = """ """ ROOT_RELS = """ """ PRESENTATION = """ """ PRESENTATION_RELS = """ """ SLIDE = """ Synthetic title """ def write_minimal_pptx( path: Path, *, content_types: str = CONTENT_TYPES, root_relationships: str = ROOT_RELS, extras: dict[str, bytes] | None = None, ) -> None: members = { "[Content_Types].xml": content_types.encode("utf-8"), "_rels/.rels": root_relationships.encode("utf-8"), "ppt/presentation.xml": PRESENTATION.encode("utf-8"), "ppt/_rels/presentation.xml.rels": PRESENTATION_RELS.encode("utf-8"), "ppt/slides/slide1.xml": SLIDE.encode("utf-8"), } members.update(extras or {}) with zipfile.ZipFile( path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6 ) as archive: for name, payload in members.items(): archive.writestr(name, payload) class ManifestTests(unittest.TestCase): def test_complete_synthetic_manifest_passes(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "poster.json" manifest = build_manifest(manifest_content_hash) report = validate_manifest_document( manifest, manifest_path=path, verify_assets=False, require_approval=True, ) self.assertTrue(report["valid"]) self.assertEqual(report["approval_status"], "approved") self.assertEqual(report["counts"]["elements"], 2) def test_placeholder_fails_closed(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["document"]["title"] = "REPLACE_ME_TITLE" with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "placeholder"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_unknown_exact_source_id_is_rejected(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["elements"][1]["source_ids"] = ["SRC-MISSING"] with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "unknown exact source"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_title_must_be_first_in_reading_order(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["elements"][0]["reading_order"] = 2 manifest["elements"][1]["reading_order"] = 1 manifest["elements"].reverse() manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "title element"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_asset_requires_explicit_provenance(self) -> None: manifest = build_manifest( manifest_content_hash, include_image=True, ) del manifest["assets"][0]["provenance"] with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "provenance"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_long_description_is_source_bound_and_follows_image(self) -> None: manifest = build_manifest( manifest_content_hash, include_image=True, ) manifest["elements"][2]["height_in"] = 4.5 manifest["elements"][2]["long_description_element_id"] = "image-description" manifest["elements"].append( { "id": "image-description", "type": "text", "reading_order": 4, "x_in": 4.5, "y_in": 6.25, "width_in": 5.0, "height_in": 1.25, "source_ids": ["SRC-ASSET"], "author_approved": True, "allow_in_bleed": False, "role": "caption", "text": ( "Synthetic long description bound to the local test-image source." ), "font_size_pt_design": 18.0, "font_face": "Arial", "bold": False, "align": "left", "vertical_align": "top", "contrast_pair_id": "black_on_white", "line_color_id": None, "line_width_pt": 0, "margin_in": 0.1, } ) manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "poster.json" report = validate_manifest_document( manifest, manifest_path=path, verify_assets=False, ) self.assertTrue(report["valid"]) manifest["elements"][3]["source_ids"] = ["SRC-CONTENT"] manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with self.assertRaisesRegex(CliError, "every source_id"): validate_manifest_document( manifest, manifest_path=path, verify_assets=False, ) def test_content_change_invalidates_author_approval(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["elements"][1]["text"] += " Changed." with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "approval.content_sha256"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_control_characters_are_rejected_before_generation(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["document"]["title"] = "Unsafe\u0001title" manifest["elements"][0]["text"] = "Unsafe\u0001title" with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "control character"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_remote_asset_path_is_rejected(self) -> None: manifest = build_manifest( manifest_content_hash, include_image=True, image_path="https://invalid.example/image.png", ) with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "relative local path"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_qr_requires_exact_visible_fallback_url(self) -> None: manifest = build_manifest( manifest_content_hash, include_image=True, ) asset = manifest["assets"][0] asset["role"] = "qr_code" asset["qr_target"] = "https://example.invalid/exact" image = manifest["elements"][2] image["height_in"] = 5.0 image["fallback_text_element_id"] = "qr-fallback" manifest["elements"].append( { "id": "qr-fallback", "type": "text", "reading_order": 4, "x_in": 4.5, "y_in": 6.5, "width_in": 5.0, "height_in": 1.0, "source_ids": ["SRC-ASSET"], "author_approved": True, "allow_in_bleed": False, "role": "qr_fallback", "text": "Destination: https://example.invalid/exact", "font_size_pt_design": 18.0, "font_face": "Arial", "bold": False, "align": "left", "vertical_align": "middle", "contrast_pair_id": "black_on_white", "line_color_id": None, "line_width_pt": 0, "margin_in": 0.1, } ) manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "poster.json" report = validate_manifest_document( manifest, manifest_path=path, verify_assets=False, ) self.assertTrue(report["valid"]) manifest["elements"][3]["text"] = "Destination omitted" manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with self.assertRaisesRegex(CliError, "exact QR target"): validate_manifest_document( manifest, manifest_path=path, verify_assets=False, ) def test_bad_declared_contrast_is_rejected(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["palette"]["colors"]["black"] = "#777777" with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "below"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_bad_contrast_can_be_reported_by_palette_audit(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["palette"]["colors"]["black"] = "#777777" manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: validation = validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, enforce_contrast=False, ) self.assertTrue(validation["valid"]) report = audit_palette(manifest) self.assertFalse(report["pass"]) def test_palette_cli_uses_finding_exit_code(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["palette"]["colors"]["black"] = "#777777" manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "poster.json" path.write_text(json.dumps(manifest), encoding="utf-8") with contextlib.redirect_stdout(io.StringIO()): result = palette_main([str(path)]) self.assertEqual(result, 1) def test_template_is_not_valid_content(self) -> None: template_path = SKILL_ROOT / "assets" / "poster_manifest_template.json" _, template = load_json_file(template_path) with self.assertRaises(CliError): validate_manifest_document( template, manifest_path=template_path, verify_assets=False, require_approval=False, ) def test_uniform_physical_scaling_is_explicit(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["physical_output"]["trim_width_in"] = 20.0 manifest["physical_output"]["trim_height_in"] = 16.0 manifest["requirements"]["conference"]["max_width_in"] = 20.0 manifest["requirements"]["conference"]["max_height_in"] = 16.0 manifest["requirements"]["printer"]["trim_width_in"] = 20.0 manifest["requirements"]["printer"]["trim_height_in"] = 16.0 manifest["requirements"]["printer"]["scaling_allowed"] = True manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: report = validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) self.assertEqual(report["physical_output"]["print_scale"], 2.0) def test_nonuniform_physical_scaling_is_rejected(self) -> None: manifest = build_manifest(manifest_content_hash) manifest["physical_output"]["trim_width_in"] = 20.0 manifest["physical_output"]["trim_height_in"] = 15.0 manifest["requirements"]["conference"]["max_width_in"] = 20.0 manifest["requirements"]["conference"]["max_height_in"] = 15.0 manifest["requirements"]["printer"]["trim_width_in"] = 20.0 manifest["requirements"]["printer"]["trim_height_in"] = 15.0 manifest["requirements"]["printer"]["scaling_allowed"] = True manifest["approval"]["content_sha256"] = manifest_content_hash(manifest) with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(CliError, "aspect ratios"): validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) def test_duplicate_json_key_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "duplicate.json" path.write_text('{"key": 1, "key": 2}', encoding="utf-8") with self.assertRaisesRegex(CliError, "duplicate JSON key"): load_json_file(path) class OutputSafetyTests(unittest.TestCase): def test_output_never_overwrites(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "report.json" atomic_write_bytes(path, b"first") with self.assertRaisesRegex(CliError, "overwrite"): atomic_write_bytes(path, b"second") self.assertEqual(path.read_bytes(), b"first") class PaletteAndPlanTests(unittest.TestCase): def test_palette_reports_reference_contrast(self) -> None: manifest = build_manifest(manifest_content_hash) report = audit_palette(manifest) self.assertTrue(report["pass"]) self.assertEqual( report["declared_contrast_pairs"][0]["ratio"], 21.0, ) def test_cmyk_requirement_blocks_readiness(self) -> None: manifest = build_manifest( manifest_content_hash, printer_color_mode="CMYK", ) with tempfile.TemporaryDirectory() as directory: validation = validate_manifest_document( manifest, manifest_path=Path(directory) / "poster.json", verify_assets=False, ) plan = build_export_plan(manifest, validation) self.assertFalse(plan["ready_for_manual_export"]) self.assertEqual(plan["blockers"][0]["code"], "CMYK_CONVERSION_REQUIRED") self.assertEqual(plan["font_preflight"]["declared_font_faces"], ["Arial"]) self.assertFalse(plan["font_preflight"]["fonts_embedded_by_generator"]) self.assertFalse( plan["media_profile"]["audio_video_or_linked_media_allowed"] ) class PptxSecurityTests(unittest.TestCase): def test_minimal_macro_free_package_passes(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "safe.pptx" write_minimal_pptx(path) report = inspect_pptx(path) self.assertTrue(report["safe"], report["findings"]) self.assertEqual(report["accessibility"]["slide_title_count"], 1) self.assertEqual( report["accessibility"]["text_runs_missing_language"], [], ) require_safe_pptx(path) def test_pptm_extension_is_rejected_without_opening(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "unsafe.pptm" write_minimal_pptx(path) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertEqual(report["findings"][0]["code"], "FILE_EXTENSION") def test_external_relationship_is_rejected(self) -> None: relationships = ROOT_RELS.replace( "", ( '' ), ) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "external.pptx" write_minimal_pptx(path, root_relationships=relationships) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "EXTERNAL_RELATIONSHIP", {finding["code"] for finding in report["findings"]}, ) def test_invalid_relationship_target_mode_is_rejected(self) -> None: relationships = ROOT_RELS.replace( "", ( '' ), ) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "target-mode.pptx" write_minimal_pptx(path, root_relationships=relationships) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "RELATIONSHIP_TARGET_MODE_INVALID", {finding["code"] for finding in report["findings"]}, ) def test_remote_linked_image_is_rejected(self) -> None: relationships = ROOT_RELS.replace( "", ( '' ), ) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "linked-image.pptx" write_minimal_pptx(path, root_relationships=relationships) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "REMOTE_LINKED_IMAGE", {finding["code"] for finding in report["findings"]}, ) def test_macro_content_hidden_in_pptx_is_rejected(self) -> None: macro_types = CONTENT_TYPES.replace( "", ( '' ), ) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "macro-disguised.pptx" write_minimal_pptx( path, content_types=macro_types, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) codes = {finding["code"] for finding in report["findings"]} self.assertIn("FORBIDDEN_CONTENT_TYPE", codes) def test_ole_markup_without_payload_is_rejected(self) -> None: slide = ( b'' b'' ) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "ole-markup.pptx" write_minimal_pptx( path, extras={"ppt/slides/slide1.xml": slide}, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "FORBIDDEN_PRESENTATION_MARKUP", {finding["code"] for finding in report["findings"]}, ) def test_non_utf8_entity_bearing_xml_is_rejected(self) -> None: unsafe_slide = ( '' ']>' '&x;' ).encode("utf-16") with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "utf16-entity.pptx" write_minimal_pptx( path, extras={"ppt/slides/slide1.xml": unsafe_slide}, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "XML_PART_INVALID", {finding["code"] for finding in report["findings"]}, ) def test_traversal_member_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "traversal.pptx" write_minimal_pptx(path, extras={"../escape.txt": b"unsafe"}) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "ZIP_TRAVERSAL", {finding["code"] for finding in report["findings"]}, ) def test_unknown_package_part_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "unknown-part.pptx" write_minimal_pptx(path, extras={"ppt/unknown.dat": b"opaque"}) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "UNKNOWN_PACKAGE_PART", {finding["code"] for finding in report["findings"]}, ) def test_strict_profile_requires_exactly_one_slide(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "two-slides.pptx" write_minimal_pptx( path, extras={"ppt/slides/slide2.xml": SLIDE.encode("utf-8")}, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "SLIDE_COUNT", {finding["code"] for finding in report["findings"]}, ) def test_image_signature_must_match_suffix(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "bad-image.pptx" write_minimal_pptx( path, extras={"ppt/media/image1.png": b"not-a-png"}, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "IMAGE_SIGNATURE", {finding["code"] for finding in report["findings"]}, ) def test_embedded_binary_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "embedded.pptx" write_minimal_pptx( path, extras={"ppt/embeddings/embeddedObject1.bin": b"unsafe"}, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) codes = {finding["code"] for finding in report["findings"]} self.assertIn("FORBIDDEN_PACKAGE_PART", codes) self.assertIn("FORBIDDEN_BINARY_PART", codes) def test_excessive_compression_ratio_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "bomb.pptx" write_minimal_pptx( path, extras={"ppt/media/repetitive.txt": b"0" * (1024 * 1024)}, ) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "ZIP_COMPRESSION_RATIO", {finding["code"] for finding in report["findings"]}, ) def test_declared_member_count_is_bounded_before_materialization(self) -> None: extras = { f"ppt/media/empty-{index}.png": b"" for index in range(MAX_MEMBERS) } with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "too-many-members.pptx" write_minimal_pptx(path, extras=extras) report = inspect_pptx(path) self.assertFalse(report["safe"]) self.assertIn( "ZIP_MEMBER_LIMIT", {finding["code"] for finding in report["findings"]}, ) if __name__ == "__main__": unittest.main()