"""Coverage for the ``excluded_checks`` / ``excluded_services`` fields added to :class:`prowler.config.schema.base.ProviderConfigBase`. Because the fields live on the base class, every registered provider schema exposes them and every provider must therefore share the same whitespace / uniqueness / non-empty guarantees. These tests lock in that contract at the base level and at the JSON-Schema level (which the UI editor consumes via ``ajv``). """ import pytest from pydantic import ValidationError from prowler.config.scan_config_schema import SCAN_CONFIG_SCHEMA from prowler.config.schema.aws import AWSProviderConfig from prowler.config.schema.registry import SCHEMAS from prowler.config.schema.validator import validate_provider_config EXCLUSION_FIELDS = ("excluded_checks", "excluded_services") class Test_JSON_Schema_Exposes_Exclusion_Fields: @pytest.mark.parametrize("provider", sorted(SCHEMAS)) @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_field_shape(self, provider, field): field_schema = SCAN_CONFIG_SCHEMA["properties"][provider]["properties"][field] assert field_schema["type"] == "array" assert field_schema["items"] == {"type": "string", "minLength": 1} assert field_schema["uniqueItems"] is True assert field_schema["default"] == [] class Test_Exclusion_Field_Validation: def _model(self, **kwargs): return AWSProviderConfig.model_validate(kwargs) @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_empty_string_is_rejected(self, field): with pytest.raises(ValidationError): self._model(**{field: [""]}) @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_whitespace_only_string_is_rejected(self, field): with pytest.raises(ValidationError): self._model(**{field: [" "]}) @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_raw_duplicates_are_rejected(self, field): with pytest.raises(ValidationError): self._model(**{field: ["s3", "s3"]}) @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_normalized_duplicates_are_rejected(self, field): # After whitespace normalization ``" s3 "`` collapses to ``"s3"`` # and must be caught by the duplicate check. with pytest.raises(ValidationError): self._model(**{field: ["s3", " s3 "]}) @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_whitespace_is_stripped(self, field): model = self._model(**{field: [" identifier "]}) assert getattr(model, field) == ["identifier"] @pytest.mark.parametrize("field", EXCLUSION_FIELDS) def test_non_string_item_is_rejected(self, field): with pytest.raises(ValidationError): self._model(**{field: [123]}) class Test_Exclusion_Defaults_Are_Not_Injected: """The strict normalization path uses ``model_dump(exclude_unset=True)`` so pre-existing configs that never set an exclusion field must round-trip without the default empty list being materialized.""" def test_absent_fields_are_not_injected_by_validator(self): # The lenient SDK runtime path also uses ``exclude_unset=True`` under # the hood; asserting on the validator output guards the promise # against future refactors. assert validate_provider_config("aws", {}, SCHEMAS["aws"]) == {} def test_absent_fields_are_not_injected_when_other_keys_are_present(self): assert validate_provider_config( "aws", {"max_ec2_instance_age_in_days": 180}, SCHEMAS["aws"], ) == {"max_ec2_instance_age_in_days": 180} def test_explicit_empty_list_round_trips(self): # Explicitly setting ``excluded_checks: []`` is different from # omitting it — the empty list is user-provided and must be # preserved by the strict-normalization contract. assert validate_provider_config( "aws", {"excluded_checks": []}, SCHEMAS["aws"], ) == {"excluded_checks": []} class Test_Extra_Fields_Are_Preserved: """``extra="allow"`` must keep plugin-provided keys around so the ecosystem contract in ``validator_test.py`` still holds after adding the exclusion fields.""" def test_unknown_keys_are_preserved_alongside_exclusions(self): out = validate_provider_config( "aws", {"excluded_checks": ["s3_bucket_public_access"], "plugin_option": "kept"}, SCHEMAS["aws"], ) assert out == { "excluded_checks": ["s3_bucket_public_access"], "plugin_option": "kept", }