mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
* 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.
315 lines
13 KiB
Python
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
|