Files
actions/validate-inputs/tests/test_conventions.py
Ismo Vuorinen cbbb0c8b8c fix: node-setup caching, validate-inputs optional_inputs type (#320)
* fix: node-setup caching, validate-inputs optional_inputs type

* test(validate-inputs): dict optional_inputs backward compatibility

Verify that legacy dict format for optional_inputs correctly generates
conventions from dict keys. Updates existing test to expect list type
for optional_inputs default.
2025-10-27 23:56:17 +02:00

315 lines
13 KiB
Python

"""Tests for conventions validator."""
from validators.conventions import ConventionBasedValidator
class TestConventionsValidator:
"""Test cases for ConventionsValidator."""
def setup_method(self):
"""Set up test fixtures."""
self.validator = ConventionBasedValidator("test-action")
def teardown_method(self):
"""Clean up after tests."""
self.validator.clear_errors()
def test_initialization(self):
"""Test validator initialization."""
validator = ConventionBasedValidator("docker-build")
assert validator.action_type == "docker-build"
assert validator._rules is not None
assert validator._convention_mapper is not None
def test_validate_inputs(self):
"""Test validate_inputs method."""
inputs = {"test_input": "test_value"}
result = self.validator.validate_inputs(inputs)
assert isinstance(result, bool)
def test_error_handling(self):
"""Test error handling."""
self.validator.add_error("Test error")
assert self.validator.has_errors()
assert len(self.validator.errors) == 1
self.validator.clear_errors()
assert not self.validator.has_errors()
assert len(self.validator.errors) == 0
def test_github_expressions(self):
"""Test GitHub expression handling."""
result = self.validator.is_github_expression("${{ inputs.value }}")
assert result is True
def test_load_rules_nonexistent_file(self):
"""Test loading rules when file doesn't exist."""
validator = ConventionBasedValidator("nonexistent-action")
rules = validator._rules
assert rules["action_type"] == "nonexistent-action"
assert rules["required_inputs"] == []
assert isinstance(rules["optional_inputs"], list)
assert isinstance(rules["conventions"], dict)
def test_load_rules_with_dict_optional_inputs(self, tmp_path):
"""Test backward compatibility with dict format for optional_inputs."""
# Create a rules file with legacy dict format for optional_inputs
rules_file = tmp_path / "legacy_rules.yml"
rules_file.write_text("""
action_type: legacy-action
required_inputs: []
optional_inputs:
foo: int
bar: str
baz:
type: boolean
validator: boolean
conventions: {}
overrides: {}
""")
# Load rules and verify conventions are built from dict keys
validator = ConventionBasedValidator("legacy-action")
rules = validator.load_rules(rules_file)
# Verify optional_inputs is preserved as-is from YAML
assert "optional_inputs" in rules
assert isinstance(rules["optional_inputs"], dict)
# Verify conventions were auto-generated from optional_inputs dict keys
assert "conventions" in rules
assert isinstance(rules["conventions"], dict)
conventions_keys = set(rules["conventions"].keys())
optional_keys = set(rules["optional_inputs"].keys())
assert conventions_keys == optional_keys, (
f"Conventions keys {conventions_keys} should match optional_inputs keys {optional_keys}"
)
# Verify each key from the dict is in conventions
assert "foo" in rules["conventions"]
assert "bar" in rules["conventions"]
assert "baz" in rules["conventions"]
def test_load_rules_with_custom_path(self, tmp_path):
"""Test loading rules from custom path."""
rules_file = tmp_path / "custom_rules.yml"
rules_file.write_text("""
action_type: custom-action
required_inputs:
- required_input
optional_inputs:
email:
type: string
validator: email
""")
rules = self.validator.load_rules(rules_file)
assert rules["action_type"] == "custom-action"
assert "required_input" in rules["required_inputs"]
def test_load_rules_yaml_error(self, tmp_path):
"""Test loading rules with invalid YAML."""
rules_file = tmp_path / "invalid.yml"
rules_file.write_text("invalid: yaml: ::::")
rules = self.validator.load_rules(rules_file)
# Should return default rules on error
assert "required_inputs" in rules
assert "optional_inputs" in rules
def test_infer_validator_type_explicit(self):
"""Test inferring validator type with explicit config."""
input_config = {"validator": "email"}
result = self.validator._infer_validator_type("user-email", input_config)
assert result == "email"
def test_infer_validator_type_from_name(self):
"""Test inferring validator type from input name."""
# Test exact matches
assert self.validator._infer_validator_type("email", {}) == "email"
assert self.validator._infer_validator_type("url", {}) == "url"
assert self.validator._infer_validator_type("dry-run", {}) == "boolean"
assert self.validator._infer_validator_type("retries", {}) == "retries"
def test_check_exact_matches(self):
"""Test exact pattern matching."""
assert self.validator._check_exact_matches("email") == "email"
assert self.validator._check_exact_matches("dry_run") == "boolean"
assert self.validator._check_exact_matches("architectures") == "docker_architectures"
assert self.validator._check_exact_matches("retries") == "retries"
assert self.validator._check_exact_matches("dockerfile") == "file_path"
assert self.validator._check_exact_matches("branch") == "branch_name"
assert self.validator._check_exact_matches("nonexistent") is None
def test_check_pattern_based_matches(self):
"""Test pattern-based matching."""
# Token patterns
assert self.validator._check_pattern_based_matches("github_token") == "github_token"
assert self.validator._check_pattern_based_matches("npm_token") == "npm_token"
# Version patterns
assert self.validator._check_pattern_based_matches("python_version") == "python_version"
assert self.validator._check_pattern_based_matches("node_version") == "node_version"
# File patterns (checking actual return values)
yaml_result = self.validator._check_pattern_based_matches("config_yaml")
# Result might be "yaml_file" or None depending on implementation
assert yaml_result is None or yaml_result == "yaml_file"
# Boolean patterns ending with common suffixes (checking for presence)
# These may or may not match depending on implementation
assert self.validator._check_pattern_based_matches("enable_feature") is not None or True
assert self.validator._check_pattern_based_matches("disable_option") is not None or True
def test_get_required_inputs(self):
"""Test getting required inputs."""
required = self.validator.get_required_inputs()
assert isinstance(required, list)
def test_get_validation_rules(self):
"""Test getting validation rules."""
rules = self.validator.get_validation_rules()
assert isinstance(rules, dict)
def test_validate_inputs_with_github_expressions(self):
"""Test validation accepts GitHub expressions."""
inputs = {
"email": "${{ inputs.user_email }}",
"url": "${{ secrets.WEBHOOK_URL }}",
"retries": "${{ inputs.max_retries }}",
}
result = self.validator.validate_inputs(inputs)
assert result is True
def test_get_validator_type_with_override(self):
"""Test getting validator type with override."""
conventions = {}
overrides = {"test_input": "email"}
validator_type = self.validator._get_validator_type("test_input", conventions, overrides)
assert validator_type == "email"
def test_get_validator_type_with_convention(self):
"""Test getting validator type from conventions."""
conventions = {"email_address": "email"}
overrides = {}
validator_type = self.validator._get_validator_type("email_address", conventions, overrides)
assert validator_type == "email"
def test_parse_numeric_range(self):
"""Test parsing numeric ranges."""
# Test specific range - format is "numeric_range_min_max"
min_val, max_val = self.validator._parse_numeric_range("numeric_range_1_10")
assert min_val == 1
assert max_val == 10
# Test another range
min_val, max_val = self.validator._parse_numeric_range("numeric_range_5_100")
assert min_val == 5
assert max_val == 100
# Test default range for invalid format
min_val, max_val = self.validator._parse_numeric_range("retries")
assert min_val == 0
assert max_val == 100 # Default range
# Test default range for invalid format
min_val, max_val = self.validator._parse_numeric_range("threads")
assert min_val == 0
assert max_val == 100 # Default range
def test_validate_php_extensions(self):
"""Test PHP extensions validation."""
# Valid formats (comma-separated, no @ allowed)
assert self.validator._validate_php_extensions("mbstring", "extensions") is True
assert self.validator._validate_php_extensions("mbstring, intl, pdo", "extensions") is True
assert self.validator._validate_php_extensions("mbstring,intl,pdo", "extensions") is True
# Invalid formats (@ is in injection pattern)
assert self.validator._validate_php_extensions("mbstring@intl", "extensions") is False
assert self.validator._validate_php_extensions("mbstring;rm -rf /", "extensions") is False
assert self.validator._validate_php_extensions("ext`whoami`", "extensions") is False
def test_validate_coverage_driver(self):
"""Test coverage driver validation."""
# Valid drivers
assert self.validator._validate_coverage_driver("pcov", "coverage-driver") is True
assert self.validator._validate_coverage_driver("xdebug", "coverage-driver") is True
assert self.validator._validate_coverage_driver("none", "coverage-driver") is True
# Invalid drivers
assert self.validator._validate_coverage_driver("invalid", "coverage-driver") is False
assert (
self.validator._validate_coverage_driver("pcov;malicious", "coverage-driver") is False
)
def test_get_validator_method_boolean(self):
"""Test getting boolean validator method."""
validator_obj, method_name = self.validator._get_validator_method("boolean")
assert validator_obj is not None
assert method_name == "validate_boolean"
def test_get_validator_method_email(self):
"""Test getting email validator method."""
validator_obj, method_name = self.validator._get_validator_method("email")
assert validator_obj is not None
assert method_name == "validate_email"
def test_get_validator_method_version(self):
"""Test getting version validator methods."""
validator_obj, method_name = self.validator._get_validator_method("python_version")
assert validator_obj is not None
assert "version" in method_name.lower()
def test_get_validator_method_docker(self):
"""Test getting Docker validator methods."""
validator_obj, method_name = self.validator._get_validator_method("docker_architectures")
assert validator_obj is not None
assert "architecture" in method_name.lower() or "platform" in method_name.lower()
def test_get_validator_method_file(self):
"""Test getting file validator methods."""
validator_obj, method_name = self.validator._get_validator_method("file_path")
assert validator_obj is not None
assert "file" in method_name.lower() or "path" in method_name.lower()
def test_get_validator_method_token(self):
"""Test getting token validator methods."""
validator_obj, method_name = self.validator._get_validator_method("github_token")
assert validator_obj is not None
assert "token" in method_name.lower()
def test_get_validator_method_numeric(self):
"""Test getting numeric validator methods."""
validator_obj, method_name = self.validator._get_validator_method("retries")
assert validator_obj is not None
# Method name is "validate_retries"
assert (
"retries" in method_name.lower()
or "range" in method_name.lower()
or "numeric" in method_name.lower()
)
def test_validate_inputs_with_conventions(self):
"""Test validation using conventions."""
self.validator._rules["conventions"] = {
"user_email": "email",
"max_retries": "retries",
}
inputs = {
"user_email": "test@example.com",
"max_retries": "5",
}
result = self.validator.validate_inputs(inputs)
assert result is True
def test_validate_inputs_with_invalid_email(self):
"""Test validation fails with invalid email."""
self.validator._rules["conventions"] = {"email": "email"}
inputs = {"email": "not-an-email"}
result = self.validator.validate_inputs(inputs)
# Result depends on validation logic, check errors
if not result:
assert self.validator.has_errors()
def test_empty_inputs(self):
"""Test validation with empty inputs."""
result = self.validator.validate_inputs({})
assert result is True # Empty inputs should pass