"""Unit-Tests fuer den Founding-Wizard Service. Testet: - Template-Renderer mit verschiedenen Inputs - Wizard-State -> Context Mapping - Markdown -> DOCX Konvertierung """ from __future__ import annotations import pytest from compliance.services.founding_wizard.template_renderer import ( find_undefined_placeholders, render_template, ) from compliance.services.founding_wizard.wizard_to_context import base_context class TestTemplateRenderer: def test_simple_variable_substitution(self): result = render_template("Hallo {{NAME}}!", {"NAME": "Welt"}) assert result == "Hallo Welt!" def test_missing_variable_placeholder(self): result = render_template("Hallo {{NAME}}!", {}) assert "[NAME fehlt]" in result def test_if_block_truthy(self): result = render_template( "Start {{#IF FLAG}}drin{{/IF}} Ende", {"FLAG": True} ) assert result == "Start drin Ende" def test_if_block_falsy(self): result = render_template( "Start {{#IF FLAG}}drin{{/IF}} Ende", {"FLAG": False} ) assert result == "Start Ende" def test_if_not_block(self): result = render_template( "{{#IF NOT FLAG}}negiert{{/IF}}", {"FLAG": False} ) assert "negiert" in result def test_truthy_int(self): result = render_template("{{#IF N}}yes{{/IF}}", {"N": 5}) assert "yes" in result def test_falsy_zero(self): result = render_template("{{#IF N}}yes{{/IF}}", {"N": 0}) assert "yes" not in result def test_truthy_list(self): result = render_template("{{#IF L}}yes{{/IF}}", {"L": [1, 2]}) assert "yes" in result def test_falsy_empty_list(self): result = render_template("{{#IF L}}yes{{/IF}}", {"L": []}) assert "yes" not in result def test_nested_if_blocks(self): template = "{{#IF A}}A-on{{#IF B}}+B-on{{/IF}}{{/IF}}" result = render_template(template, {"A": True, "B": True}) assert result == "A-on+B-on" def test_nested_outer_false_inner_true(self): """Bug-Regression: nested IF in outer-false darf nicht den falschen close-tag matchen.""" template = "{{#IF OUTER}}outer-{{#IF INNER}}inner{{/IF}}-end{{/IF}}AFTER" result = render_template(template, {"OUTER": False, "INNER": True}) assert result == "AFTER" assert "{{/IF}}" not in result assert "outer" not in result def test_consecutive_if_blocks(self): """Bug-Regression: 2 aufeinanderfolgende IF-Bloecke.""" template = "{{#IF A}}a{{/IF}}{{#IF B}}b{{/IF}}" result = render_template(template, {"A": False, "B": True}) assert result == "b" assert "{{" not in result def test_orphan_if_tag_removed(self): """Orphan {{/IF}} aufraeumen.""" template = "Text{{/IF}}mehr" result = render_template(template, {}) assert "{{/IF}}" not in result def test_real_go_gf_pattern(self): """Realistic Pattern aus GO-GF Template.""" template = ( "{{#IF HAS_CEO_DESIGNATION}}Mit CEO {{CEO_NAME}}{{#IF HAS_SHA}} und SHA{{/IF}}.{{/IF}}" "{{#IF NOT HAS_CEO_DESIGNATION}}Kein CEO. Eskalation nach § 6.{{/IF}}" ) # Fall: kein CEO, kein SHA r1 = render_template(template, {"HAS_CEO_DESIGNATION": False, "HAS_SHA": False}) assert r1 == "Kein CEO. Eskalation nach § 6." # Fall: CEO + SHA r2 = render_template(template, {"HAS_CEO_DESIGNATION": True, "HAS_SHA": True, "CEO_NAME": "Benjamin"}) assert r2 == "Mit CEO Benjamin und SHA." # Fall: CEO ohne SHA r3 = render_template(template, {"HAS_CEO_DESIGNATION": True, "HAS_SHA": False, "CEO_NAME": "Benjamin"}) assert r3 == "Mit CEO Benjamin." def test_find_undefined_placeholders(self): template = "{{X}} {{Y}} {{#IF Z}}.{{/IF}}" undefined = find_undefined_placeholders(template, {"X": "1"}) assert "Y" in undefined assert "Z" in undefined assert "X" not in undefined class TestWizardToContext: def _basic_state(self) -> dict: return { "basics": { "company_name": "Test GmbH", "legal_form": "GmbH", "company_seat": "Stuttgart", "company_address": "Königstraße 1, 70173 Stuttgart", "company_purpose_description": "Test purpose", "company_purpose_bullets": ["a) Test", "b) Test 2"], "industry": "SaaS", "business_year": "Kalenderjahr", "has_research_focus": True, }, "capital": { "stammkapital_eur": 25000, "einlage_method": "Geld", "einlage_quote_initial_pct": 50, "has_sacheinlage": False, }, "gesellschafter": [ { "id": "g1", "anteil_nr": 1, "name": "Benjamin Bönisch", "geburtsdatum": "1980-01-01", "adresse": "Test 1", "nennbetrag_eur": 12500, "is_geschaeftsfuehrer": True, "internal_role": "CEO", "rolle": "founder", "has_academic_background": False, }, { "id": "g2", "anteil_nr": 2, "name": "Sharang Parnerkar", "geburtsdatum": "1985-05-15", "adresse": "Test 2", "nennbetrag_eur": 12500, "is_geschaeftsfuehrer": True, "internal_role": "CTO", "rolle": "founder", "has_academic_background": False, }, ], "notar": { "notary_name": "Dr. Notar", "notary_place": "Stuttgart", "notarial_date": "2026-06-01", }, "sha": { "has_sha": True, "vesting_months": 48, "cliff_months": 12, "drag_along_threshold_pct": 75, "tag_along_threshold_pct": 20, "reserved_matters_majority_pct": 75, "has_beirat": False, "has_texas_shootout": False, "has_ceo_designation": False, }, } def test_basics_in_context(self): ctx = base_context(self._basic_state()) assert ctx["COMPANY_NAME"] == "Test GmbH" assert ctx["COMPANY_LEGAL_FORM"] == "GmbH" assert ctx["COMPANY_SEAT"] == "Stuttgart" assert ctx["STAMMKAPITAL_EUR"] == "25.000" def test_num_gf_2_man(self): ctx = base_context(self._basic_state()) assert ctx["NUM_GF"] == 2 assert ctx["NUM_GF_TEXT"] == "zwei" assert ctx["IS_MULTI_GF"] is True assert ctx["NUM_GF_IS_2"] is True assert ctx["NUM_GF_GT_2"] is False def test_parties_list_format(self): ctx = base_context(self._basic_state()) plist = ctx["PARTIES_LIST"] assert "Benjamin Bönisch" in plist assert "Sharang Parnerkar" in plist assert "a)" in plist assert "b)" in plist def test_flags_default(self): ctx = base_context(self._basic_state()) assert ctx["HAS_SHA"] is True assert ctx["HAS_RESEARCH_FOCUS"] is True assert ctx["HAS_ACADEMIC_FOUNDER"] is False assert ctx["HAS_BEIRAT"] is False def test_academic_flag_detection(self): state = self._basic_state() state["gesellschafter"][0]["has_academic_background"] = True ctx = base_context(state) assert ctx["HAS_ACADEMIC_FOUNDER"] is True class TestMarkdownToDocx: def test_basic_conversion(self): from compliance.services.founding_wizard.markdown_to_docx import markdown_to_docx_bytes md = "# Titel\n\nDas ist ein Absatz.\n\n## Unterthema\n\n- Punkt 1\n- Punkt 2" result = markdown_to_docx_bytes(md) assert isinstance(result, bytes) # DOCX is a ZIP file starting with PK\x03\x04 assert result[:4] == b"PK\x03\x04" assert len(result) > 1000 # reasonable size def test_table_conversion(self): from compliance.services.founding_wizard.markdown_to_docx import markdown_to_docx_bytes md = "| A | B |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |" result = markdown_to_docx_bytes(md) assert result[:4] == b"PK\x03\x04" def test_bold_italic(self): from compliance.services.founding_wizard.markdown_to_docx import markdown_to_docx_bytes md = "Das ist **fett** und _kursiv_ und `code`." result = markdown_to_docx_bytes(md) assert result[:4] == b"PK\x03\x04" class TestPerPersonContext: """Tests fuer per-person Context-Building (IP-Assignment, GF-Vertrag).""" def test_build_person_context_ip_areas_as_list(self): from compliance.api.founding_wizard_routes import _build_person_context base = {"COMPANY_NAME": "X GmbH"} person = { "name": "Benjamin Bönisch", "geburtsdatum": "1980-01-01", "adresse": "Test 1", "internal_role": "CEO", "ip_areas": ["Compliance-Engine", "RAG-Pipeline"], } ctx = _build_person_context(base, person, "ip_assignment_agreement") assert ctx["ASSIGNOR_NAME"] == "Benjamin Bönisch" assert "Compliance-Engine" in ctx["IP_LIST_DETAILS"] assert "RAG-Pipeline" in ctx["IP_LIST_DETAILS"] # Two distinct persons should yield distinct IP_LIST_DETAILS person2 = {**person, "name": "Sharang", "ip_areas": ["Security", "Infrastruktur"]} ctx2 = _build_person_context(base, person2, "ip_assignment_agreement") assert ctx["IP_LIST_DETAILS"] != ctx2["IP_LIST_DETAILS"] assert "Security" in ctx2["IP_LIST_DETAILS"] def test_build_person_context_fallback_when_no_ip_areas(self): """Wenn keine ip_areas gesetzt sind, behaelt der Context den Default aus base.""" from compliance.api.founding_wizard_routes import _build_person_context base = {"COMPANY_NAME": "X GmbH", "IP_LIST_DETAILS": "- Default IP"} person = {"name": "Foo", "ip_areas": []} ctx = _build_person_context(base, person, "ip_assignment_agreement") assert ctx["IP_LIST_DETAILS"] == "- Default IP" def test_safe_slug_handles_special_chars(self): from compliance.api.founding_wizard_routes import _safe_slug assert _safe_slug("Benjamin Bönisch") == "Benjamin_B_nisch" assert _safe_slug("Sharang Parnerkar") == "Sharang_Parnerkar" assert _safe_slug("") == "Person" assert _safe_slug(" ") == "Person" def test_per_person_docs_set_contains_expected(self): from compliance.api.founding_wizard_routes import PER_PERSON_DOCS assert "ip_assignment_agreement" in PER_PERSON_DOCS assert "managing_director_employment_contract" in PER_PERSON_DOCS # Satzung etc. duerfen NICHT per-person sein: assert "articles_of_association" not in PER_PERSON_DOCS assert "sha" not in PER_PERSON_DOCS class TestBasicsRegisterCourt: def test_register_court_propagates(self): state = TestWizardToContext()._basic_state() state["basics"]["register_court"] = "Amtsgericht Stuttgart" state["basics"]["hrb_number"] = "HRB 12345" ctx = base_context(state) assert ctx["REGISTER_COURT"] == "Amtsgericht Stuttgart" assert ctx["COMPANY_REGISTRY_COURT"] == "Amtsgericht Stuttgart" assert ctx["HRB_NUMBER"] == "HRB 12345" assert ctx["HAS_HRB"] is True def test_register_court_default_when_missing(self): ctx = base_context(TestWizardToContext()._basic_state()) assert "[zuständiges Amtsgericht]" in ctx["REGISTER_COURT"] assert ctx["HAS_HRB"] is False class TestEndToEndRendering: """Test mit echtem Template-aehnlichen Markdown + 2-Mann GmbH Daten.""" def test_minimum_satzung_render(self): template = """# Satzung der {{COMPANY_NAME}} ## § 1 Firma (1) Die Gesellschaft führt die Firma {{COMPANY_NAME}}. (2) Sitz ist {{COMPANY_SEAT}}. {{#IF HAS_SHA}} ## § 5 SHA-Verweis Es gilt das SHA. {{/IF}} {{#IF NOT HAS_SHA}} ## § 5 Hinweis Kein SHA vereinbart. {{/IF}} """ ctx = base_context(TestWizardToContext()._basic_state()) result = render_template(template, ctx) assert "Test GmbH" in result assert "Stuttgart" in result assert "§ 5 SHA-Verweis" in result assert "Kein SHA vereinbart" not in result