"""Unit tests for ScanProfile validation logic.""" import pytest from iac_reverse.models import ( MAX_RESOURCE_TYPE_FILTERS, PROVIDER_SUPPORTED_RESOURCE_TYPES, ProviderType, ScanProfile, ) class TestScanProfileValidationCredentials: """Tests for credentials validation.""" def test_empty_credentials_returns_error(self): profile = ScanProfile( provider=ProviderType.KUBERNETES, credentials={}, ) errors = profile.validate() assert any("credentials must not be empty" in e for e in errors) def test_non_empty_credentials_passes(self): profile = ScanProfile( provider=ProviderType.KUBERNETES, credentials={"kubeconfig_path": "/home/user/.kube/config"}, ) errors = profile.validate() assert errors == [] class TestScanProfileValidationResourceTypeFilters: """Tests for resource_type_filters count limit.""" def test_none_filters_passes(self): profile = ScanProfile( provider=ProviderType.DOCKER_SWARM, credentials={"host": "localhost"}, resource_type_filters=None, ) errors = profile.validate() assert errors == [] def test_empty_filters_passes(self): profile = ScanProfile( provider=ProviderType.DOCKER_SWARM, credentials={"host": "localhost"}, resource_type_filters=[], ) errors = profile.validate() assert errors == [] def test_filters_at_max_limit_passes(self): # Use valid resource types repeated to reach the limit valid_types = PROVIDER_SUPPORTED_RESOURCE_TYPES[ProviderType.WINDOWS] filters = (valid_types * (MAX_RESOURCE_TYPE_FILTERS // len(valid_types) + 1))[ :MAX_RESOURCE_TYPE_FILTERS ] profile = ScanProfile( provider=ProviderType.WINDOWS, credentials={"host": "win01"}, resource_type_filters=filters, ) errors = profile.validate() assert errors == [] def test_filters_exceeding_max_limit_returns_error(self): # Use valid resource types repeated to exceed the limit valid_types = PROVIDER_SUPPORTED_RESOURCE_TYPES[ProviderType.WINDOWS] filters = (valid_types * (MAX_RESOURCE_TYPE_FILTERS // len(valid_types) + 1))[ : MAX_RESOURCE_TYPE_FILTERS + 1 ] profile = ScanProfile( provider=ProviderType.WINDOWS, credentials={"host": "win01"}, resource_type_filters=filters, ) errors = profile.validate() assert any("at most" in e and "200" in e for e in errors) def test_max_limit_is_200(self): assert MAX_RESOURCE_TYPE_FILTERS == 200 class TestScanProfileValidationResourceTypeSupport: """Tests for resource type validation against provider's supported types.""" def test_valid_resource_types_for_kubernetes(self): profile = ScanProfile( provider=ProviderType.KUBERNETES, credentials={"kubeconfig_path": "/path"}, resource_type_filters=["kubernetes_deployment", "kubernetes_service"], ) errors = profile.validate() assert errors == [] def test_valid_resource_types_for_docker_swarm(self): profile = ScanProfile( provider=ProviderType.DOCKER_SWARM, credentials={"host": "localhost"}, resource_type_filters=["docker_service", "docker_network"], ) errors = profile.validate() assert errors == [] def test_unsupported_resource_type_returns_error(self): profile = ScanProfile( provider=ProviderType.KUBERNETES, credentials={"kubeconfig_path": "/path"}, resource_type_filters=["kubernetes_deployment", "invalid_type"], ) errors = profile.validate() assert any("unsupported resource types" in e for e in errors) assert any("invalid_type" in e for e in errors) def test_cross_provider_resource_type_returns_error(self): """A resource type valid for one provider is invalid for another.""" profile = ScanProfile( provider=ProviderType.KUBERNETES, credentials={"kubeconfig_path": "/path"}, resource_type_filters=["docker_service"], ) errors = profile.validate() assert any("unsupported resource types" in e for e in errors) assert any("docker_service" in e for e in errors) def test_multiple_unsupported_types_listed_in_error(self): profile = ScanProfile( provider=ProviderType.SYNOLOGY, credentials={"host": "nas01"}, resource_type_filters=["fake_type_a", "fake_type_b"], ) errors = profile.validate() assert any("fake_type_a" in e and "fake_type_b" in e for e in errors) @pytest.mark.parametrize("provider", list(ProviderType)) def test_all_providers_have_supported_types(self, provider): """Every provider must have at least one supported resource type.""" assert provider in PROVIDER_SUPPORTED_RESOURCE_TYPES assert len(PROVIDER_SUPPORTED_RESOURCE_TYPES[provider]) > 0 @pytest.mark.parametrize("provider", list(ProviderType)) def test_all_supported_types_pass_validation(self, provider): """All listed supported types for a provider should pass validation.""" supported = PROVIDER_SUPPORTED_RESOURCE_TYPES[provider] profile = ScanProfile( provider=provider, credentials={"key": "value"}, resource_type_filters=supported, ) errors = profile.validate() assert errors == [] class TestScanProfileValidationMultipleErrors: """Tests that all validation errors are returned in a single response.""" def test_empty_credentials_and_too_many_filters(self): filters = ["invalid_type"] * (MAX_RESOURCE_TYPE_FILTERS + 1) profile = ScanProfile( provider=ProviderType.HARVESTER, credentials={}, resource_type_filters=filters, ) errors = profile.validate() # Should have at least: credentials error, too many filters, unsupported types assert len(errors) >= 3 assert any("credentials" in e for e in errors) assert any("at most" in e for e in errors) assert any("unsupported" in e for e in errors) def test_empty_credentials_and_unsupported_types(self): profile = ScanProfile( provider=ProviderType.BARE_METAL, credentials={}, resource_type_filters=["nonexistent_type"], ) errors = profile.validate() assert len(errors) >= 2 assert any("credentials" in e for e in errors) assert any("unsupported" in e for e in errors) def test_no_short_circuit_on_first_error(self): """Validation must not stop at the first error found.""" profile = ScanProfile( provider=ProviderType.WINDOWS, credentials={}, resource_type_filters=["totally_fake_resource"], ) errors = profile.validate() # Both credentials and unsupported type errors should be present assert len(errors) >= 2