Files
openrabbit/tests/test_safe_dispatch.py
latte f94d21580c
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 26s
security fixes
2025-12-28 19:55:05 +00:00

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"])