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