diff --git a/tests/unit/test_gateway/test_dispatch_mux.py b/tests/unit/test_gateway/test_dispatch_mux.py new file mode 100644 index 00000000..bcd0b42d --- /dev/null +++ b/tests/unit/test_gateway/test_dispatch_mux.py @@ -0,0 +1,165 @@ +""" +Tests for Gateway Dispatch Mux +""" + +import pytest +import asyncio +from unittest.mock import MagicMock, AsyncMock + +from trustgraph.gateway.dispatch.mux import Mux, MAX_QUEUE_SIZE + + +class TestMux: + """Test cases for Mux class""" + + def test_mux_initialization(self): + """Test Mux initialization""" + mock_dispatcher_manager = MagicMock() + mock_ws = MagicMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=mock_ws, + running=mock_running + ) + + assert mux.dispatcher_manager == mock_dispatcher_manager + assert mux.ws == mock_ws + assert mux.running == mock_running + assert isinstance(mux.q, asyncio.Queue) + assert mux.q.maxsize == MAX_QUEUE_SIZE + + @pytest.mark.asyncio + async def test_mux_destroy_with_websocket(self): + """Test Mux destroy method with websocket""" + mock_dispatcher_manager = MagicMock() + mock_ws = AsyncMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=mock_ws, + running=mock_running + ) + + # Call destroy + await mux.destroy() + + # Verify running.stop was called + mock_running.stop.assert_called_once() + + # Verify websocket close was called + mock_ws.close.assert_called_once() + + @pytest.mark.asyncio + async def test_mux_destroy_without_websocket(self): + """Test Mux destroy method without websocket""" + mock_dispatcher_manager = MagicMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=None, + running=mock_running + ) + + # Call destroy + await mux.destroy() + + # Verify running.stop was called + mock_running.stop.assert_called_once() + # No websocket to close + + @pytest.mark.asyncio + async def test_mux_receive_valid_message(self): + """Test Mux receive method with valid message""" + mock_dispatcher_manager = MagicMock() + mock_ws = MagicMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=mock_ws, + running=mock_running + ) + + # Mock message with valid JSON + mock_msg = MagicMock() + mock_msg.json.return_value = { + "request": {"type": "test"}, + "id": "test-id-123" + } + + # Call receive + await mux.receive(mock_msg) + + # Verify json was called + mock_msg.json.assert_called_once() + + @pytest.mark.asyncio + async def test_mux_receive_message_without_request(self): + """Test Mux receive method with message missing request field""" + mock_dispatcher_manager = MagicMock() + mock_ws = MagicMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=mock_ws, + running=mock_running + ) + + # Mock message without request field + mock_msg = MagicMock() + mock_msg.json.return_value = { + "id": "test-id-123" + } + + # receive method should handle the RuntimeError internally + # Based on the code, it seems to catch exceptions + await mux.receive(mock_msg) + + @pytest.mark.asyncio + async def test_mux_receive_message_without_id(self): + """Test Mux receive method with message missing id field""" + mock_dispatcher_manager = MagicMock() + mock_ws = MagicMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=mock_ws, + running=mock_running + ) + + # Mock message without id field + mock_msg = MagicMock() + mock_msg.json.return_value = { + "request": {"type": "test"} + } + + # receive method should handle the RuntimeError internally + await mux.receive(mock_msg) + + @pytest.mark.asyncio + async def test_mux_receive_invalid_json(self): + """Test Mux receive method with invalid JSON""" + mock_dispatcher_manager = MagicMock() + mock_ws = MagicMock() + mock_running = MagicMock() + + mux = Mux( + dispatcher_manager=mock_dispatcher_manager, + ws=mock_ws, + running=mock_running + ) + + # Mock message with invalid JSON + mock_msg = MagicMock() + mock_msg.json.side_effect = ValueError("Invalid JSON") + + # receive method should handle the ValueError internally + await mux.receive(mock_msg) + + mock_msg.json.assert_called_once() \ No newline at end of file diff --git a/tests/unit/test_gateway/test_dispatch_requestor.py b/tests/unit/test_gateway/test_dispatch_requestor.py new file mode 100644 index 00000000..3d3a396b --- /dev/null +++ b/tests/unit/test_gateway/test_dispatch_requestor.py @@ -0,0 +1,115 @@ +""" +Tests for Gateway Service Requestor +""" + +import pytest +from unittest.mock import MagicMock, AsyncMock, patch + +from trustgraph.gateway.dispatch.requestor import ServiceRequestor + + +class TestServiceRequestor: + """Test cases for ServiceRequestor class""" + + @patch('trustgraph.gateway.dispatch.requestor.Publisher') + @patch('trustgraph.gateway.dispatch.requestor.Subscriber') + def test_service_requestor_initialization(self, mock_subscriber, mock_publisher): + """Test ServiceRequestor initialization""" + mock_pulsar_client = MagicMock() + mock_request_schema = MagicMock() + mock_response_schema = MagicMock() + + requestor = ServiceRequestor( + pulsar_client=mock_pulsar_client, + request_queue="test-request-queue", + request_schema=mock_request_schema, + response_queue="test-response-queue", + response_schema=mock_response_schema, + subscription="test-subscription", + consumer_name="test-consumer", + timeout=300 + ) + + # Verify Publisher was created correctly + mock_publisher.assert_called_once_with( + mock_pulsar_client, "test-request-queue", schema=mock_request_schema + ) + + # Verify Subscriber was created correctly + mock_subscriber.assert_called_once_with( + mock_pulsar_client, "test-response-queue", + "test-subscription", "test-consumer", mock_response_schema + ) + + assert requestor.timeout == 300 + assert requestor.running is True + + @patch('trustgraph.gateway.dispatch.requestor.Publisher') + @patch('trustgraph.gateway.dispatch.requestor.Subscriber') + def test_service_requestor_with_defaults(self, mock_subscriber, mock_publisher): + """Test ServiceRequestor initialization with default parameters""" + mock_pulsar_client = MagicMock() + mock_request_schema = MagicMock() + mock_response_schema = MagicMock() + + requestor = ServiceRequestor( + pulsar_client=mock_pulsar_client, + request_queue="test-queue", + request_schema=mock_request_schema, + response_queue="response-queue", + response_schema=mock_response_schema + ) + + # Verify default values + mock_subscriber.assert_called_once_with( + mock_pulsar_client, "response-queue", + "api-gateway", "api-gateway", mock_response_schema + ) + assert requestor.timeout == 600 # Default timeout + + @patch('trustgraph.gateway.dispatch.requestor.Publisher') + @patch('trustgraph.gateway.dispatch.requestor.Subscriber') + @pytest.mark.asyncio + async def test_service_requestor_start(self, mock_subscriber, mock_publisher): + """Test ServiceRequestor start method""" + mock_pulsar_client = MagicMock() + mock_sub_instance = AsyncMock() + mock_subscriber.return_value = mock_sub_instance + + requestor = ServiceRequestor( + pulsar_client=mock_pulsar_client, + request_queue="test-queue", + request_schema=MagicMock(), + response_queue="response-queue", + response_schema=MagicMock() + ) + + # Call start + await requestor.start() + + # Verify subscriber start was called + mock_sub_instance.start.assert_called_once() + assert requestor.running is True + + @patch('trustgraph.gateway.dispatch.requestor.Publisher') + @patch('trustgraph.gateway.dispatch.requestor.Subscriber') + def test_service_requestor_attributes(self, mock_subscriber, mock_publisher): + """Test ServiceRequestor has correct attributes""" + mock_pulsar_client = MagicMock() + mock_pub_instance = MagicMock() + mock_sub_instance = MagicMock() + mock_publisher.return_value = mock_pub_instance + mock_subscriber.return_value = mock_sub_instance + + requestor = ServiceRequestor( + pulsar_client=mock_pulsar_client, + request_queue="test-queue", + request_schema=MagicMock(), + response_queue="response-queue", + response_schema=MagicMock() + ) + + # Verify attributes are set correctly + assert requestor.pub == mock_pub_instance + assert requestor.sub == mock_sub_instance + assert requestor.running is True \ No newline at end of file diff --git a/tests/unit/test_gateway/test_dispatch_sender.py b/tests/unit/test_gateway/test_dispatch_sender.py new file mode 100644 index 00000000..692604d5 --- /dev/null +++ b/tests/unit/test_gateway/test_dispatch_sender.py @@ -0,0 +1,120 @@ +""" +Tests for Gateway Service Sender +""" + +import pytest +from unittest.mock import MagicMock, AsyncMock, patch + +from trustgraph.gateway.dispatch.sender import ServiceSender + + +class TestServiceSender: + """Test cases for ServiceSender class""" + + @patch('trustgraph.gateway.dispatch.sender.Publisher') + def test_service_sender_initialization(self, mock_publisher): + """Test ServiceSender initialization""" + mock_pulsar_client = MagicMock() + mock_schema = MagicMock() + + sender = ServiceSender( + pulsar_client=mock_pulsar_client, + queue="test-queue", + schema=mock_schema + ) + + # Verify Publisher was created correctly + mock_publisher.assert_called_once_with( + mock_pulsar_client, "test-queue", schema=mock_schema + ) + + @patch('trustgraph.gateway.dispatch.sender.Publisher') + @pytest.mark.asyncio + async def test_service_sender_start(self, mock_publisher): + """Test ServiceSender start method""" + mock_pub_instance = AsyncMock() + mock_publisher.return_value = mock_pub_instance + + sender = ServiceSender( + pulsar_client=MagicMock(), + queue="test-queue", + schema=MagicMock() + ) + + # Call start + await sender.start() + + # Verify publisher start was called + mock_pub_instance.start.assert_called_once() + + @patch('trustgraph.gateway.dispatch.sender.Publisher') + @pytest.mark.asyncio + async def test_service_sender_stop(self, mock_publisher): + """Test ServiceSender stop method""" + mock_pub_instance = AsyncMock() + mock_publisher.return_value = mock_pub_instance + + sender = ServiceSender( + pulsar_client=MagicMock(), + queue="test-queue", + schema=MagicMock() + ) + + # Call stop + await sender.stop() + + # Verify publisher stop was called + mock_pub_instance.stop.assert_called_once() + + @patch('trustgraph.gateway.dispatch.sender.Publisher') + def test_service_sender_to_request_not_implemented(self, mock_publisher): + """Test ServiceSender to_request method raises RuntimeError""" + sender = ServiceSender( + pulsar_client=MagicMock(), + queue="test-queue", + schema=MagicMock() + ) + + with pytest.raises(RuntimeError, match="Not defined"): + sender.to_request({"test": "request"}) + + @patch('trustgraph.gateway.dispatch.sender.Publisher') + @pytest.mark.asyncio + async def test_service_sender_process(self, mock_publisher): + """Test ServiceSender process method""" + mock_pub_instance = AsyncMock() + mock_publisher.return_value = mock_pub_instance + + # Create a concrete sender that implements to_request + class ConcreteSender(ServiceSender): + def to_request(self, request): + return {"processed": request} + + sender = ConcreteSender( + pulsar_client=MagicMock(), + queue="test-queue", + schema=MagicMock() + ) + + test_request = {"test": "data"} + + # Call process + await sender.process(test_request) + + # Verify publisher send was called with processed request + mock_pub_instance.send.assert_called_once_with(None, {"processed": test_request}) + + @patch('trustgraph.gateway.dispatch.sender.Publisher') + def test_service_sender_attributes(self, mock_publisher): + """Test ServiceSender has correct attributes""" + mock_pub_instance = MagicMock() + mock_publisher.return_value = mock_pub_instance + + sender = ServiceSender( + pulsar_client=MagicMock(), + queue="test-queue", + schema=MagicMock() + ) + + # Verify attributes are set correctly + assert sender.pub == mock_pub_instance \ No newline at end of file diff --git a/tests/unit/test_gateway/test_endpoint_manager.py b/tests/unit/test_gateway/test_endpoint_manager.py new file mode 100644 index 00000000..a4d0490a --- /dev/null +++ b/tests/unit/test_gateway/test_endpoint_manager.py @@ -0,0 +1,85 @@ +""" +Tests for Gateway Endpoint Manager +""" + +import pytest +from unittest.mock import MagicMock + +from trustgraph.gateway.endpoint.manager import EndpointManager + + +class TestEndpointManager: + """Test cases for EndpointManager class""" + + def test_endpoint_manager_initialization(self): + """Test EndpointManager initialization creates all endpoints""" + mock_dispatcher_manager = MagicMock() + mock_auth = MagicMock() + + # Mock dispatcher methods + mock_dispatcher_manager.dispatch_global_service.return_value = MagicMock() + mock_dispatcher_manager.dispatch_socket.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_service.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_import.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_export.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_stream.return_value = MagicMock() + + manager = EndpointManager( + dispatcher_manager=mock_dispatcher_manager, + auth=mock_auth, + prometheus_url="http://prometheus:9090", + timeout=300 + ) + + assert manager.dispatcher_manager == mock_dispatcher_manager + assert manager.timeout == 300 + assert manager.services == {} + assert len(manager.endpoints) > 0 # Should have multiple endpoints + + def test_endpoint_manager_with_default_timeout(self): + """Test EndpointManager with default timeout value""" + mock_dispatcher_manager = MagicMock() + mock_auth = MagicMock() + + # Mock dispatcher methods + mock_dispatcher_manager.dispatch_global_service.return_value = MagicMock() + mock_dispatcher_manager.dispatch_socket.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_service.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_import.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_export.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_stream.return_value = MagicMock() + + manager = EndpointManager( + dispatcher_manager=mock_dispatcher_manager, + auth=mock_auth, + prometheus_url="http://prometheus:9090" + ) + + assert manager.timeout == 600 # Default value + + def test_endpoint_manager_dispatcher_calls(self): + """Test EndpointManager calls all required dispatcher methods""" + mock_dispatcher_manager = MagicMock() + mock_auth = MagicMock() + + # Mock dispatcher methods + mock_dispatcher_manager.dispatch_global_service.return_value = MagicMock() + mock_dispatcher_manager.dispatch_socket.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_service.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_import.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_export.return_value = MagicMock() + mock_dispatcher_manager.dispatch_flow_stream.return_value = MagicMock() + + EndpointManager( + dispatcher_manager=mock_dispatcher_manager, + auth=mock_auth, + prometheus_url="http://test:9090" + ) + + # Verify all dispatcher methods were called during initialization + mock_dispatcher_manager.dispatch_global_service.assert_called_once() + mock_dispatcher_manager.dispatch_socket.assert_called() + mock_dispatcher_manager.dispatch_flow_service.assert_called_once() + mock_dispatcher_manager.dispatch_flow_import.assert_called() + mock_dispatcher_manager.dispatch_flow_export.assert_called() + mock_dispatcher_manager.dispatch_flow_stream.assert_called() \ No newline at end of file diff --git a/tests/unit/test_gateway/test_endpoint_socket.py b/tests/unit/test_gateway/test_endpoint_socket.py new file mode 100644 index 00000000..7c7a127c --- /dev/null +++ b/tests/unit/test_gateway/test_endpoint_socket.py @@ -0,0 +1,124 @@ +""" +Tests for Gateway Socket Endpoint +""" + +import pytest +from unittest.mock import MagicMock, AsyncMock +from aiohttp import WSMsgType + +from trustgraph.gateway.endpoint.socket import SocketEndpoint + + +class TestSocketEndpoint: + """Test cases for SocketEndpoint class""" + + def test_socket_endpoint_initialization(self): + """Test SocketEndpoint initialization""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + + endpoint = SocketEndpoint( + endpoint_path="/api/socket", + auth=mock_auth, + dispatcher=mock_dispatcher + ) + + assert endpoint.path == "/api/socket" + assert endpoint.auth == mock_auth + assert endpoint.dispatcher == mock_dispatcher + assert endpoint.operation == "socket" + + @pytest.mark.asyncio + async def test_worker_method(self): + """Test SocketEndpoint worker method""" + mock_auth = MagicMock() + mock_dispatcher = AsyncMock() + + endpoint = SocketEndpoint("/api/socket", mock_auth, mock_dispatcher) + + mock_ws = MagicMock() + mock_running = MagicMock() + + # Call worker method + await endpoint.worker(mock_ws, mock_dispatcher, mock_running) + + # Verify dispatcher.run was called + mock_dispatcher.run.assert_called_once() + + @pytest.mark.asyncio + async def test_listener_method_with_text_message(self): + """Test SocketEndpoint listener method with text message""" + mock_auth = MagicMock() + mock_dispatcher = AsyncMock() + + endpoint = SocketEndpoint("/api/socket", mock_auth, mock_dispatcher) + + # Mock websocket with text message + mock_msg = MagicMock() + mock_msg.type = WSMsgType.TEXT + + # Create async iterator for websocket + async def async_iter(): + yield mock_msg + + mock_ws = MagicMock() + mock_ws.__aiter__ = lambda: async_iter() + mock_running = MagicMock() + + # Call listener method + await endpoint.listener(mock_ws, mock_dispatcher, mock_running) + + # Verify dispatcher.receive was called with the message + mock_dispatcher.receive.assert_called_once_with(mock_msg) + + @pytest.mark.asyncio + async def test_listener_method_with_binary_message(self): + """Test SocketEndpoint listener method with binary message""" + mock_auth = MagicMock() + mock_dispatcher = AsyncMock() + + endpoint = SocketEndpoint("/api/socket", mock_auth, mock_dispatcher) + + # Mock websocket with binary message + mock_msg = MagicMock() + mock_msg.type = WSMsgType.BINARY + + # Create async iterator for websocket + async def async_iter(): + yield mock_msg + + mock_ws = MagicMock() + mock_ws.__aiter__ = lambda: async_iter() + mock_running = MagicMock() + + # Call listener method + await endpoint.listener(mock_ws, mock_dispatcher, mock_running) + + # Verify dispatcher.receive was called with the message + mock_dispatcher.receive.assert_called_once_with(mock_msg) + + @pytest.mark.asyncio + async def test_listener_method_with_close_message(self): + """Test SocketEndpoint listener method with close message""" + mock_auth = MagicMock() + mock_dispatcher = AsyncMock() + + endpoint = SocketEndpoint("/api/socket", mock_auth, mock_dispatcher) + + # Mock websocket with close message + mock_msg = MagicMock() + mock_msg.type = WSMsgType.CLOSE + + # Create async iterator for websocket + async def async_iter(): + yield mock_msg + + mock_ws = MagicMock() + mock_ws.__aiter__ = lambda: async_iter() + mock_running = MagicMock() + + # Call listener method + await endpoint.listener(mock_ws, mock_dispatcher, mock_running) + + # Verify dispatcher.receive was NOT called for close message + mock_dispatcher.receive.assert_not_called() \ No newline at end of file diff --git a/tests/unit/test_gateway/test_endpoint_stream.py b/tests/unit/test_gateway/test_endpoint_stream.py new file mode 100644 index 00000000..b99946c8 --- /dev/null +++ b/tests/unit/test_gateway/test_endpoint_stream.py @@ -0,0 +1,124 @@ +""" +Tests for Gateway Stream Endpoint +""" + +import pytest +from unittest.mock import MagicMock + +from trustgraph.gateway.endpoint.stream_endpoint import StreamEndpoint + + +class TestStreamEndpoint: + """Test cases for StreamEndpoint class""" + + def test_stream_endpoint_initialization_with_post(self): + """Test StreamEndpoint initialization with POST method""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + + endpoint = StreamEndpoint( + endpoint_path="/api/stream", + auth=mock_auth, + dispatcher=mock_dispatcher, + method="POST" + ) + + assert endpoint.path == "/api/stream" + assert endpoint.auth == mock_auth + assert endpoint.dispatcher == mock_dispatcher + assert endpoint.operation == "service" + assert endpoint.method == "POST" + + def test_stream_endpoint_initialization_with_get(self): + """Test StreamEndpoint initialization with GET method""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + + endpoint = StreamEndpoint( + endpoint_path="/api/stream", + auth=mock_auth, + dispatcher=mock_dispatcher, + method="GET" + ) + + assert endpoint.method == "GET" + + def test_stream_endpoint_initialization_default_method(self): + """Test StreamEndpoint initialization with default POST method""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + + endpoint = StreamEndpoint( + endpoint_path="/api/stream", + auth=mock_auth, + dispatcher=mock_dispatcher + ) + + assert endpoint.method == "POST" # Default value + + @pytest.mark.asyncio + async def test_stream_endpoint_start_method(self): + """Test StreamEndpoint start method (should be no-op)""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + + endpoint = StreamEndpoint("/api/stream", mock_auth, mock_dispatcher) + + # start() should complete without error + await endpoint.start() + + def test_add_routes_with_post_method(self): + """Test add_routes method with POST method""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + mock_app = MagicMock() + + endpoint = StreamEndpoint( + endpoint_path="/api/stream", + auth=mock_auth, + dispatcher=mock_dispatcher, + method="POST" + ) + + endpoint.add_routes(mock_app) + + # Verify add_routes was called with POST route + mock_app.add_routes.assert_called_once() + call_args = mock_app.add_routes.call_args[0][0] + assert len(call_args) == 1 # One route added + + def test_add_routes_with_get_method(self): + """Test add_routes method with GET method""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + mock_app = MagicMock() + + endpoint = StreamEndpoint( + endpoint_path="/api/stream", + auth=mock_auth, + dispatcher=mock_dispatcher, + method="GET" + ) + + endpoint.add_routes(mock_app) + + # Verify add_routes was called with GET route + mock_app.add_routes.assert_called_once() + call_args = mock_app.add_routes.call_args[0][0] + assert len(call_args) == 1 # One route added + + def test_add_routes_with_invalid_method_raises_error(self): + """Test add_routes method with invalid method raises RuntimeError""" + mock_auth = MagicMock() + mock_dispatcher = MagicMock() + mock_app = MagicMock() + + endpoint = StreamEndpoint( + endpoint_path="/api/stream", + auth=mock_auth, + dispatcher=mock_dispatcher, + method="INVALID" + ) + + with pytest.raises(RuntimeError, match="Bad method"): + endpoint.add_routes(mock_app) \ No newline at end of file