mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +00:00
1375 lines
55 KiB
Python
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"
|