from pathlib import Path import sys import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api")) pytest.importorskip("pptx") from insurance.ppt.template_asset_service import ( parse_template_pptx, resolve_template_asset, ) @pytest.mark.parametrize( ("asset_id", "expected_slides"), [ ("builtin://single_savings.pptx", 20), ("builtin://multi_savings_comparison.pptx", 17), ("builtin://savings_iul_comprehensive.pptx", 18), ], ) def test_builtin_ppt_templates_are_parseable(asset_id, expected_slides): path = resolve_template_asset(asset_id) assert path assert Path(path).is_file() parsed = parse_template_pptx(path) assert parsed["slideCount"] == expected_slides assert parsed["slidesConfig"][0]["pageType"] == "cover" assert parsed["slidesConfig"][-1]["pageType"] == "closing" def test_template_asset_resolver_blocks_parent_traversal(): with pytest.raises(ValueError, match="路径非法"): resolve_template_asset("builtin://../outside.pptx") @pytest.mark.parametrize( ("asset_id", "expect_picture"), [ ("builtin://single_savings.pptx", False), ("builtin://multi_savings_comparison.pptx", False), ("builtin://savings_iul_comprehensive.pptx", True), ], ) def test_renderer_uses_builtin_template_frames_without_sample_copy( tmp_path, asset_id, expect_picture ): from insurance.ppt.scripts.fast_pptx_renderer import render_deck from pptx import Presentation source_path = resolve_template_asset(asset_id) output_path = tmp_path / f"{Path(source_path).stem}-rendered.pptx" result = render_deck({ "products": [{ "kind": "savings", "productName": "测试储蓄计划", "insured": {"age": 35}, "policy": { "currency": "USD", "annualPremium": 50000, "payYears": 5, }, "benefitRows": [], "withdrawalRows": [], }], "company": {}, "scenarioSlides": [ {"pageType": "cover", "title": "测试模板"}, {"pageType": "closing", "title": "下一步"}, ], "templateConfig": { "sourceTemplatePath": source_path, "slidesConfig": [ {"pageType": "cover", "sourceSlide": 1}, {"pageType": "closing", "sourceSlide": 2}, ], }, }, str(output_path), "deepblue") assert result["ok"] is True assert result["slides"] == 2 assert result["rendererMode"] == "clone-edit-v2" assert result["templateFrameMap"] == [1, 2] assert output_path.is_file() presentation = Presentation(str(output_path)) all_text = "\n".join( shape.text for slide in presentation.slides for shape in slide.shapes if getattr(shape, "has_text_frame", False) ) assert "测试模板" in all_text assert "US$500,000" not in all_text pictures = sum( 1 for slide in presentation.slides for shape in slide.shapes if shape.shape_type == 13 ) assert (pictures > 0) is expect_picture def test_cover_renderer_allows_null_optional_display_fields(tmp_path): from insurance.ppt.scripts.fast_pptx_renderer import render_deck output_path = tmp_path / "null-cover-fields.pptx" result = render_deck({ "customer": {"name": None}, "products": [{ "kind": "savings", "productName": None, "policy": {}, }], "company": {"displayName": None}, "scenarioSlides": [{ "pageType": "cover", "title": "{{customerName}} 专属方案", "subtitle": "{{productName}}", }], }, str(output_path), "deepblue") assert result["ok"] is True assert result["slides"] == 1 assert output_path.is_file() def test_renderer_rejects_uploaded_template_with_too_few_frames(tmp_path): from insurance.ppt.scripts.fast_pptx_renderer import render_deck source_path = resolve_template_asset("builtin://single_savings.pptx") output_path = tmp_path / "too-many-slides.pptx" scenario_slides = [ {"pageType": "guidance", "title": f"第 {index} 页"} for index in range(21) ] with pytest.raises(ValueError, match="只有 20 页"): render_deck({ "products": [{"kind": "savings"}], "company": {}, "scenarioSlides": scenario_slides, "templateConfig": { "sourceTemplatePath": source_path, "slidesConfig": [{"pageType": "guidance"}] * 20, }, }, str(output_path), "deepblue")