Files
breakpilot-compliance/backend-compliance/tests/test_founding_wizard.py
Benjamin Admin 7335f64f4f
CI / loc-budget (push) Failing after 20s
CI / detect-changes (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
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 19s
CI / nodejs-build (push) Successful in 3m17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
feat(founding-wizard): Per-Person IP-Assignment + Prefill + E2E-Tests
Wizard unterstuetzt jetzt 2-4 Gesellschafter mit individuellem IP-Bereich:
- Pro Gruender ein IP-Assignment-Vertrag (z.B. Benjamin: Compliance+RAG;
  Sharang: Security+Infrastruktur). Pro GF ein eigener Dienstvertrag.
- Step 1: Prefill-Button aus Unternehmensprofil + Felder Registergericht
  und HRB-Nr.
- Step 2: Rollen-Dropdown (CEO/CTO/CFO/COO/CPO/GF/Sonstige) statt freie
  Texteingabe, IP-Bereiche-Textarea pro Person.

Backend:
- generate_documents() iteriert pro Person fuer PER_PERSON_DOCS.
- _build_person_context() injiziert ASSIGNOR_*, GF_*, IP_LIST_DETAILS
  aus person.ip_areas.
- base_context() propagiert basics.register_court und basics.hrb_number.

Tests:
- 30/30 Pytest gruen (6 neue: Per-Person-Context, Slug-Helper,
  Registergericht-Propagation).
- 4 neue Playwright-E2E-Specs (hermetisch via route.fulfill, mit
  Console-/Page-Error-Traps): kompletter 8-Step-Flow, Prefill-Fehlerpfad,
  Step-Navigation/Reset, Rollen-Dropdown + IP-Areas.
- Spec setzt 'bp-sdk-cookie-consent' im addInitScript damit der
  CookieBannerOverlay nicht die Wizard-Buttons ueberlagert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:49:10 +02:00

321 lines
12 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 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