feat: Applicability Engine + API-Filter + DB-Sync + Cleanup
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 35s
CI / test-python-voice (push) Successful in 33s
CI / test-bqas (push) Successful in 37s
CI / Deploy (push) Failing after 2s

- Applicability Engine (deterministisch, kein LLM): filtert Controls
  nach Branche, Unternehmensgroesse, Scope-Signalen
- API-Filter auf GET /controls, /controls-count, /controls-meta
- POST /controls/applicable Endpoint fuer Company-Profile-Matching
- 35 Unit-Tests fuer Engine
- Port-8098-Konflikt mit Nginx gefixt (nur expose, kein Host-Port)
- CLAUDE.md: control-pipeline Dokumentation ergaenzt
- 6 internationale Gesetze geloescht (ES/FR/HU/NL/SE/CZ — nur DACH)
- DB-Backup-Import-Script (import_backup.py)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-09 21:58:17 +02:00
parent ee5241a7bc
commit 441d5740bd
6 changed files with 829 additions and 1 deletions

View File

@@ -0,0 +1,229 @@
"""
Tests for the Applicability Engine (Phase C2).
Tests the deterministic filtering logic for industry, company size,
and scope signals without requiring a database connection.
"""
import pytest
from services.applicability_engine import (
_matches_company_size,
_matches_industry,
_matches_scope_signals,
_parse_json_text,
)
# =============================================================================
# _parse_json_text
# =============================================================================
class TestParseJsonText:
def test_none_returns_none(self):
assert _parse_json_text(None) is None
def test_valid_json_list(self):
assert _parse_json_text('["all"]') == ["all"]
def test_valid_json_list_multiple(self):
result = _parse_json_text('["Telekommunikation", "Energie"]')
assert result == ["Telekommunikation", "Energie"]
def test_valid_json_dict(self):
result = _parse_json_text('{"requires_any": ["uses_ai"]}')
assert result == {"requires_any": ["uses_ai"]}
def test_invalid_json_returns_none(self):
assert _parse_json_text("not json") is None
def test_empty_string_returns_none(self):
assert _parse_json_text("") is None
def test_already_list_passthrough(self):
val = ["all"]
assert _parse_json_text(val) == ["all"]
def test_already_dict_passthrough(self):
val = {"requires_any": ["uses_ai"]}
assert _parse_json_text(val) == val
def test_integer_returns_none(self):
assert _parse_json_text(42) is None
# =============================================================================
# _matches_industry
# =============================================================================
class TestMatchesIndustry:
def test_null_matches_any_industry(self):
assert _matches_industry(None, "Telekommunikation") is True
def test_all_matches_any_industry(self):
assert _matches_industry('["all"]', "Telekommunikation") is True
assert _matches_industry('["all"]', "Energie") is True
def test_specific_industry_matches(self):
assert _matches_industry(
'["Telekommunikation", "Energie"]', "Telekommunikation"
) is True
def test_specific_industry_no_match(self):
assert _matches_industry(
'["Telekommunikation", "Energie"]', "Gesundheitswesen"
) is False
def test_malformed_json_matches(self):
"""Malformed data should be treated as 'applies to everyone'."""
assert _matches_industry("not json", "anything") is True
def test_all_with_other_industries(self):
assert _matches_industry(
'["all", "Telekommunikation"]', "Gesundheitswesen"
) is True
# =============================================================================
# _matches_company_size
# =============================================================================
class TestMatchesCompanySize:
def test_null_matches_any_size(self):
assert _matches_company_size(None, "medium") is True
def test_all_matches_any_size(self):
assert _matches_company_size('["all"]', "micro") is True
assert _matches_company_size('["all"]', "enterprise") is True
def test_specific_size_matches(self):
assert _matches_company_size(
'["medium", "large", "enterprise"]', "large"
) is True
def test_specific_size_no_match(self):
assert _matches_company_size(
'["medium", "large", "enterprise"]', "small"
) is False
def test_micro_excluded_from_nis2(self):
"""NIS2 typically requires medium+."""
assert _matches_company_size(
'["medium", "large", "enterprise"]', "micro"
) is False
def test_malformed_json_matches(self):
assert _matches_company_size("broken", "medium") is True
# =============================================================================
# _matches_scope_signals
# =============================================================================
class TestMatchesScopeSignals:
def test_null_conditions_always_match(self):
assert _matches_scope_signals(None, ["uses_ai"]) is True
assert _matches_scope_signals(None, []) is True
def test_empty_requires_any_matches(self):
assert _matches_scope_signals('{"requires_any": []}', ["uses_ai"]) is True
def test_no_requires_any_key_matches(self):
assert _matches_scope_signals(
'{"description": "some text"}', ["uses_ai"]
) is True
def test_requires_any_with_matching_signal(self):
conditions = '{"requires_any": ["uses_ai"], "description": "AI Act"}'
assert _matches_scope_signals(conditions, ["uses_ai"]) is True
def test_requires_any_with_no_matching_signal(self):
conditions = '{"requires_any": ["uses_ai"], "description": "AI Act"}'
assert _matches_scope_signals(
conditions, ["third_country_transfer"]
) is False
def test_requires_any_with_one_of_multiple_matching(self):
conditions = '{"requires_any": ["uses_ai", "processes_health_data"]}'
assert _matches_scope_signals(
conditions, ["processes_health_data", "financial_data"]
) is True
def test_requires_any_with_no_signals_provided(self):
conditions = '{"requires_any": ["uses_ai"]}'
assert _matches_scope_signals(conditions, []) is False
def test_malformed_json_matches(self):
assert _matches_scope_signals("broken", ["uses_ai"]) is True
def test_multiple_required_signals_any_match(self):
"""requires_any means at least ONE must match."""
conditions = (
'{"requires_any": ["uses_ai", "third_country_transfer", '
'"processes_health_data"]}'
)
assert _matches_scope_signals(
conditions, ["third_country_transfer"]
) is True
def test_multiple_required_signals_none_match(self):
conditions = (
'{"requires_any": ["uses_ai", "third_country_transfer"]}'
)
assert _matches_scope_signals(
conditions, ["financial_data", "employee_monitoring"]
) is False
# =============================================================================
# Integration-style: combined filtering scenarios
# =============================================================================
class TestCombinedFiltering:
"""Test typical real-world filtering scenarios."""
def test_dsgvo_art5_applies_to_everyone(self):
"""DSGVO Art. 5 = all industries, all sizes, no scope conditions."""
assert _matches_industry('["all"]', "Telekommunikation") is True
assert _matches_company_size('["all"]', "micro") is True
assert _matches_scope_signals(None, []) is True
def test_nis2_art21_kritis_medium_plus(self):
"""NIS2 Art. 21 = KRITIS sectors, medium+."""
industries = '["Energie", "Gesundheitswesen", "Digitale Infrastruktur", "Logistik / Transport"]'
sizes = '["medium", "large", "enterprise"]'
# Matches: Energie + large
assert _matches_industry(industries, "Energie") is True
assert _matches_company_size(sizes, "large") is True
# No match: IT company
assert _matches_industry(industries, "Technologie / IT") is False
# No match: small company
assert _matches_company_size(sizes, "small") is False
def test_ai_act_scope_condition(self):
"""AI Act = all industries, all sizes, but only if uses_ai."""
conditions = '{"requires_any": ["uses_ai"], "description": "Nur bei KI-Einsatz"}'
# Company uses AI
assert _matches_scope_signals(conditions, ["uses_ai"]) is True
# Company does not use AI
assert _matches_scope_signals(conditions, []) is False
assert _matches_scope_signals(
conditions, ["third_country_transfer"]
) is False
def test_tkg_telekom_only(self):
"""TKG = only Telekommunikation, all sizes."""
industries = '["Telekommunikation"]'
assert _matches_industry(industries, "Telekommunikation") is True
assert _matches_industry(industries, "Energie") is False