Files
actions/validate-inputs/tests/test_base.py
Ismo Vuorinen 7061aafd35 chore: add tests, update docs and actions (#299)
* 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
2025-10-18 13:09:19 +03:00

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()