mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +00:00
* docs: update documentation * feat: validate-inputs has it's own pyproject * security: mask DOCKERHUB_PASSWORD * chore: add tokens, checkout, recrete docs, integration tests * fix: add `statuses: write` permission to pr-lint
212 lines
6.9 KiB
Python
212 lines
6.9 KiB
Python
"""Tests for the base validator class."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from validators.base import BaseValidator
|
|
|
|
|
|
class ConcreteValidator(BaseValidator):
|
|
"""Concrete implementation for testing."""
|
|
|
|
def validate_inputs(self, inputs: dict[str, str]) -> bool:
|
|
"""Simple validation implementation."""
|
|
return self.validate_required_inputs(inputs)
|
|
|
|
def get_required_inputs(self) -> list[str]:
|
|
"""Return test required inputs."""
|
|
return ["required1", "required2"]
|
|
|
|
def get_validation_rules(self) -> dict:
|
|
"""Return test validation rules."""
|
|
return {"test": "rules"}
|
|
|
|
|
|
class TestBaseValidator(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|
"""Test the BaseValidator abstract class."""
|
|
|
|
def setUp(self): # pylint: disable=attribute-defined-outside-init
|
|
"""Set up test fixtures."""
|
|
self.validator = ConcreteValidator("test_action")
|
|
|
|
def test_initialization(self):
|
|
"""Test validator initialization."""
|
|
assert self.validator.action_type == "test_action"
|
|
assert self.validator.errors == []
|
|
assert self.validator._rules == {}
|
|
|
|
def test_error_management(self):
|
|
"""Test error handling methods."""
|
|
# Initially no errors
|
|
assert not self.validator.has_errors()
|
|
|
|
# Add an error
|
|
self.validator.add_error("Test error")
|
|
assert self.validator.has_errors()
|
|
assert len(self.validator.errors) == 1
|
|
assert self.validator.errors[0] == "Test error"
|
|
|
|
# Add another error
|
|
self.validator.add_error("Another error")
|
|
assert len(self.validator.errors) == 2
|
|
|
|
# Clear errors
|
|
self.validator.clear_errors()
|
|
assert not self.validator.has_errors()
|
|
assert self.validator.errors == []
|
|
|
|
def test_validate_required_inputs(self):
|
|
"""Test required input validation."""
|
|
# Missing required inputs
|
|
inputs = {}
|
|
assert not self.validator.validate_required_inputs(inputs)
|
|
assert len(self.validator.errors) == 2
|
|
|
|
# Clear for next test
|
|
self.validator.clear_errors()
|
|
|
|
# One required input missing
|
|
inputs = {"required1": "value1"}
|
|
assert not self.validator.validate_required_inputs(inputs)
|
|
assert len(self.validator.errors) == 1
|
|
assert "required2" in self.validator.errors[0]
|
|
|
|
# Clear for next test
|
|
self.validator.clear_errors()
|
|
|
|
# All required inputs present
|
|
inputs = {"required1": "value1", "required2": "value2"}
|
|
assert self.validator.validate_required_inputs(inputs)
|
|
assert not self.validator.has_errors()
|
|
|
|
# Empty required input
|
|
inputs = {"required1": "value1", "required2": " "}
|
|
assert not self.validator.validate_required_inputs(inputs)
|
|
assert "required2" in self.validator.errors[0]
|
|
|
|
def test_validate_security_patterns(self):
|
|
"""Test security pattern validation."""
|
|
# Safe value
|
|
assert self.validator.validate_security_patterns("safe_value")
|
|
assert not self.validator.has_errors()
|
|
|
|
# Command injection patterns
|
|
dangerous_values = [
|
|
"value; rm -rf /",
|
|
"value && malicious",
|
|
"value || exit",
|
|
"value | grep",
|
|
"value `command`",
|
|
"$(command)",
|
|
"${variable}",
|
|
"../../../etc/passwd",
|
|
"..\\..\\windows",
|
|
]
|
|
|
|
for dangerous in dangerous_values:
|
|
self.validator.clear_errors()
|
|
assert not self.validator.validate_security_patterns(dangerous, "test_input"), (
|
|
f"Failed to detect dangerous pattern: {dangerous}"
|
|
)
|
|
assert self.validator.has_errors()
|
|
|
|
def test_validate_path_security(self):
|
|
"""Test path security validation."""
|
|
# Valid paths
|
|
valid_paths = [
|
|
"relative/path/file.txt",
|
|
"file.txt",
|
|
"./local/file",
|
|
"subdir/another/file.yml",
|
|
]
|
|
|
|
for path in valid_paths:
|
|
self.validator.clear_errors()
|
|
assert self.validator.validate_path_security(path), (
|
|
f"Incorrectly rejected valid path: {path}"
|
|
)
|
|
assert not self.validator.has_errors()
|
|
|
|
# Invalid paths
|
|
invalid_paths = [
|
|
"/absolute/path",
|
|
"C:\\Windows\\System32",
|
|
"../parent/directory",
|
|
"path/../../../etc",
|
|
"..\\..\\windows",
|
|
]
|
|
|
|
for path in invalid_paths:
|
|
self.validator.clear_errors()
|
|
assert not self.validator.validate_path_security(path), (
|
|
f"Failed to reject invalid path: {path}"
|
|
)
|
|
assert self.validator.has_errors()
|
|
|
|
def test_validate_empty_allowed(self):
|
|
"""Test empty value validation."""
|
|
# Non-empty value
|
|
assert self.validator.validate_empty_allowed("value", "test")
|
|
assert not self.validator.has_errors()
|
|
|
|
# Empty string
|
|
assert not self.validator.validate_empty_allowed("", "test")
|
|
assert self.validator.has_errors()
|
|
assert "cannot be empty" in self.validator.errors[0]
|
|
|
|
# Whitespace only
|
|
self.validator.clear_errors()
|
|
assert not self.validator.validate_empty_allowed(" ", "test")
|
|
assert self.validator.has_errors()
|
|
|
|
@patch("pathlib.Path.exists")
|
|
@patch("pathlib.Path.open")
|
|
@patch("yaml.safe_load")
|
|
def test_load_rules(self, mock_yaml_load, mock_path_open, mock_exists):
|
|
"""Test loading validation rules from YAML."""
|
|
# The mock_path_open is handled by the patch decorator
|
|
del mock_path_open # Unused but required by decorator
|
|
# Mock YAML content
|
|
mock_rules = {
|
|
"required_inputs": ["input1"],
|
|
"conventions": {"token": "github_token"},
|
|
}
|
|
mock_yaml_load.return_value = mock_rules
|
|
mock_exists.return_value = True
|
|
|
|
# Create a Path object
|
|
from pathlib import Path
|
|
|
|
rules_path = Path("/fake/path/rules.yml")
|
|
|
|
# Load the rules
|
|
rules = self.validator.load_rules(rules_path)
|
|
|
|
assert rules == mock_rules
|
|
assert self.validator._rules == mock_rules
|
|
|
|
def test_github_actions_output(self):
|
|
"""Test GitHub Actions output formatting."""
|
|
# Success case
|
|
output = self.validator.get_github_actions_output()
|
|
assert output["status"] == "success"
|
|
assert output["error"] == ""
|
|
|
|
# Failure case
|
|
self.validator.add_error("Error 1")
|
|
self.validator.add_error("Error 2")
|
|
output = self.validator.get_github_actions_output()
|
|
assert output["status"] == "failure"
|
|
assert output["error"] == "Error 1; Error 2"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|