194 lines
7.1 KiB
Python
194 lines
7.1 KiB
Python
"""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
|