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