Files
actions/validate-inputs/tests/test_validator.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

246 lines
8.4 KiB
Python

"""Tests for the main validator entry point."""
import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest # pylint: disable=import-error
# Add the parent directory to the path
sys.path.insert(0, str(Path(__file__).parent.parent))
class TestValidatorScript:
"""Test the main validator.py script functionality."""
def setup_method(self):
"""Set up test environment before each test."""
# Clear environment variables
for key in list(os.environ.keys()):
if key.startswith("INPUT_"):
del os.environ[key]
# Create temporary output file
# Need persistent file for teardown, can't use context manager
self.temp_output = tempfile.NamedTemporaryFile(mode="w", delete=False) # noqa: SIM115
os.environ["GITHUB_OUTPUT"] = self.temp_output.name
self.temp_output.close()
def teardown_method(self):
"""Clean up after each test."""
# Clean up temp file
if hasattr(self, "temp_output") and Path(self.temp_output.name).exists():
os.unlink(self.temp_output.name)
# Clear environment
for key in list(os.environ.keys()):
if key.startswith("INPUT_"):
del os.environ[key]
def test_main_no_action_type(self):
"""Test that validator fails when no action type is provided."""
# Remove action type
if "INPUT_ACTION_TYPE" in os.environ:
del os.environ["INPUT_ACTION_TYPE"]
from validator import main
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
def test_main_with_valid_inputs(self):
"""Test validator with valid inputs."""
os.environ["INPUT_ACTION_TYPE"] = "docker-build"
os.environ["INPUT_CONTEXT"] = "." # Required by docker-build custom validator
os.environ["INPUT_IMAGE"] = "myapp"
os.environ["INPUT_TAG"] = "v1.0.0"
from validator import main
# Should not raise SystemExit
main()
# Check output file
with Path(self.temp_output.name).open() as f:
output = f.read()
assert "status=success" in output
assert "action=docker_build" in output
def test_main_with_invalid_inputs(self):
"""Test validator with invalid inputs."""
os.environ["INPUT_ACTION_TYPE"] = "docker-build"
os.environ["INPUT_IMAGE"] = "INVALID-IMAGE" # Uppercase not allowed
from validator import main
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
# Check output file
with Path(self.temp_output.name).open() as f:
output = f.read()
assert "status=failure" in output
def test_main_collects_all_inputs(self):
"""Test that validator collects all INPUT_ environment variables."""
os.environ["INPUT_ACTION_TYPE"] = "test-action"
os.environ["INPUT_FIRST_INPUT"] = "value1"
os.environ["INPUT_SECOND_INPUT"] = "value2"
os.environ["INPUT_THIRD_INPUT"] = "value3"
# Mock the validator to capture inputs
mock_validator = MagicMock()
mock_validator.validate_inputs.return_value = True
mock_validator.errors = []
# Patch get_validator at module level
with patch("validator.get_validator") as mock_get_validator:
mock_get_validator.return_value = mock_validator
from validator import main
main()
# Check that validate_inputs was called with correct inputs
mock_validator.validate_inputs.assert_called_once()
inputs = mock_validator.validate_inputs.call_args[0][0]
# Should have both underscore and dash versions
assert inputs == {
"first_input": "value1",
"first-input": "value1",
"second_input": "value2",
"second-input": "value2",
"third_input": "value3",
"third-input": "value3",
}
def test_main_output_format(self):
"""Test that output is formatted correctly for GitHub Actions."""
os.environ["INPUT_ACTION_TYPE"] = "test-action"
from validators.base import BaseValidator
from validators.registry import ValidatorRegistry
class TestValidator(BaseValidator):
def validate_inputs(self, inputs): # noqa: ARG002
return True
def get_required_inputs(self):
return []
def get_validation_rules(self):
return {}
registry = ValidatorRegistry()
registry.register_validator("test-action", TestValidator)
from validator import main
main()
# Check GitHub output format
with Path(self.temp_output.name).open() as f:
output = f.read()
assert "status=success" in output
assert "action=test_action" in output
assert "inputs_validated=" in output
def test_main_error_reporting(self):
"""Test that validation errors are properly reported."""
os.environ["INPUT_ACTION_TYPE"] = "test-action"
os.environ["INPUT_TEST"] = "invalid"
# Create a mock validator that returns errors
mock_validator = MagicMock()
mock_validator.validate_inputs.return_value = False
mock_validator.errors = ["Test error 1", "Test error 2"]
# Patch get_validator at module level
with patch("validator.get_validator") as mock_get_validator:
mock_get_validator.return_value = mock_validator
from validator import main
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
# Check output file contains error count
with Path(self.temp_output.name).open() as f:
output = f.read()
assert "status=failure" in output
assert "errors=2" in output
class TestValidatorIntegration:
"""Integration tests for the validator system."""
def setup_method(self):
"""Set up test environment."""
# Need persistent file for teardown, can't use context manager
self.temp_output = tempfile.NamedTemporaryFile(mode="w", delete=False) # noqa: SIM115
os.environ["GITHUB_OUTPUT"] = self.temp_output.name
self.temp_output.close()
def teardown_method(self):
"""Clean up after tests."""
if hasattr(self, "temp_output") and Path(self.temp_output.name).exists():
os.unlink(self.temp_output.name)
# Clear environment
for key in list(os.environ.keys()):
if key.startswith("INPUT_"):
del os.environ[key]
def test_registry_loads_correct_validator(self):
"""Test that registry loads the correct validator for each action."""
from validators.registry import ValidatorRegistry
registry = ValidatorRegistry()
# Test that we get validators for known actions
docker_validator = registry.get_validator("docker-build")
assert docker_validator is not None
assert hasattr(docker_validator, "validate_inputs")
# Test fallback for unknown action
unknown_validator = registry.get_validator("unknown-action")
assert unknown_validator is not None
assert hasattr(unknown_validator, "validate_inputs")
def test_custom_validator_loading(self):
"""Test that custom validators are loaded when available."""
from validators.registry import ValidatorRegistry
registry = ValidatorRegistry()
# sync-labels has a custom validator
validator = registry.get_validator("sync-labels")
assert validator is not None
assert validator.__class__.__name__ == "CustomValidator"
def test_convention_based_validation(self):
"""Test that convention-based validation works."""
from validators.registry import ValidatorRegistry
registry = ValidatorRegistry()
validator = registry.get_validator("test-action")
# Test different convention patterns
test_inputs = {
"dry-run": "true", # Boolean
"token": "${{ github.token }}", # Token
"version": "1.2.3", # Version
"email": "test@example.com", # Email
}
# Convention validator should handle these
result = validator.validate_inputs(test_inputs)
# The result depends on the specific validation logic
assert isinstance(result, bool)