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>
230 lines
8.1 KiB
Python
230 lines
8.1 KiB
Python
"""
|
|
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
|