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
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:
229
control-pipeline/tests/test_applicability_engine.py
Normal file
229
control-pipeline/tests/test_applicability_engine.py
Normal 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
|
||||
Reference in New Issue
Block a user