Files
actions/validate-inputs/tests/test_conventions.py
Ismo Vuorinen 96c305c557 refactor: centralize validation logic with validate_with helper (#412)
* chore: sonarcloud fixes

* chore: coderabbit cr fixes
2025-12-23 13:29:37 +02:00

1375 lines
55 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 (pattern mismatch and injection)
assert (
self.validator._validate_php_extensions("mbstring@intl", "extensions") is False
) # @ not in pattern
assert (
self.validator._validate_php_extensions("mbstring;rm -rf /", "extensions") is False
) # injection
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
def test_validate_mode_enum_valid(self):
"""Test mode enum validation with valid values."""
valid_modes = [
"check",
"fix",
"", # Empty is optional
]
for mode in valid_modes:
self.validator.clear_errors()
result = self.validator._validate_mode_enum(mode, "mode")
assert result is True, f"Should accept mode: {mode}"
def test_validate_mode_enum_invalid(self):
"""Test mode enum validation with invalid values."""
invalid_modes = [
"lint", # Wrong value
"validate", # Wrong value
"CHECK", # Uppercase
"Fix", # Mixed case
"check,fix", # Comma-separated not allowed
"auto", # Wrong value
"both", # Wrong value
]
for mode in invalid_modes:
self.validator.clear_errors()
result = self.validator._validate_mode_enum(mode, "mode")
assert result is False, f"Should reject mode: {mode}"
assert self.validator.has_errors()
def test_validate_report_format_valid(self):
"""Test report format validation with valid values."""
valid_formats = [
"checkstyle",
"colored-line-number",
"compact",
"github-actions",
"html",
"json",
"junit",
"junit-xml",
"line-number",
"sarif",
"stylish",
"tab",
"teamcity",
"xml",
"", # Empty is optional
]
for fmt in valid_formats:
self.validator.clear_errors()
result = self.validator._validate_report_format(fmt, "report-format")
assert result is True, f"Should accept format: {fmt}"
def test_validate_report_format_invalid(self):
"""Test report format validation with invalid values."""
invalid_formats = [
"text", # Wrong value
"csv", # Wrong value
"markdown", # Wrong value
"SARIF", # Uppercase
"Json", # Mixed case
"json,sarif", # Comma-separated not allowed
"pdf", # Wrong value
]
for fmt in invalid_formats:
self.validator.clear_errors()
result = self.validator._validate_report_format(fmt, "report-format")
assert result is False, f"Should reject format: {fmt}"
assert self.validator.has_errors()
def test_validate_linter_list_valid(self):
"""Test linter list validation with valid values."""
valid_lists = [
"gosec",
"govet",
"staticcheck",
"gosec,govet,staticcheck",
"eslint,prettier,typescript-eslint",
"my_linter",
"my-linter",
"linter123",
"a,b,c",
"", # Empty is optional
]
for linter_list in valid_lists:
self.validator.clear_errors()
result = self.validator._validate_linter_list(linter_list, "enable-linters")
assert result is True, f"Should accept linter list: {linter_list}"
def test_validate_linter_list_invalid(self):
"""Test linter list validation with invalid values."""
invalid_lists = [
"linter;rm -rf /", # Dangerous characters
"linter1,,linter2", # Double comma
",linter", # Leading comma
"linter,", # Trailing comma
"linter one", # Space
"linter@test", # @ not allowed
"linter$name", # $ not allowed
]
for linter_list in invalid_lists:
self.validator.clear_errors()
result = self.validator._validate_linter_list(linter_list, "enable-linters")
assert result is False, f"Should reject linter list: {linter_list}"
assert self.validator.has_errors()
def test_validate_timeout_with_unit_valid(self):
"""Test timeout with unit validation with valid values."""
valid_timeouts = [
"5m",
"30s",
"1h",
"500ms",
"100ns",
"1000us",
"1000µs",
"2h",
"90s",
"15m",
"", # Empty is optional
]
for timeout in valid_timeouts:
self.validator.clear_errors()
result = self.validator._validate_timeout_with_unit(timeout, "timeout")
assert result is True, f"Should accept timeout: {timeout}"
def test_validate_timeout_with_unit_invalid(self):
"""Test timeout with unit validation with invalid values."""
invalid_timeouts = [
"5", # Missing unit
"m", # Missing number
"5minutes", # Wrong unit
"5M", # Uppercase unit
"5 m", # Space
"-5m", # Negative not allowed
"5.5m", # Decimal not allowed
"5sec", # Wrong unit
"5min", # Wrong unit
]
for timeout in invalid_timeouts:
self.validator.clear_errors()
result = self.validator._validate_timeout_with_unit(timeout, "timeout")
assert result is False, f"Should reject timeout: {timeout}"
assert self.validator.has_errors()
def test_validate_severity_enum_valid(self):
"""Test severity enum validation with valid values."""
valid_severities = [
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
"UNKNOWN",
"CRITICAL,HIGH",
"CRITICAL,HIGH,MEDIUM",
"LOW,MEDIUM,HIGH,CRITICAL",
"UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
"", # Empty is optional
]
for severity in valid_severities:
self.validator.clear_errors()
result = self.validator._validate_severity_enum(severity, "severity")
assert result is True, f"Should accept severity: {severity}"
def test_validate_severity_enum_invalid(self):
"""Test severity enum validation with invalid values."""
invalid_severities = [
"INVALID", # Wrong value
"critical", # Lowercase not allowed
"Critical", # Mixed case
"CRITICAL,INVALID", # One invalid
"CRITICAL,,HIGH", # Double comma (empty severity)
",CRITICAL", # Leading comma (empty severity)
"CRITICAL,", # Trailing comma (empty severity)
"CRIT", # Wrong abbreviation
"HI", # Wrong abbreviation
]
for severity in invalid_severities:
self.validator.clear_errors()
result = self.validator._validate_severity_enum(severity, "severity")
assert result is False, f"Should reject severity: {severity}"
assert self.validator.has_errors()
def test_validate_severity_enum_with_spaces(self):
"""Test that spaces after commas are handled correctly."""
# These should be valid - spaces are stripped
valid_with_spaces = [
"CRITICAL, HIGH",
"CRITICAL , HIGH",
"CRITICAL, HIGH",
"LOW, MEDIUM, HIGH",
]
for severity in valid_with_spaces:
self.validator.clear_errors()
result = self.validator._validate_severity_enum(severity, "severity")
assert result is True, f"Should accept severity with spaces: {severity}"
def test_validate_comma_separated_list_pattern_based(self):
"""Test comma-separated list validator with pattern-based validation."""
# Valid pattern-based lists
valid_lists = [
"item1",
"item1,item2",
"item-1,item_2,item3",
"", # Empty is optional
]
for value in valid_lists:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value, "test-input", item_pattern=r"^[a-zA-Z0-9_-]+$", item_name="item"
)
assert result is True, f"Should accept pattern-based list: {value}"
# Invalid pattern-based lists
invalid_lists = [
"item1,,item2", # Double comma (empty item)
",item1", # Leading comma
"item1,", # Trailing comma
"item 1", # Space in item
"item@1", # Invalid character
]
for value in invalid_lists:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value, "test-input", item_pattern=r"^[a-zA-Z0-9_-]+$", item_name="item"
)
assert result is False, f"Should reject pattern-based list: {value}"
assert self.validator.has_errors()
def test_validate_comma_separated_list_enum_based(self):
"""Test comma-separated list validator with enum-based validation."""
valid_items = ["vuln", "config", "secret", "license"]
# Valid enum-based lists
valid_lists = [
"vuln",
"vuln,config",
"vuln,config,secret,license",
"license,config",
"", # Empty is optional
]
for value in valid_lists:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value, "scanners", valid_items=valid_items, item_name="scanner"
)
assert result is True, f"Should accept enum-based list: {value}"
# Invalid enum-based lists
invalid_lists = [
"invalid", # Not in enum
"vuln,invalid", # One invalid item
"vuln,,config", # Double comma
",vuln", # Leading comma
"config,", # Trailing comma
]
for value in invalid_lists:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value, "scanners", valid_items=valid_items, item_name="scanner"
)
assert result is False, f"Should reject enum-based list: {value}"
assert self.validator.has_errors()
def test_validate_comma_separated_list_injection_check(self):
"""Test comma-separated list validator with injection checking."""
# Valid values (no injection) - using relaxed pattern that allows @#
valid_values = [
"item1,item2",
"safe_value",
"item@host", # @ is not a shell injection vector
"item#comment", # # is not a shell injection vector
"", # Empty
]
for value in valid_values:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value,
"test-input",
item_pattern=r"^[a-zA-Z0-9_@#-]+$", # Explicit pattern allowing @#
check_injection=True,
item_name="item",
)
assert result is True, f"Should accept safe value: {value}"
# Invalid values (shell injection patterns)
injection_values = [
"item;ls", # Semicolon
"item&whoami", # Ampersand
"item|cat", # Pipe
"item`date`", # Backtick
"item$(echo)", # Command substitution
]
for value in injection_values:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value,
"test-input",
item_pattern=r"^[a-zA-Z0-9_@#-]+$", # Same pattern for consistency
check_injection=True,
item_name="item",
)
assert result is False, f"Should reject injection pattern: {value}"
assert self.validator.has_errors()
assert "injection" in self.validator.errors[0].lower()
def test_validate_comma_separated_list_with_spaces(self):
"""Test that comma-separated list handles spaces correctly."""
# Spaces should be stripped
valid_with_spaces = [
"item1, item2",
"item1 , item2",
"item1, item2",
"item1 , item2 ,item3",
]
for value in valid_with_spaces:
self.validator.clear_errors()
result = self.validator._validate_comma_separated_list(
value, "test-input", item_pattern=r"^[a-zA-Z0-9]+$", item_name="item"
)
assert result is True, f"Should accept list with spaces: {value}"
def test_validate_scanner_list_valid(self):
"""Test scanner list validation with valid values."""
valid_scanners = [
"vuln",
"config",
"secret",
"license",
"vuln,config",
"vuln,config,secret",
"vuln,config,secret,license",
"license,secret,config,vuln", # Order doesn't matter
"", # Empty is optional
]
for scanners in valid_scanners:
self.validator.clear_errors()
result = self.validator._validate_scanner_list(scanners, "trivy-scanners")
assert result is True, f"Should accept scanner list: {scanners}"
def test_validate_scanner_list_invalid(self):
"""Test scanner list validation with invalid values."""
invalid_scanners = [
"invalid", # Not a valid scanner
"vuln,invalid", # One invalid
"vuln,,config", # Double comma
",vuln", # Leading comma
"config,", # Trailing comma
"VULN", # Wrong case
"vulnerability", # Wrong name
]
for scanners in invalid_scanners:
self.validator.clear_errors()
result = self.validator._validate_scanner_list(scanners, "trivy-scanners")
assert result is False, f"Should reject scanner list: {scanners}"
assert self.validator.has_errors()
def test_validate_binary_enum_valid(self):
"""Test binary enum validation with valid values."""
# Test default check/fix values
valid_values = ["check", "fix", ""]
for value in valid_values:
self.validator.clear_errors()
result = self.validator._validate_binary_enum(value, "mode")
assert result is True, f"Should accept binary enum: {value}"
# Test custom binary enum
valid_custom = ["enabled", "disabled", ""]
for value in valid_custom:
self.validator.clear_errors()
result = self.validator._validate_binary_enum(
value, "status", valid_values=["enabled", "disabled"]
)
assert result is True, f"Should accept custom binary enum: {value}"
def test_validate_binary_enum_invalid(self):
"""Test binary enum validation with invalid values."""
# Test invalid values for default check/fix
invalid_values = ["invalid", "CHECK", "Fix", "checking", "fixed"]
for value in invalid_values:
self.validator.clear_errors()
result = self.validator._validate_binary_enum(value, "mode")
assert result is False, f"Should reject binary enum: {value}"
assert self.validator.has_errors()
# Test case-sensitive validation
case_sensitive_invalid = ["CHECK", "FIX", "Check"]
for value in case_sensitive_invalid:
self.validator.clear_errors()
result = self.validator._validate_binary_enum(value, "mode", case_sensitive=True)
assert result is False, f"Should reject case-sensitive: {value}"
assert self.validator.has_errors()
def test_validate_binary_enum_case_insensitive(self):
"""Test binary enum with case-insensitive validation."""
# Test case-insensitive validation
case_variations = ["check", "CHECK", "Check", "fix", "FIX", "Fix"]
for value in case_variations:
self.validator.clear_errors()
result = self.validator._validate_binary_enum(value, "mode", case_sensitive=False)
assert result is True, f"Should accept case-insensitive: {value}"
def test_validate_binary_enum_wrong_count(self):
"""Test binary enum with wrong number of values."""
# Should raise ValueError if not exactly 2 values
try:
self.validator._validate_binary_enum("test", "input", valid_values=["only_one"])
raise AssertionError("Should raise ValueError for single value")
except ValueError as e:
assert "exactly 2 valid values" in str(e)
try:
self.validator._validate_binary_enum(
"test", "input", valid_values=["one", "two", "three"]
)
raise AssertionError("Should raise ValueError for three values")
except ValueError as e:
assert "exactly 2 valid values" in str(e)
def test_validate_format_enum_valid(self):
"""Test format enum validation with valid values."""
# Test default comprehensive format list
valid_formats = [
"json",
"sarif",
"checkstyle",
"github-actions",
"html",
"xml",
"junit-xml",
"stylish",
"", # Empty is optional
]
for fmt in valid_formats:
self.validator.clear_errors()
result = self.validator._validate_format_enum(fmt, "format")
assert result is True, f"Should accept format: {fmt}"
# Test custom format list
custom_formats = ["json", "sarif", "text"]
valid_custom = ["json", "sarif", ""]
for fmt in valid_custom:
self.validator.clear_errors()
result = self.validator._validate_format_enum(
fmt, "output-format", valid_formats=custom_formats
)
assert result is True, f"Should accept custom format: {fmt}"
def test_validate_format_enum_invalid(self):
"""Test format enum validation with invalid values."""
# Test invalid formats for default list
invalid_formats = ["invalid", "txt", "pdf", "markdown", "yaml"]
for fmt in invalid_formats:
self.validator.clear_errors()
result = self.validator._validate_format_enum(fmt, "format")
assert result is False, f"Should reject format: {fmt}"
assert self.validator.has_errors()
# Test format not in custom list
custom_formats = ["json", "sarif"]
invalid_custom = ["xml", "html", "text"]
for fmt in invalid_custom:
self.validator.clear_errors()
result = self.validator._validate_format_enum(
fmt, "output-format", valid_formats=custom_formats
)
assert result is False, f"Should reject custom format: {fmt}"
assert self.validator.has_errors()
def test_validate_format_enum_allow_custom(self):
"""Test format enum with allow_custom flag."""
# Test that allow_custom=True accepts any format
any_formats = ["json", "custom-format", "my-tool-format", ""]
for fmt in any_formats:
self.validator.clear_errors()
result = self.validator._validate_format_enum(fmt, "format", allow_custom=True)
assert result is True, f"Should accept any format with allow_custom: {fmt}"
# Test that known formats still work with custom list
known_formats = ["json", "sarif", "xml"]
for fmt in known_formats:
self.validator.clear_errors()
result = self.validator._validate_format_enum(
fmt,
"format",
valid_formats=["json", "sarif"],
allow_custom=True,
)
assert result is True, f"Should accept format with allow_custom: {fmt}"
def test_validate_multi_value_enum_valid(self):
"""Test multi-value enum validation with valid values."""
# Test 3-value enum
valid_values_3 = ["check", "fix", ""]
for value in valid_values_3:
self.validator.clear_errors()
result = self.validator._validate_multi_value_enum(
value, "mode", valid_values=["check", "fix", "both"]
)
assert result is True, f"Should accept 3-value enum: {value}"
# Test 4-value enum
valid_values_4 = ["php", "python", "go", "dotnet", ""]
for value in valid_values_4:
self.validator.clear_errors()
result = self.validator._validate_multi_value_enum(
value, "language", valid_values=["php", "python", "go", "dotnet"]
)
assert result is True, f"Should accept 4-value enum: {value}"
def test_validate_multi_value_enum_invalid(self):
"""Test multi-value enum validation with invalid values."""
# Test invalid values for 3-value enum
invalid_values = ["invalid", "CHECK", "Fix"]
for value in invalid_values:
self.validator.clear_errors()
result = self.validator._validate_multi_value_enum(
value, "mode", valid_values=["check", "fix", "both"]
)
assert result is False, f"Should reject multi-value enum: {value}"
assert self.validator.has_errors()
def test_validate_multi_value_enum_case_insensitive(self):
"""Test multi-value enum with case-insensitive validation."""
# Test case variations
case_variations = ["check", "CHECK", "Check", "fix", "FIX", "both", "BOTH"]
for value in case_variations:
self.validator.clear_errors()
result = self.validator._validate_multi_value_enum(
value,
"mode",
valid_values=["check", "fix", "both"],
case_sensitive=False,
)
assert result is True, f"Should accept case-insensitive: {value}"
def test_validate_multi_value_enum_wrong_count(self):
"""Test multi-value enum with wrong number of values."""
# Should raise ValueError if less than min_values
try:
self.validator._validate_multi_value_enum("test", "input", valid_values=["only_one"])
raise AssertionError("Should raise ValueError for single value")
except ValueError as e:
assert ">= 2 values" in str(e)
# Should raise ValueError if more than max_values
try:
self.validator._validate_multi_value_enum(
"test",
"input",
valid_values=["v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10", "v11"],
)
raise AssertionError("Should raise ValueError for 11 values")
except ValueError as e:
assert "<= 10 values" in str(e)
def test_validate_exit_code_list_valid(self):
"""Test exit code list validation with valid values."""
valid_codes = [
"0",
"1",
"255",
"0,1,2",
"5,10,15",
"0,130",
"0,1,2,5,10",
"", # Empty is optional
]
for codes in valid_codes:
self.validator.clear_errors()
result = self.validator._validate_exit_code_list(codes, "success-codes")
assert result is True, f"Should accept exit codes: {codes}"
def test_validate_exit_code_list_invalid(self):
"""Test exit code list validation with invalid values."""
invalid_codes = [
"256", # Out of range
"0,256", # One out of range
"-1", # Negative
"0,-1", # One negative
"abc", # Non-numeric
"0,abc", # One non-numeric
"0,,1", # Double comma (empty)
",0", # Leading comma
"0,", # Trailing comma
"999", # Way out of range
]
for codes in invalid_codes:
self.validator.clear_errors()
result = self.validator._validate_exit_code_list(codes, "success-codes")
assert result is False, f"Should reject exit codes: {codes}"
assert self.validator.has_errors()
def test_validate_exit_code_list_edge_cases(self):
"""Test exit code list with edge cases."""
# Test boundary values
self.validator.clear_errors()
result = self.validator._validate_exit_code_list("0,255", "codes")
assert result is True, "Should accept boundary values 0 and 255"
# Test with spaces (should be stripped)
self.validator.clear_errors()
result = self.validator._validate_exit_code_list("0, 1, 2", "codes")
assert result is True, "Should accept codes with spaces"
# Phase 2B: High-value validators
def test_validate_key_value_list_valid(self):
"""Test valid key-value lists."""
# Single key-value pair
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=value", "build-args")
assert result is True, "Should accept single key-value pair"
# Multiple key-value pairs
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY1=value1,KEY2=value2", "build-args")
assert result is True, "Should accept multiple key-value pairs"
# Empty value (valid for some use cases)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=", "build-args")
assert result is True, "Should accept empty value"
# Value containing equals sign
self.validator.clear_errors()
result = self.validator._validate_key_value_list(
"CONNECTION_STRING=host=localhost;port=5432", "env-vars"
)
assert result is False, "Should reject value with semicolon (injection risk)"
# Underscores and hyphens in keys
self.validator.clear_errors()
result = self.validator._validate_key_value_list(
"BUILD_ARG=test,my-key=value", "build-args"
)
assert result is True, "Should accept underscores and hyphens in keys"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("", "build-args")
assert result is True, "Should accept empty string"
def test_validate_key_value_list_invalid(self):
"""Test invalid key-value lists."""
# Missing equals sign
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY", "build-args")
assert result is False, "Should reject missing equals sign"
assert any("Expected format: KEY=VALUE" in err for err in self.validator.errors), (
"Should have format error message"
)
# Empty key
self.validator.clear_errors()
result = self.validator._validate_key_value_list("=value", "build-args")
assert result is False, "Should reject empty key"
assert any("Key cannot be empty" in err for err in self.validator.errors), (
"Should have empty key error"
)
# Empty pair after comma
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=value,", "build-args")
assert result is False, "Should reject trailing comma"
# Invalid characters in key
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY@=value", "build-args")
assert result is False, "Should reject invalid characters in key"
def test_validate_key_value_list_injection(self):
"""Test security checks for key-value lists."""
# Semicolon (command separator)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=value;whoami", "build-args")
assert result is False, "Should reject semicolon"
assert any("Potential injection" in err for err in self.validator.errors), (
"Should have injection error"
)
# Pipe (command chaining)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=value|ls", "build-args")
assert result is False, "Should reject pipe"
# Backticks (command substitution)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=`whoami`", "build-args")
assert result is False, "Should reject backticks"
# Dollar sign (variable expansion)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=$PATH", "build-args")
assert result is False, "Should reject dollar sign"
# Parentheses (subshell)
self.validator.clear_errors()
result = self.validator._validate_key_value_list("KEY=(echo test)", "build-args")
assert result is False, "Should reject parentheses"
def test_validate_path_list_valid(self):
"""Test valid path lists."""
# Single file path
self.validator.clear_errors()
result = self.validator._validate_path_list("src/index.js", "paths")
assert result is True, "Should accept single file path"
# Multiple paths
self.validator.clear_errors()
result = self.validator._validate_path_list("src/,dist/,build/", "paths")
assert result is True, "Should accept multiple paths"
# Glob patterns
self.validator.clear_errors()
result = self.validator._validate_path_list("src/**/*.js", "file-pattern")
assert result is True, "Should accept glob patterns"
# Multiple glob patterns
self.validator.clear_errors()
result = self.validator._validate_path_list(
"*.js,src/**/*.ts,test/[ab].spec.js", "file-pattern"
)
assert result is True, "Should accept multiple glob patterns"
# Absolute paths
self.validator.clear_errors()
result = self.validator._validate_path_list("/usr/local/bin", "paths")
assert result is True, "Should accept absolute paths"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_path_list("", "paths")
assert result is True, "Should accept empty string"
# Paths with special chars (@, ~, +)
self.validator.clear_errors()
result = self.validator._validate_path_list(
"@scope/package,~/config,node_modules/+utils", "paths"
)
assert result is True, "Should accept @, ~, + in paths"
def test_validate_path_list_invalid(self):
"""Test invalid path lists."""
# Empty path after comma
self.validator.clear_errors()
result = self.validator._validate_path_list("src/,", "paths")
assert result is False, "Should reject trailing comma"
assert any("Contains empty path" in err for err in self.validator.errors), (
"Should have empty path error"
)
# Invalid characters (when glob disabled)
self.validator.clear_errors()
result = self.validator._validate_path_list("src/*.js", "paths", allow_glob=False)
assert result is False, "Should reject glob when disabled"
def test_validate_path_list_security(self):
"""Test security checks for path lists."""
# Path traversal with ../
self.validator.clear_errors()
result = self.validator._validate_path_list("../etc/passwd", "paths")
assert result is False, "Should reject ../ path traversal"
assert any("Path traversal detected" in err for err in self.validator.errors), (
"Should have path traversal error"
)
# Path traversal in middle
self.validator.clear_errors()
result = self.validator._validate_path_list("src/../etc/passwd", "paths")
assert result is False, "Should reject path traversal in middle"
# Path ending with /..
self.validator.clear_errors()
result = self.validator._validate_path_list("src/..", "paths")
assert result is False, "Should reject path ending with /.."
# Semicolon (command separator)
self.validator.clear_errors()
result = self.validator._validate_path_list("src/;rm -rf /", "paths")
assert result is False, "Should reject semicolon"
assert any("Potential injection" in err for err in self.validator.errors), (
"Should have injection error"
)
# Pipe (command chaining)
self.validator.clear_errors()
result = self.validator._validate_path_list("src/|ls", "paths")
assert result is False, "Should reject pipe"
# Backticks (command substitution)
self.validator.clear_errors()
result = self.validator._validate_path_list("src/`whoami`", "paths")
assert result is False, "Should reject backticks"
# Dollar sign (variable expansion)
self.validator.clear_errors()
result = self.validator._validate_path_list("$HOME/config", "paths")
assert result is False, "Should reject dollar sign"
# Quick wins: Additional enum validators
def test_validate_network_mode_valid(self):
"""Test valid Docker network modes."""
# Valid network modes
self.validator.clear_errors()
result = self.validator._validate_network_mode("host", "network")
assert result is True, "Should accept 'host'"
self.validator.clear_errors()
result = self.validator._validate_network_mode("none", "network")
assert result is True, "Should accept 'none'"
self.validator.clear_errors()
result = self.validator._validate_network_mode("default", "network")
assert result is True, "Should accept 'default'"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_network_mode("", "network")
assert result is True, "Should accept empty string"
def test_validate_network_mode_invalid(self):
"""Test invalid Docker network modes."""
# Invalid values
self.validator.clear_errors()
result = self.validator._validate_network_mode("bridge", "network")
assert result is False, "Should reject 'bridge'"
# Case sensitive
self.validator.clear_errors()
result = self.validator._validate_network_mode("HOST", "network")
assert result is False, "Should reject uppercase"
# Invalid mode
self.validator.clear_errors()
result = self.validator._validate_network_mode("custom", "network")
assert result is False, "Should reject unknown mode"
def test_validate_language_enum_valid(self):
"""Test valid language enum values."""
# Valid languages
self.validator.clear_errors()
result = self.validator._validate_language_enum("php", "language")
assert result is True, "Should accept 'php'"
self.validator.clear_errors()
result = self.validator._validate_language_enum("python", "language")
assert result is True, "Should accept 'python'"
self.validator.clear_errors()
result = self.validator._validate_language_enum("go", "language")
assert result is True, "Should accept 'go'"
self.validator.clear_errors()
result = self.validator._validate_language_enum("dotnet", "language")
assert result is True, "Should accept 'dotnet'"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_language_enum("", "language")
assert result is True, "Should accept empty string"
def test_validate_language_enum_invalid(self):
"""Test invalid language enum values."""
# Invalid languages
self.validator.clear_errors()
result = self.validator._validate_language_enum("node", "language")
assert result is False, "Should reject 'node'"
self.validator.clear_errors()
result = self.validator._validate_language_enum("ruby", "language")
assert result is False, "Should reject 'ruby'"
# Case sensitive
self.validator.clear_errors()
result = self.validator._validate_language_enum("PHP", "language")
assert result is False, "Should reject uppercase"
self.validator.clear_errors()
result = self.validator._validate_language_enum("Python", "language")
assert result is False, "Should reject mixed case"
def test_validate_framework_mode_valid(self):
"""Test valid PHP framework modes."""
# Valid framework modes
self.validator.clear_errors()
result = self.validator._validate_framework_mode("auto", "framework")
assert result is True, "Should accept 'auto'"
self.validator.clear_errors()
result = self.validator._validate_framework_mode("laravel", "framework")
assert result is True, "Should accept 'laravel'"
self.validator.clear_errors()
result = self.validator._validate_framework_mode("generic", "framework")
assert result is True, "Should accept 'generic'"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_framework_mode("", "framework")
assert result is True, "Should accept empty string"
def test_validate_framework_mode_invalid(self):
"""Test invalid PHP framework modes."""
# Invalid frameworks
self.validator.clear_errors()
result = self.validator._validate_framework_mode("symfony", "framework")
assert result is False, "Should reject 'symfony'"
# Case sensitive
self.validator.clear_errors()
result = self.validator._validate_framework_mode("Auto", "framework")
assert result is False, "Should reject mixed case"
self.validator.clear_errors()
result = self.validator._validate_framework_mode("LARAVEL", "framework")
assert result is False, "Should reject uppercase"
# Phase 2C: Specialized validators
def test_validate_json_format_valid(self):
"""Test valid JSON formats."""
# Valid JSON objects
self.validator.clear_errors()
result = self.validator._validate_json_format('{"key":"value"}', "platform-build-args")
assert result is True, "Should accept valid JSON object"
# Valid JSON array
self.validator.clear_errors()
result = self.validator._validate_json_format('["item1","item2"]', "platform-build-args")
assert result is True, "Should accept valid JSON array"
# Complex nested JSON
self.validator.clear_errors()
result = self.validator._validate_json_format(
'{"platforms":["linux/amd64","linux/arm64"],"args":{"GO_VERSION":"1.21"}}',
"platform-build-args",
)
assert result is True, "Should accept complex nested JSON"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_json_format("", "platform-build-args")
assert result is True, "Should accept empty string"
def test_validate_json_format_invalid(self):
"""Test invalid JSON formats."""
# Invalid JSON syntax
self.validator.clear_errors()
result = self.validator._validate_json_format("{invalid}", "platform-build-args")
assert result is False, "Should reject invalid JSON"
assert any("Invalid JSON" in err for err in self.validator.errors)
# Missing quotes
self.validator.clear_errors()
result = self.validator._validate_json_format("{key:value}", "platform-build-args")
assert result is False, "Should reject unquoted keys"
# Not JSON
self.validator.clear_errors()
result = self.validator._validate_json_format("plain text", "platform-build-args")
assert result is False, "Should reject plain text"
def test_validate_cache_config_valid(self):
"""Test valid Docker cache configurations."""
# Registry cache
self.validator.clear_errors()
result = self.validator._validate_cache_config(
"type=registry,ref=user/repo:cache", "cache-from"
)
assert result is True, "Should accept registry cache config"
# Local cache
self.validator.clear_errors()
result = self.validator._validate_cache_config("type=local,dest=/tmp/cache", "cache-export")
assert result is True, "Should accept local cache config"
# GitHub Actions cache
self.validator.clear_errors()
result = self.validator._validate_cache_config("type=gha", "cache-from")
assert result is True, "Should accept gha cache type"
# Inline cache
self.validator.clear_errors()
result = self.validator._validate_cache_config("type=inline", "cache-export")
assert result is True, "Should accept inline cache type"
# S3 cache with multiple parameters
self.validator.clear_errors()
result = self.validator._validate_cache_config(
"type=s3,region=us-east-1,bucket=my-bucket", "cache-export"
)
assert result is True, "Should accept s3 cache with parameters"
# Empty value (optional)
self.validator.clear_errors()
result = self.validator._validate_cache_config("", "cache-from")
assert result is True, "Should accept empty string"
def test_validate_cache_config_invalid(self):
"""Test invalid Docker cache configurations."""
# Missing type
self.validator.clear_errors()
result = self.validator._validate_cache_config("registry", "cache-from")
assert result is False, "Should reject missing type"
assert any("Must start with 'type=" in err for err in self.validator.errors)
# Invalid type
self.validator.clear_errors()
result = self.validator._validate_cache_config("type=invalid", "cache-from")
assert result is False, "Should reject invalid cache type"
assert any("Invalid cache type" in err for err in self.validator.errors)
# Invalid format (missing =)
self.validator.clear_errors()
result = self.validator._validate_cache_config("type=local,destpath", "cache-export")
assert result is False, "Should reject invalid key=value format"