All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 26s
230 lines
7.6 KiB
Python
230 lines
7.6 KiB
Python
"""Tests for safe dispatch utility."""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
# Add tools directory to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "tools" / "ai-review"))
|
|
|
|
from utils.safe_dispatch import (
|
|
MAX_EVENT_SIZE,
|
|
load_event_data,
|
|
safe_dispatch,
|
|
)
|
|
|
|
|
|
class TestLoadEventData:
|
|
"""Test event data loading and validation."""
|
|
|
|
def test_load_valid_json(self):
|
|
"""Test loading valid JSON."""
|
|
event_json = '{"action": "created", "issue": {"number": 123}}'
|
|
|
|
data = load_event_data(event_json)
|
|
|
|
assert data["action"] == "created"
|
|
assert data["issue"]["number"] == 123
|
|
|
|
def test_reject_invalid_json(self):
|
|
"""Test that invalid JSON is rejected."""
|
|
invalid_json = '{"action": "created", invalid}'
|
|
|
|
with pytest.raises(ValueError, match="Invalid JSON"):
|
|
load_event_data(invalid_json)
|
|
|
|
def test_reject_too_large_data(self):
|
|
"""Test that data exceeding size limit is rejected."""
|
|
# Create JSON larger than MAX_EVENT_SIZE
|
|
large_data = {"data": "x" * (MAX_EVENT_SIZE + 1)}
|
|
large_json = json.dumps(large_data)
|
|
|
|
with pytest.raises(ValueError, match="Event data too large"):
|
|
load_event_data(large_json)
|
|
|
|
def test_reject_non_object_json(self):
|
|
"""Test that non-object JSON is rejected."""
|
|
# JSON array
|
|
with pytest.raises(ValueError, match="must be a JSON object"):
|
|
load_event_data('["array"]')
|
|
|
|
# JSON string
|
|
with pytest.raises(ValueError, match="must be a JSON object"):
|
|
load_event_data('"string"')
|
|
|
|
# JSON number
|
|
load_event_data("123")
|
|
|
|
def test_accept_empty_object(self):
|
|
"""Test that empty object is valid."""
|
|
data = load_event_data("{}")
|
|
assert data == {}
|
|
|
|
|
|
class TestSafeDispatch:
|
|
"""Test safe dispatch functionality."""
|
|
|
|
@patch("utils.safe_dispatch.get_dispatcher")
|
|
def test_successful_dispatch(self, mock_get_dispatcher):
|
|
"""Test successful event dispatch."""
|
|
# Mock dispatcher
|
|
mock_dispatcher = Mock()
|
|
mock_result = Mock()
|
|
mock_result.errors = []
|
|
mock_result.agents_run = ["PRAgent"]
|
|
mock_result.results = [Mock(success=True, message="Success")]
|
|
mock_dispatcher.dispatch.return_value = mock_result
|
|
mock_get_dispatcher.return_value = mock_dispatcher
|
|
|
|
event_json = json.dumps(
|
|
{
|
|
"action": "created",
|
|
"issue": {"number": 123},
|
|
"comment": {"body": "test"},
|
|
}
|
|
)
|
|
|
|
exit_code = safe_dispatch("issue_comment", "owner/repo", event_json)
|
|
|
|
assert exit_code == 0
|
|
mock_dispatcher.dispatch.assert_called_once()
|
|
|
|
@patch("utils.safe_dispatch.get_dispatcher")
|
|
def test_dispatch_with_errors(self, mock_get_dispatcher):
|
|
"""Test dispatch that encounters errors."""
|
|
# Mock dispatcher with errors
|
|
mock_dispatcher = Mock()
|
|
mock_result = Mock()
|
|
mock_result.errors = ["Agent failed"]
|
|
mock_result.agents_run = ["PRAgent"]
|
|
mock_result.results = [Mock(success=False, message="Failed")]
|
|
mock_dispatcher.dispatch.return_value = mock_result
|
|
mock_get_dispatcher.return_value = mock_dispatcher
|
|
|
|
event_json = '{"action": "created"}'
|
|
|
|
exit_code = safe_dispatch("issue_comment", "owner/repo", event_json)
|
|
|
|
assert exit_code == 1
|
|
|
|
def test_invalid_repository_format(self):
|
|
"""Test that invalid repository format returns error."""
|
|
event_json = '{"action": "created"}'
|
|
|
|
exit_code = safe_dispatch("issue_comment", "invalid-repo", event_json)
|
|
|
|
assert exit_code == 1
|
|
|
|
def test_path_traversal_rejection(self):
|
|
"""Test that path traversal attempts are rejected."""
|
|
event_json = '{"action": "created"}'
|
|
|
|
exit_code = safe_dispatch("issue_comment", "owner/../../etc/passwd", event_json)
|
|
|
|
assert exit_code == 1
|
|
|
|
def test_shell_injection_rejection(self):
|
|
"""Test that shell injection attempts are rejected."""
|
|
event_json = '{"action": "created"}'
|
|
|
|
exit_code = safe_dispatch("issue_comment", "owner/repo; rm -rf /", event_json)
|
|
|
|
assert exit_code == 1
|
|
|
|
def test_invalid_json_rejection(self):
|
|
"""Test that invalid JSON returns error."""
|
|
exit_code = safe_dispatch("issue_comment", "owner/repo", "invalid json")
|
|
|
|
assert exit_code == 1
|
|
|
|
@patch("utils.safe_dispatch.get_dispatcher")
|
|
def test_sanitization_applied(self, mock_get_dispatcher):
|
|
"""Test that data is sanitized before dispatch."""
|
|
mock_dispatcher = Mock()
|
|
mock_result = Mock()
|
|
mock_result.errors = []
|
|
mock_result.agents_run = []
|
|
mock_result.results = []
|
|
mock_dispatcher.dispatch.return_value = mock_result
|
|
mock_get_dispatcher.return_value = mock_dispatcher
|
|
|
|
# Event with sensitive data
|
|
event_json = json.dumps(
|
|
{
|
|
"action": "created",
|
|
"issue": {
|
|
"number": 123,
|
|
"user": {
|
|
"login": "testuser",
|
|
"email": "secret@example.com", # Should be sanitized
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
safe_dispatch("issue_comment", "owner/repo", event_json)
|
|
|
|
# Check that dispatch was called
|
|
call_args = mock_dispatcher.dispatch.call_args
|
|
dispatched_data = call_args[1]["event_data"]
|
|
|
|
# Sensitive data should not be in the minimal context
|
|
assert "email" not in str(dispatched_data)
|
|
|
|
@patch("utils.safe_dispatch.get_dispatcher")
|
|
def test_exception_handling(self, mock_get_dispatcher):
|
|
"""Test that unexpected exceptions are handled."""
|
|
mock_dispatcher = Mock()
|
|
mock_dispatcher.dispatch.side_effect = Exception("Unexpected error")
|
|
mock_get_dispatcher.return_value = mock_dispatcher
|
|
|
|
event_json = '{"action": "created"}'
|
|
|
|
exit_code = safe_dispatch("issue_comment", "owner/repo", event_json)
|
|
|
|
assert exit_code == 1
|
|
|
|
|
|
class TestInputValidation:
|
|
"""Test input validation edge cases."""
|
|
|
|
def test_repository_with_special_chars(self):
|
|
"""Test repository names with allowed special characters."""
|
|
event_json = '{"action": "created"}'
|
|
|
|
# Underscores and hyphens are allowed
|
|
with patch("utils.safe_dispatch.get_dispatcher") as mock:
|
|
mock_dispatcher = Mock()
|
|
mock_result = Mock(errors=[], agents_run=[], results=[])
|
|
mock_dispatcher.dispatch.return_value = mock_result
|
|
mock.return_value = mock_dispatcher
|
|
|
|
exit_code = safe_dispatch("issue_comment", "my-org/my_repo", event_json)
|
|
assert exit_code == 0
|
|
|
|
def test_unicode_in_event_data(self):
|
|
"""Test handling of Unicode in event data."""
|
|
event_json = json.dumps(
|
|
{
|
|
"action": "created",
|
|
"comment": {"body": "Hello 世界 🌍"},
|
|
}
|
|
)
|
|
|
|
with patch("utils.safe_dispatch.get_dispatcher") as mock:
|
|
with patch('utils.safe_dispatch.get_dispatcher') as mock:
|
|
mock_dispatcher = Mock()
|
|
mock_result = Mock(errors=[], agents_run=[], results=[])
|
|
mock_dispatcher.dispatch.return_value = mock_result
|
|
mock.return_value = mock_dispatcher
|
|
|
|
exit_code = safe_dispatch("issue_comment", "owner/repo", event_json)
|
|
assert exit_code == 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|