Files
breakpilot-compliance/backend-compliance/tests/test_founding_wizard.py
T
Benjamin Admin badb356740
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 42s
CI / detect-changes (push) Successful in 10s
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Successful in 16s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
fix(founding-wizard): nested IF-Bloecke korrekt aufloesen (innermost-first)
2026-05-20 19:21:08 +02:00

257 lines
9.2 KiB
Python

"""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 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