mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 16:36:21 +02:00
Test suite executed from CI pipeline (#433)
* Test strategy & test cases * Unit tests * Integration tests
This commit is contained in:
parent
9c7a070681
commit
2f7fddd206
101 changed files with 17811 additions and 1 deletions
69
tests/unit/test_gateway/test_auth.py
Normal file
69
tests/unit/test_gateway/test_auth.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
Tests for Gateway Authentication
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from trustgraph.gateway.auth import Authenticator
|
||||
|
||||
|
||||
class TestAuthenticator:
|
||||
"""Test cases for Authenticator class"""
|
||||
|
||||
def test_authenticator_initialization_with_token(self):
|
||||
"""Test Authenticator initialization with valid token"""
|
||||
auth = Authenticator(token="test-token-123")
|
||||
|
||||
assert auth.token == "test-token-123"
|
||||
assert auth.allow_all is False
|
||||
|
||||
def test_authenticator_initialization_with_allow_all(self):
|
||||
"""Test Authenticator initialization with allow_all=True"""
|
||||
auth = Authenticator(allow_all=True)
|
||||
|
||||
assert auth.token is None
|
||||
assert auth.allow_all is True
|
||||
|
||||
def test_authenticator_initialization_without_token_raises_error(self):
|
||||
"""Test Authenticator initialization without token raises RuntimeError"""
|
||||
with pytest.raises(RuntimeError, match="Need a token"):
|
||||
Authenticator()
|
||||
|
||||
def test_authenticator_initialization_with_empty_token_raises_error(self):
|
||||
"""Test Authenticator initialization with empty token raises RuntimeError"""
|
||||
with pytest.raises(RuntimeError, match="Need a token"):
|
||||
Authenticator(token="")
|
||||
|
||||
def test_permitted_with_allow_all_returns_true(self):
|
||||
"""Test permitted method returns True when allow_all is enabled"""
|
||||
auth = Authenticator(allow_all=True)
|
||||
|
||||
# Should return True regardless of token or roles
|
||||
assert auth.permitted("any-token", []) is True
|
||||
assert auth.permitted("different-token", ["admin"]) is True
|
||||
assert auth.permitted(None, ["user"]) is True
|
||||
|
||||
def test_permitted_with_matching_token_returns_true(self):
|
||||
"""Test permitted method returns True with matching token"""
|
||||
auth = Authenticator(token="secret-token")
|
||||
|
||||
# Should return True when tokens match
|
||||
assert auth.permitted("secret-token", []) is True
|
||||
assert auth.permitted("secret-token", ["admin", "user"]) is True
|
||||
|
||||
def test_permitted_with_non_matching_token_returns_false(self):
|
||||
"""Test permitted method returns False with non-matching token"""
|
||||
auth = Authenticator(token="secret-token")
|
||||
|
||||
# Should return False when tokens don't match
|
||||
assert auth.permitted("wrong-token", []) is False
|
||||
assert auth.permitted("different-token", ["admin"]) is False
|
||||
assert auth.permitted(None, ["user"]) is False
|
||||
|
||||
def test_permitted_with_token_and_allow_all_returns_true(self):
|
||||
"""Test permitted method with both token and allow_all set"""
|
||||
auth = Authenticator(token="test-token", allow_all=True)
|
||||
|
||||
# allow_all should take precedence
|
||||
assert auth.permitted("any-token", []) is True
|
||||
assert auth.permitted("wrong-token", ["admin"]) is True
|
||||
408
tests/unit/test_gateway/test_config_receiver.py
Normal file
408
tests/unit/test_gateway/test_config_receiver.py
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
"""
|
||||
Tests for Gateway Config Receiver
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import Mock, patch, Mock, MagicMock
|
||||
import uuid
|
||||
|
||||
from trustgraph.gateway.config.receiver import ConfigReceiver
|
||||
|
||||
# Save the real method before patching
|
||||
_real_config_loader = ConfigReceiver.config_loader
|
||||
|
||||
# Patch async methods at module level to prevent coroutine warnings
|
||||
ConfigReceiver.config_loader = Mock()
|
||||
|
||||
|
||||
class TestConfigReceiver:
|
||||
"""Test cases for ConfigReceiver class"""
|
||||
|
||||
def test_config_receiver_initialization(self):
|
||||
"""Test ConfigReceiver initialization"""
|
||||
mock_pulsar_client = Mock()
|
||||
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
assert config_receiver.pulsar_client == mock_pulsar_client
|
||||
assert config_receiver.flow_handlers == []
|
||||
assert config_receiver.flows == {}
|
||||
|
||||
def test_add_handler(self):
|
||||
"""Test adding flow handlers"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
handler1 = Mock()
|
||||
handler2 = Mock()
|
||||
|
||||
config_receiver.add_handler(handler1)
|
||||
config_receiver.add_handler(handler2)
|
||||
|
||||
assert len(config_receiver.flow_handlers) == 2
|
||||
assert handler1 in config_receiver.flow_handlers
|
||||
assert handler2 in config_receiver.flow_handlers
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_config_with_new_flows(self):
|
||||
"""Test on_config method with new flows"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Track calls manually instead of using AsyncMock
|
||||
start_flow_calls = []
|
||||
|
||||
async def mock_start_flow(*args):
|
||||
start_flow_calls.append(args)
|
||||
|
||||
config_receiver.start_flow = mock_start_flow
|
||||
|
||||
# Create mock message with flows
|
||||
mock_msg = Mock()
|
||||
mock_msg.value.return_value = Mock(
|
||||
version="1.0",
|
||||
config={
|
||||
"flows": {
|
||||
"flow1": '{"name": "test_flow_1", "steps": []}',
|
||||
"flow2": '{"name": "test_flow_2", "steps": []}'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await config_receiver.on_config(mock_msg, None, None)
|
||||
|
||||
# Verify flows were added
|
||||
assert "flow1" in config_receiver.flows
|
||||
assert "flow2" in config_receiver.flows
|
||||
assert config_receiver.flows["flow1"] == {"name": "test_flow_1", "steps": []}
|
||||
assert config_receiver.flows["flow2"] == {"name": "test_flow_2", "steps": []}
|
||||
|
||||
# Verify start_flow was called for each new flow
|
||||
assert len(start_flow_calls) == 2
|
||||
assert ("flow1", {"name": "test_flow_1", "steps": []}) in start_flow_calls
|
||||
assert ("flow2", {"name": "test_flow_2", "steps": []}) in start_flow_calls
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_config_with_removed_flows(self):
|
||||
"""Test on_config method with removed flows"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Pre-populate with existing flows
|
||||
config_receiver.flows = {
|
||||
"flow1": {"name": "test_flow_1", "steps": []},
|
||||
"flow2": {"name": "test_flow_2", "steps": []}
|
||||
}
|
||||
|
||||
# Track calls manually instead of using AsyncMock
|
||||
stop_flow_calls = []
|
||||
|
||||
async def mock_stop_flow(*args):
|
||||
stop_flow_calls.append(args)
|
||||
|
||||
config_receiver.stop_flow = mock_stop_flow
|
||||
|
||||
# Create mock message with only flow1 (flow2 removed)
|
||||
mock_msg = Mock()
|
||||
mock_msg.value.return_value = Mock(
|
||||
version="1.0",
|
||||
config={
|
||||
"flows": {
|
||||
"flow1": '{"name": "test_flow_1", "steps": []}'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await config_receiver.on_config(mock_msg, None, None)
|
||||
|
||||
# Verify flow2 was removed
|
||||
assert "flow1" in config_receiver.flows
|
||||
assert "flow2" not in config_receiver.flows
|
||||
|
||||
# Verify stop_flow was called for removed flow
|
||||
assert len(stop_flow_calls) == 1
|
||||
assert stop_flow_calls[0] == ("flow2", {"name": "test_flow_2", "steps": []})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_config_with_no_flows(self):
|
||||
"""Test on_config method with no flows in config"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Mock the start_flow and stop_flow methods with async functions
|
||||
async def mock_start_flow(*args):
|
||||
pass
|
||||
async def mock_stop_flow(*args):
|
||||
pass
|
||||
config_receiver.start_flow = mock_start_flow
|
||||
config_receiver.stop_flow = mock_stop_flow
|
||||
|
||||
# Create mock message without flows
|
||||
mock_msg = Mock()
|
||||
mock_msg.value.return_value = Mock(
|
||||
version="1.0",
|
||||
config={}
|
||||
)
|
||||
|
||||
await config_receiver.on_config(mock_msg, None, None)
|
||||
|
||||
# Verify no flows were added
|
||||
assert config_receiver.flows == {}
|
||||
|
||||
# Since no flows were in the config, the flow methods shouldn't be called
|
||||
# (We can't easily assert this with simple async functions, but the test
|
||||
# passes if no exceptions are thrown)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_config_exception_handling(self):
|
||||
"""Test on_config method handles exceptions gracefully"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Create mock message that will cause an exception
|
||||
mock_msg = Mock()
|
||||
mock_msg.value.side_effect = Exception("Test exception")
|
||||
|
||||
# This should not raise an exception
|
||||
await config_receiver.on_config(mock_msg, None, None)
|
||||
|
||||
# Verify flows remain empty
|
||||
assert config_receiver.flows == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_flow_with_handlers(self):
|
||||
"""Test start_flow method with multiple handlers"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Add mock handlers
|
||||
handler1 = Mock()
|
||||
handler1.start_flow = Mock()
|
||||
handler2 = Mock()
|
||||
handler2.start_flow = Mock()
|
||||
|
||||
config_receiver.add_handler(handler1)
|
||||
config_receiver.add_handler(handler2)
|
||||
|
||||
flow_data = {"name": "test_flow", "steps": []}
|
||||
|
||||
await config_receiver.start_flow("flow1", flow_data)
|
||||
|
||||
# Verify all handlers were called
|
||||
handler1.start_flow.assert_called_once_with("flow1", flow_data)
|
||||
handler2.start_flow.assert_called_once_with("flow1", flow_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_flow_with_handler_exception(self):
|
||||
"""Test start_flow method handles handler exceptions"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Add mock handler that raises exception
|
||||
handler = Mock()
|
||||
handler.start_flow = Mock(side_effect=Exception("Handler error"))
|
||||
|
||||
config_receiver.add_handler(handler)
|
||||
|
||||
flow_data = {"name": "test_flow", "steps": []}
|
||||
|
||||
# This should not raise an exception
|
||||
await config_receiver.start_flow("flow1", flow_data)
|
||||
|
||||
# Verify handler was called
|
||||
handler.start_flow.assert_called_once_with("flow1", flow_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_flow_with_handlers(self):
|
||||
"""Test stop_flow method with multiple handlers"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Add mock handlers
|
||||
handler1 = Mock()
|
||||
handler1.stop_flow = Mock()
|
||||
handler2 = Mock()
|
||||
handler2.stop_flow = Mock()
|
||||
|
||||
config_receiver.add_handler(handler1)
|
||||
config_receiver.add_handler(handler2)
|
||||
|
||||
flow_data = {"name": "test_flow", "steps": []}
|
||||
|
||||
await config_receiver.stop_flow("flow1", flow_data)
|
||||
|
||||
# Verify all handlers were called
|
||||
handler1.stop_flow.assert_called_once_with("flow1", flow_data)
|
||||
handler2.stop_flow.assert_called_once_with("flow1", flow_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_flow_with_handler_exception(self):
|
||||
"""Test stop_flow method handles handler exceptions"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Add mock handler that raises exception
|
||||
handler = Mock()
|
||||
handler.stop_flow = Mock(side_effect=Exception("Handler error"))
|
||||
|
||||
config_receiver.add_handler(handler)
|
||||
|
||||
flow_data = {"name": "test_flow", "steps": []}
|
||||
|
||||
# This should not raise an exception
|
||||
await config_receiver.stop_flow("flow1", flow_data)
|
||||
|
||||
# Verify handler was called
|
||||
handler.stop_flow.assert_called_once_with("flow1", flow_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_loader_creates_consumer(self):
|
||||
"""Test config_loader method creates Pulsar consumer"""
|
||||
mock_pulsar_client = Mock()
|
||||
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
# Temporarily restore the real config_loader for this test
|
||||
config_receiver.config_loader = _real_config_loader.__get__(config_receiver)
|
||||
|
||||
# Mock Consumer class
|
||||
with patch('trustgraph.gateway.config.receiver.Consumer') as mock_consumer_class, \
|
||||
patch('uuid.uuid4') as mock_uuid:
|
||||
|
||||
mock_uuid.return_value = "test-uuid"
|
||||
mock_consumer = Mock()
|
||||
async def mock_start():
|
||||
pass
|
||||
mock_consumer.start = mock_start
|
||||
mock_consumer_class.return_value = mock_consumer
|
||||
|
||||
# Create a task that will complete quickly
|
||||
async def quick_task():
|
||||
await config_receiver.config_loader()
|
||||
|
||||
# Run the task with a timeout to prevent hanging
|
||||
try:
|
||||
await asyncio.wait_for(quick_task(), timeout=0.1)
|
||||
except asyncio.TimeoutError:
|
||||
# This is expected since the method runs indefinitely
|
||||
pass
|
||||
|
||||
# Verify Consumer was created with correct parameters
|
||||
mock_consumer_class.assert_called_once()
|
||||
call_args = mock_consumer_class.call_args
|
||||
|
||||
assert call_args[1]['client'] == mock_pulsar_client
|
||||
assert call_args[1]['subscriber'] == "gateway-test-uuid"
|
||||
assert call_args[1]['handler'] == config_receiver.on_config
|
||||
assert call_args[1]['start_of_messages'] is True
|
||||
|
||||
@patch('asyncio.create_task')
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_creates_config_loader_task(self, mock_create_task):
|
||||
"""Test start method creates config loader task"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Mock create_task to avoid actually creating tasks with real coroutines
|
||||
mock_task = Mock()
|
||||
mock_create_task.return_value = mock_task
|
||||
|
||||
await config_receiver.start()
|
||||
|
||||
# Verify task was created
|
||||
mock_create_task.assert_called_once()
|
||||
|
||||
# Verify the argument passed to create_task is a coroutine
|
||||
call_args = mock_create_task.call_args[0]
|
||||
assert len(call_args) == 1 # Should have one argument (the coroutine)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_config_mixed_flow_operations(self):
|
||||
"""Test on_config with mixed add/remove operations"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Pre-populate with existing flows
|
||||
config_receiver.flows = {
|
||||
"flow1": {"name": "test_flow_1", "steps": []},
|
||||
"flow2": {"name": "test_flow_2", "steps": []}
|
||||
}
|
||||
|
||||
# Track calls manually instead of using Mock
|
||||
start_flow_calls = []
|
||||
stop_flow_calls = []
|
||||
|
||||
async def mock_start_flow(*args):
|
||||
start_flow_calls.append(args)
|
||||
|
||||
async def mock_stop_flow(*args):
|
||||
stop_flow_calls.append(args)
|
||||
|
||||
# Directly assign to avoid patch.object detecting async methods
|
||||
original_start_flow = config_receiver.start_flow
|
||||
original_stop_flow = config_receiver.stop_flow
|
||||
config_receiver.start_flow = mock_start_flow
|
||||
config_receiver.stop_flow = mock_stop_flow
|
||||
|
||||
try:
|
||||
|
||||
# Create mock message with flow1 removed and flow3 added
|
||||
mock_msg = Mock()
|
||||
mock_msg.value.return_value = Mock(
|
||||
version="1.0",
|
||||
config={
|
||||
"flows": {
|
||||
"flow2": '{"name": "test_flow_2", "steps": []}',
|
||||
"flow3": '{"name": "test_flow_3", "steps": []}'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await config_receiver.on_config(mock_msg, None, None)
|
||||
|
||||
# Verify final state
|
||||
assert "flow1" not in config_receiver.flows
|
||||
assert "flow2" in config_receiver.flows
|
||||
assert "flow3" in config_receiver.flows
|
||||
|
||||
# Verify operations
|
||||
assert len(start_flow_calls) == 1
|
||||
assert start_flow_calls[0] == ("flow3", {"name": "test_flow_3", "steps": []})
|
||||
assert len(stop_flow_calls) == 1
|
||||
assert stop_flow_calls[0] == ("flow1", {"name": "test_flow_1", "steps": []})
|
||||
|
||||
finally:
|
||||
# Restore original methods
|
||||
config_receiver.start_flow = original_start_flow
|
||||
config_receiver.stop_flow = original_stop_flow
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_config_invalid_json_flow_data(self):
|
||||
"""Test on_config handles invalid JSON in flow data"""
|
||||
mock_pulsar_client = Mock()
|
||||
config_receiver = ConfigReceiver(mock_pulsar_client)
|
||||
|
||||
# Mock the start_flow method with an async function
|
||||
async def mock_start_flow(*args):
|
||||
pass
|
||||
config_receiver.start_flow = mock_start_flow
|
||||
|
||||
# Create mock message with invalid JSON
|
||||
mock_msg = Mock()
|
||||
mock_msg.value.return_value = Mock(
|
||||
version="1.0",
|
||||
config={
|
||||
"flows": {
|
||||
"flow1": '{"invalid": json}', # Invalid JSON
|
||||
"flow2": '{"name": "valid_flow", "steps": []}' # Valid JSON
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# This should handle the exception gracefully
|
||||
await config_receiver.on_config(mock_msg, None, None)
|
||||
|
||||
# The entire operation should fail due to JSON parsing error
|
||||
# So no flows should be added
|
||||
assert config_receiver.flows == {}
|
||||
93
tests/unit/test_gateway/test_dispatch_config.py
Normal file
93
tests/unit/test_gateway/test_dispatch_config.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Tests for Gateway Config Dispatch
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, AsyncMock, Mock
|
||||
|
||||
from trustgraph.gateway.dispatch.config import ConfigRequestor
|
||||
|
||||
# Import parent class for local patching
|
||||
from trustgraph.gateway.dispatch.requestor import ServiceRequestor
|
||||
|
||||
|
||||
class TestConfigRequestor:
|
||||
"""Test cases for ConfigRequestor class"""
|
||||
|
||||
@patch('trustgraph.gateway.dispatch.config.TranslatorRegistry')
|
||||
def test_config_requestor_initialization(self, mock_translator_registry):
|
||||
"""Test ConfigRequestor initialization"""
|
||||
# Mock translators
|
||||
mock_request_translator = Mock()
|
||||
mock_response_translator = Mock()
|
||||
mock_translator_registry.get_request_translator.return_value = mock_request_translator
|
||||
mock_translator_registry.get_response_translator.return_value = mock_response_translator
|
||||
|
||||
# Mock dependencies
|
||||
mock_pulsar_client = Mock()
|
||||
|
||||
requestor = ConfigRequestor(
|
||||
pulsar_client=mock_pulsar_client,
|
||||
consumer="test-consumer",
|
||||
subscriber="test-subscriber",
|
||||
timeout=60
|
||||
)
|
||||
|
||||
# Verify translator setup
|
||||
mock_translator_registry.get_request_translator.assert_called_once_with("config")
|
||||
mock_translator_registry.get_response_translator.assert_called_once_with("config")
|
||||
|
||||
assert requestor.request_translator == mock_request_translator
|
||||
assert requestor.response_translator == mock_response_translator
|
||||
|
||||
@patch('trustgraph.gateway.dispatch.config.TranslatorRegistry')
|
||||
def test_config_requestor_to_request(self, mock_translator_registry):
|
||||
"""Test ConfigRequestor to_request method"""
|
||||
# Mock translators
|
||||
mock_request_translator = Mock()
|
||||
mock_translator_registry.get_request_translator.return_value = mock_request_translator
|
||||
mock_translator_registry.get_response_translator.return_value = Mock()
|
||||
|
||||
# Setup translator response
|
||||
mock_request_translator.to_pulsar.return_value = "translated_request"
|
||||
|
||||
# Patch ServiceRequestor async methods with regular mocks (not AsyncMock)
|
||||
with patch.object(ServiceRequestor, 'start', return_value=None), \
|
||||
patch.object(ServiceRequestor, 'process', return_value=None):
|
||||
requestor = ConfigRequestor(
|
||||
pulsar_client=Mock(),
|
||||
consumer="test-consumer",
|
||||
subscriber="test-subscriber"
|
||||
)
|
||||
|
||||
# Call to_request
|
||||
result = requestor.to_request({"test": "body"})
|
||||
|
||||
# Verify translator was called correctly
|
||||
mock_request_translator.to_pulsar.assert_called_once_with({"test": "body"})
|
||||
assert result == "translated_request"
|
||||
|
||||
@patch('trustgraph.gateway.dispatch.config.TranslatorRegistry')
|
||||
def test_config_requestor_from_response(self, mock_translator_registry):
|
||||
"""Test ConfigRequestor from_response method"""
|
||||
# Mock translators
|
||||
mock_response_translator = Mock()
|
||||
mock_translator_registry.get_request_translator.return_value = Mock()
|
||||
mock_translator_registry.get_response_translator.return_value = mock_response_translator
|
||||
|
||||
# Setup translator response
|
||||
mock_response_translator.from_response_with_completion.return_value = "translated_response"
|
||||
|
||||
requestor = ConfigRequestor(
|
||||
pulsar_client=Mock(),
|
||||
consumer="test-consumer",
|
||||
subscriber="test-subscriber"
|
||||
)
|
||||
|
||||
# Call from_response
|
||||
mock_message = Mock()
|
||||
result = requestor.from_response(mock_message)
|
||||
|
||||
# Verify translator was called correctly
|
||||
mock_response_translator.from_response_with_completion.assert_called_once_with(mock_message)
|
||||
assert result == "translated_response"
|
||||
558
tests/unit/test_gateway/test_dispatch_manager.py
Normal file
558
tests/unit/test_gateway/test_dispatch_manager.py
Normal file
|
|
@ -0,0 +1,558 @@
|
|||
"""
|
||||
Tests for Gateway Dispatcher Manager
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
||||
import uuid
|
||||
|
||||
from trustgraph.gateway.dispatch.manager import DispatcherManager, DispatcherWrapper
|
||||
|
||||
# Keep the real methods intact for proper testing
|
||||
|
||||
|
||||
class TestDispatcherWrapper:
|
||||
"""Test cases for DispatcherWrapper class"""
|
||||
|
||||
def test_dispatcher_wrapper_initialization(self):
|
||||
"""Test DispatcherWrapper initialization"""
|
||||
mock_handler = Mock()
|
||||
wrapper = DispatcherWrapper(mock_handler)
|
||||
|
||||
assert wrapper.handler == mock_handler
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatcher_wrapper_process(self):
|
||||
"""Test DispatcherWrapper process method"""
|
||||
mock_handler = AsyncMock()
|
||||
wrapper = DispatcherWrapper(mock_handler)
|
||||
|
||||
result = await wrapper.process("arg1", "arg2")
|
||||
|
||||
mock_handler.assert_called_once_with("arg1", "arg2")
|
||||
assert result == mock_handler.return_value
|
||||
|
||||
|
||||
class TestDispatcherManager:
|
||||
"""Test cases for DispatcherManager class"""
|
||||
|
||||
def test_dispatcher_manager_initialization(self):
|
||||
"""Test DispatcherManager initialization"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
assert manager.pulsar_client == mock_pulsar_client
|
||||
assert manager.config_receiver == mock_config_receiver
|
||||
assert manager.prefix == "api-gateway" # default prefix
|
||||
assert manager.flows == {}
|
||||
assert manager.dispatchers == {}
|
||||
|
||||
# Verify manager was added as handler to config receiver
|
||||
mock_config_receiver.add_handler.assert_called_once_with(manager)
|
||||
|
||||
def test_dispatcher_manager_initialization_with_custom_prefix(self):
|
||||
"""Test DispatcherManager initialization with custom prefix"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver, prefix="custom-prefix")
|
||||
|
||||
assert manager.prefix == "custom-prefix"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_flow(self):
|
||||
"""Test start_flow method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
flow_data = {"name": "test_flow", "steps": []}
|
||||
|
||||
await manager.start_flow("flow1", flow_data)
|
||||
|
||||
assert "flow1" in manager.flows
|
||||
assert manager.flows["flow1"] == flow_data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_flow(self):
|
||||
"""Test stop_flow method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Pre-populate with a flow
|
||||
flow_data = {"name": "test_flow", "steps": []}
|
||||
manager.flows["flow1"] = flow_data
|
||||
|
||||
await manager.stop_flow("flow1", flow_data)
|
||||
|
||||
assert "flow1" not in manager.flows
|
||||
|
||||
def test_dispatch_global_service_returns_wrapper(self):
|
||||
"""Test dispatch_global_service returns DispatcherWrapper"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
wrapper = manager.dispatch_global_service()
|
||||
|
||||
assert isinstance(wrapper, DispatcherWrapper)
|
||||
assert wrapper.handler == manager.process_global_service
|
||||
|
||||
def test_dispatch_core_export_returns_wrapper(self):
|
||||
"""Test dispatch_core_export returns DispatcherWrapper"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
wrapper = manager.dispatch_core_export()
|
||||
|
||||
assert isinstance(wrapper, DispatcherWrapper)
|
||||
assert wrapper.handler == manager.process_core_export
|
||||
|
||||
def test_dispatch_core_import_returns_wrapper(self):
|
||||
"""Test dispatch_core_import returns DispatcherWrapper"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
wrapper = manager.dispatch_core_import()
|
||||
|
||||
assert isinstance(wrapper, DispatcherWrapper)
|
||||
assert wrapper.handler == manager.process_core_import
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_core_import(self):
|
||||
"""Test process_core_import method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.CoreImport') as mock_core_import:
|
||||
mock_importer = Mock()
|
||||
mock_importer.process = AsyncMock(return_value="import_result")
|
||||
mock_core_import.return_value = mock_importer
|
||||
|
||||
result = await manager.process_core_import("data", "error", "ok", "request")
|
||||
|
||||
mock_core_import.assert_called_once_with(mock_pulsar_client)
|
||||
mock_importer.process.assert_called_once_with("data", "error", "ok", "request")
|
||||
assert result == "import_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_core_export(self):
|
||||
"""Test process_core_export method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.CoreExport') as mock_core_export:
|
||||
mock_exporter = Mock()
|
||||
mock_exporter.process = AsyncMock(return_value="export_result")
|
||||
mock_core_export.return_value = mock_exporter
|
||||
|
||||
result = await manager.process_core_export("data", "error", "ok", "request")
|
||||
|
||||
mock_core_export.assert_called_once_with(mock_pulsar_client)
|
||||
mock_exporter.process.assert_called_once_with("data", "error", "ok", "request")
|
||||
assert result == "export_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_global_service(self):
|
||||
"""Test process_global_service method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
manager.invoke_global_service = AsyncMock(return_value="global_result")
|
||||
|
||||
params = {"kind": "test_kind"}
|
||||
result = await manager.process_global_service("data", "responder", params)
|
||||
|
||||
manager.invoke_global_service.assert_called_once_with("data", "responder", "test_kind")
|
||||
assert result == "global_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_global_service_with_existing_dispatcher(self):
|
||||
"""Test invoke_global_service with existing dispatcher"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Pre-populate with existing dispatcher
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher.process = AsyncMock(return_value="cached_result")
|
||||
manager.dispatchers[(None, "config")] = mock_dispatcher
|
||||
|
||||
result = await manager.invoke_global_service("data", "responder", "config")
|
||||
|
||||
mock_dispatcher.process.assert_called_once_with("data", "responder")
|
||||
assert result == "cached_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_global_service_creates_new_dispatcher(self):
|
||||
"""Test invoke_global_service creates new dispatcher"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.global_dispatchers') as mock_dispatchers:
|
||||
mock_dispatcher_class = Mock()
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher.start = AsyncMock()
|
||||
mock_dispatcher.process = AsyncMock(return_value="new_result")
|
||||
mock_dispatcher_class.return_value = mock_dispatcher
|
||||
mock_dispatchers.__getitem__.return_value = mock_dispatcher_class
|
||||
|
||||
result = await manager.invoke_global_service("data", "responder", "config")
|
||||
|
||||
# Verify dispatcher was created with correct parameters
|
||||
mock_dispatcher_class.assert_called_once_with(
|
||||
pulsar_client=mock_pulsar_client,
|
||||
timeout=120,
|
||||
consumer="api-gateway-config-request",
|
||||
subscriber="api-gateway-config-request"
|
||||
)
|
||||
mock_dispatcher.start.assert_called_once()
|
||||
mock_dispatcher.process.assert_called_once_with("data", "responder")
|
||||
|
||||
# Verify dispatcher was cached
|
||||
assert manager.dispatchers[(None, "config")] == mock_dispatcher
|
||||
assert result == "new_result"
|
||||
|
||||
def test_dispatch_flow_import_returns_method(self):
|
||||
"""Test dispatch_flow_import returns correct method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
result = manager.dispatch_flow_import()
|
||||
|
||||
assert result == manager.process_flow_import
|
||||
|
||||
def test_dispatch_flow_export_returns_method(self):
|
||||
"""Test dispatch_flow_export returns correct method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
result = manager.dispatch_flow_export()
|
||||
|
||||
assert result == manager.process_flow_export
|
||||
|
||||
def test_dispatch_socket_returns_method(self):
|
||||
"""Test dispatch_socket returns correct method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
result = manager.dispatch_socket()
|
||||
|
||||
assert result == manager.process_socket
|
||||
|
||||
def test_dispatch_flow_service_returns_wrapper(self):
|
||||
"""Test dispatch_flow_service returns DispatcherWrapper"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
wrapper = manager.dispatch_flow_service()
|
||||
|
||||
assert isinstance(wrapper, DispatcherWrapper)
|
||||
assert wrapper.handler == manager.process_flow_service
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_flow_import_with_valid_flow_and_kind(self):
|
||||
"""Test process_flow_import with valid flow and kind"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"triples-store": {"queue": "test_queue"}
|
||||
}
|
||||
}
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.import_dispatchers') as mock_dispatchers, \
|
||||
patch('uuid.uuid4') as mock_uuid:
|
||||
mock_uuid.return_value = "test-uuid"
|
||||
mock_dispatcher_class = Mock()
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher.start = AsyncMock()
|
||||
mock_dispatcher_class.return_value = mock_dispatcher
|
||||
mock_dispatchers.__getitem__.return_value = mock_dispatcher_class
|
||||
mock_dispatchers.__contains__.return_value = True
|
||||
|
||||
params = {"flow": "test_flow", "kind": "triples"}
|
||||
result = await manager.process_flow_import("ws", "running", params)
|
||||
|
||||
mock_dispatcher_class.assert_called_once_with(
|
||||
pulsar_client=mock_pulsar_client,
|
||||
ws="ws",
|
||||
running="running",
|
||||
queue={"queue": "test_queue"}
|
||||
)
|
||||
mock_dispatcher.start.assert_called_once()
|
||||
assert result == mock_dispatcher
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_flow_import_with_invalid_flow(self):
|
||||
"""Test process_flow_import with invalid flow"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
params = {"flow": "invalid_flow", "kind": "triples"}
|
||||
|
||||
with pytest.raises(RuntimeError, match="Invalid flow"):
|
||||
await manager.process_flow_import("ws", "running", params)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_flow_import_with_invalid_kind(self):
|
||||
"""Test process_flow_import with invalid kind"""
|
||||
import warnings
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", RuntimeWarning)
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"triples-store": {"queue": "test_queue"}
|
||||
}
|
||||
}
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.import_dispatchers') as mock_dispatchers:
|
||||
mock_dispatchers.__contains__.return_value = False
|
||||
|
||||
params = {"flow": "test_flow", "kind": "invalid_kind"}
|
||||
|
||||
with pytest.raises(RuntimeError, match="Invalid kind"):
|
||||
await manager.process_flow_import("ws", "running", params)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_flow_export_with_valid_flow_and_kind(self):
|
||||
"""Test process_flow_export with valid flow and kind"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"triples-store": {"queue": "test_queue"}
|
||||
}
|
||||
}
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.export_dispatchers') as mock_dispatchers, \
|
||||
patch('uuid.uuid4') as mock_uuid:
|
||||
mock_uuid.return_value = "test-uuid"
|
||||
mock_dispatcher_class = Mock()
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher_class.return_value = mock_dispatcher
|
||||
mock_dispatchers.__getitem__.return_value = mock_dispatcher_class
|
||||
mock_dispatchers.__contains__.return_value = True
|
||||
|
||||
params = {"flow": "test_flow", "kind": "triples"}
|
||||
result = await manager.process_flow_export("ws", "running", params)
|
||||
|
||||
mock_dispatcher_class.assert_called_once_with(
|
||||
pulsar_client=mock_pulsar_client,
|
||||
ws="ws",
|
||||
running="running",
|
||||
queue={"queue": "test_queue"},
|
||||
consumer="api-gateway-test-uuid",
|
||||
subscriber="api-gateway-test-uuid"
|
||||
)
|
||||
assert result == mock_dispatcher
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_socket(self):
|
||||
"""Test process_socket method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.Mux') as mock_mux:
|
||||
mock_mux_instance = Mock()
|
||||
mock_mux.return_value = mock_mux_instance
|
||||
|
||||
result = await manager.process_socket("ws", "running", {})
|
||||
|
||||
mock_mux.assert_called_once_with(manager, "ws", "running")
|
||||
assert result == mock_mux_instance
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_flow_service(self):
|
||||
"""Test process_flow_service method"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
manager.invoke_flow_service = AsyncMock(return_value="flow_result")
|
||||
|
||||
params = {"flow": "test_flow", "kind": "agent"}
|
||||
result = await manager.process_flow_service("data", "responder", params)
|
||||
|
||||
manager.invoke_flow_service.assert_called_once_with("data", "responder", "test_flow", "agent")
|
||||
assert result == "flow_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_flow_service_with_existing_dispatcher(self):
|
||||
"""Test invoke_flow_service with existing dispatcher"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Add flow to the flows dictionary
|
||||
manager.flows["test_flow"] = {"services": {"agent": {}}}
|
||||
|
||||
# Pre-populate with existing dispatcher
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher.process = AsyncMock(return_value="cached_result")
|
||||
manager.dispatchers[("test_flow", "agent")] = mock_dispatcher
|
||||
|
||||
result = await manager.invoke_flow_service("data", "responder", "test_flow", "agent")
|
||||
|
||||
mock_dispatcher.process.assert_called_once_with("data", "responder")
|
||||
assert result == "cached_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_flow_service_creates_request_response_dispatcher(self):
|
||||
"""Test invoke_flow_service creates request-response dispatcher"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"agent": {
|
||||
"request": "agent_request_queue",
|
||||
"response": "agent_response_queue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.request_response_dispatchers') as mock_dispatchers:
|
||||
mock_dispatcher_class = Mock()
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher.start = AsyncMock()
|
||||
mock_dispatcher.process = AsyncMock(return_value="new_result")
|
||||
mock_dispatcher_class.return_value = mock_dispatcher
|
||||
mock_dispatchers.__getitem__.return_value = mock_dispatcher_class
|
||||
mock_dispatchers.__contains__.return_value = True
|
||||
|
||||
result = await manager.invoke_flow_service("data", "responder", "test_flow", "agent")
|
||||
|
||||
# Verify dispatcher was created with correct parameters
|
||||
mock_dispatcher_class.assert_called_once_with(
|
||||
pulsar_client=mock_pulsar_client,
|
||||
request_queue="agent_request_queue",
|
||||
response_queue="agent_response_queue",
|
||||
timeout=120,
|
||||
consumer="api-gateway-test_flow-agent-request",
|
||||
subscriber="api-gateway-test_flow-agent-request"
|
||||
)
|
||||
mock_dispatcher.start.assert_called_once()
|
||||
mock_dispatcher.process.assert_called_once_with("data", "responder")
|
||||
|
||||
# Verify dispatcher was cached
|
||||
assert manager.dispatchers[("test_flow", "agent")] == mock_dispatcher
|
||||
assert result == "new_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_flow_service_creates_sender_dispatcher(self):
|
||||
"""Test invoke_flow_service creates sender dispatcher"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"text-load": {"queue": "text_load_queue"}
|
||||
}
|
||||
}
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.request_response_dispatchers') as mock_rr_dispatchers, \
|
||||
patch('trustgraph.gateway.dispatch.manager.sender_dispatchers') as mock_sender_dispatchers:
|
||||
mock_rr_dispatchers.__contains__.return_value = False
|
||||
mock_sender_dispatchers.__contains__.return_value = True
|
||||
|
||||
mock_dispatcher_class = Mock()
|
||||
mock_dispatcher = Mock()
|
||||
mock_dispatcher.start = AsyncMock()
|
||||
mock_dispatcher.process = AsyncMock(return_value="sender_result")
|
||||
mock_dispatcher_class.return_value = mock_dispatcher
|
||||
mock_sender_dispatchers.__getitem__.return_value = mock_dispatcher_class
|
||||
|
||||
result = await manager.invoke_flow_service("data", "responder", "test_flow", "text-load")
|
||||
|
||||
# Verify dispatcher was created with correct parameters
|
||||
mock_dispatcher_class.assert_called_once_with(
|
||||
pulsar_client=mock_pulsar_client,
|
||||
queue={"queue": "text_load_queue"}
|
||||
)
|
||||
mock_dispatcher.start.assert_called_once()
|
||||
mock_dispatcher.process.assert_called_once_with("data", "responder")
|
||||
|
||||
# Verify dispatcher was cached
|
||||
assert manager.dispatchers[("test_flow", "text-load")] == mock_dispatcher
|
||||
assert result == "sender_result"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_flow_service_invalid_flow(self):
|
||||
"""Test invoke_flow_service with invalid flow"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Invalid flow"):
|
||||
await manager.invoke_flow_service("data", "responder", "invalid_flow", "agent")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_flow_service_unsupported_kind_by_flow(self):
|
||||
"""Test invoke_flow_service with kind not supported by flow"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow without agent interface
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"text-completion": {"request": "req", "response": "resp"}
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(RuntimeError, match="This kind not supported by flow"):
|
||||
await manager.invoke_flow_service("data", "responder", "test_flow", "agent")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invoke_flow_service_invalid_kind(self):
|
||||
"""Test invoke_flow_service with invalid kind"""
|
||||
mock_pulsar_client = Mock()
|
||||
mock_config_receiver = Mock()
|
||||
manager = DispatcherManager(mock_pulsar_client, mock_config_receiver)
|
||||
|
||||
# Setup test flow with interface but unsupported kind
|
||||
manager.flows["test_flow"] = {
|
||||
"interfaces": {
|
||||
"invalid-kind": {"request": "req", "response": "resp"}
|
||||
}
|
||||
}
|
||||
|
||||
with patch('trustgraph.gateway.dispatch.manager.request_response_dispatchers') as mock_rr_dispatchers, \
|
||||
patch('trustgraph.gateway.dispatch.manager.sender_dispatchers') as mock_sender_dispatchers:
|
||||
mock_rr_dispatchers.__contains__.return_value = False
|
||||
mock_sender_dispatchers.__contains__.return_value = False
|
||||
|
||||
with pytest.raises(RuntimeError, match="Invalid kind"):
|
||||
await manager.invoke_flow_service("data", "responder", "test_flow", "invalid-kind")
|
||||
171
tests/unit/test_gateway/test_dispatch_mux.py
Normal file
171
tests/unit/test_gateway/test_dispatch_mux.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"""
|
||||
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 = AsyncMock()
|
||||
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",
|
||||
"service": "test-service"
|
||||
}
|
||||
|
||||
# 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 = AsyncMock()
|
||||
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)
|
||||
|
||||
mock_ws.send_json.assert_called_once_with({"error": "Bad message"})
|
||||
|
||||
@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 = AsyncMock()
|
||||
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)
|
||||
|
||||
mock_ws.send_json.assert_called_once_with({"error": "Bad message"})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mux_receive_invalid_json(self):
|
||||
"""Test Mux receive method with invalid JSON"""
|
||||
mock_dispatcher_manager = MagicMock()
|
||||
mock_ws = AsyncMock()
|
||||
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()
|
||||
mock_ws.send_json.assert_called_once_with({"error": "Invalid JSON"})
|
||||
118
tests/unit/test_gateway/test_dispatch_requestor.py
Normal file
118
tests/unit/test_gateway/test_dispatch_requestor.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""
|
||||
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_pub_instance = AsyncMock()
|
||||
mock_subscriber.return_value = mock_sub_instance
|
||||
mock_publisher.return_value = mock_pub_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 both subscriber and publisher start were called
|
||||
mock_sub_instance.start.assert_called_once()
|
||||
mock_pub_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 = AsyncMock()
|
||||
mock_sub_instance = AsyncMock()
|
||||
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
|
||||
120
tests/unit/test_gateway/test_dispatch_sender.py
Normal file
120
tests/unit/test_gateway/test_dispatch_sender.py
Normal file
|
|
@ -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
|
||||
89
tests/unit/test_gateway/test_dispatch_serialize.py
Normal file
89
tests/unit/test_gateway/test_dispatch_serialize.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
Tests for Gateway Dispatch Serialization
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from trustgraph.gateway.dispatch.serialize import to_value, to_subgraph, serialize_value
|
||||
from trustgraph.schema import Value, Triple
|
||||
|
||||
|
||||
class TestDispatchSerialize:
|
||||
"""Test cases for dispatch serialization functions"""
|
||||
|
||||
def test_to_value_with_uri(self):
|
||||
"""Test to_value function with URI"""
|
||||
input_data = {"v": "http://example.com/resource", "e": True}
|
||||
|
||||
result = to_value(input_data)
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "http://example.com/resource"
|
||||
assert result.is_uri is True
|
||||
|
||||
def test_to_value_with_literal(self):
|
||||
"""Test to_value function with literal value"""
|
||||
input_data = {"v": "literal string", "e": False}
|
||||
|
||||
result = to_value(input_data)
|
||||
|
||||
assert isinstance(result, Value)
|
||||
assert result.value == "literal string"
|
||||
assert result.is_uri is False
|
||||
|
||||
def test_to_subgraph_with_multiple_triples(self):
|
||||
"""Test to_subgraph function with multiple triples"""
|
||||
input_data = [
|
||||
{
|
||||
"s": {"v": "subject1", "e": True},
|
||||
"p": {"v": "predicate1", "e": True},
|
||||
"o": {"v": "object1", "e": False}
|
||||
},
|
||||
{
|
||||
"s": {"v": "subject2", "e": False},
|
||||
"p": {"v": "predicate2", "e": True},
|
||||
"o": {"v": "object2", "e": True}
|
||||
}
|
||||
]
|
||||
|
||||
result = to_subgraph(input_data)
|
||||
|
||||
assert len(result) == 2
|
||||
assert all(isinstance(triple, Triple) for triple in result)
|
||||
|
||||
# Check first triple
|
||||
assert result[0].s.value == "subject1"
|
||||
assert result[0].s.is_uri is True
|
||||
assert result[0].p.value == "predicate1"
|
||||
assert result[0].p.is_uri is True
|
||||
assert result[0].o.value == "object1"
|
||||
assert result[0].o.is_uri is False
|
||||
|
||||
# Check second triple
|
||||
assert result[1].s.value == "subject2"
|
||||
assert result[1].s.is_uri is False
|
||||
|
||||
def test_to_subgraph_with_empty_list(self):
|
||||
"""Test to_subgraph function with empty input"""
|
||||
input_data = []
|
||||
|
||||
result = to_subgraph(input_data)
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_serialize_value_with_uri(self):
|
||||
"""Test serialize_value function with URI value"""
|
||||
value = Value(value="http://example.com/test", is_uri=True)
|
||||
|
||||
result = serialize_value(value)
|
||||
|
||||
assert result == {"v": "http://example.com/test", "e": True}
|
||||
|
||||
def test_serialize_value_with_literal(self):
|
||||
"""Test serialize_value function with literal value"""
|
||||
value = Value(value="test literal", is_uri=False)
|
||||
|
||||
result = serialize_value(value)
|
||||
|
||||
assert result == {"v": "test literal", "e": False}
|
||||
55
tests/unit/test_gateway/test_endpoint_constant.py
Normal file
55
tests/unit/test_gateway/test_endpoint_constant.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
Tests for Gateway Constant Endpoint
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
from aiohttp import web
|
||||
|
||||
from trustgraph.gateway.endpoint.constant_endpoint import ConstantEndpoint
|
||||
|
||||
|
||||
class TestConstantEndpoint:
|
||||
"""Test cases for ConstantEndpoint class"""
|
||||
|
||||
def test_constant_endpoint_initialization(self):
|
||||
"""Test ConstantEndpoint initialization"""
|
||||
mock_auth = MagicMock()
|
||||
mock_dispatcher = MagicMock()
|
||||
|
||||
endpoint = ConstantEndpoint(
|
||||
endpoint_path="/api/test",
|
||||
auth=mock_auth,
|
||||
dispatcher=mock_dispatcher
|
||||
)
|
||||
|
||||
assert endpoint.path == "/api/test"
|
||||
assert endpoint.auth == mock_auth
|
||||
assert endpoint.dispatcher == mock_dispatcher
|
||||
assert endpoint.operation == "service"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constant_endpoint_start_method(self):
|
||||
"""Test ConstantEndpoint start method (should be no-op)"""
|
||||
mock_auth = MagicMock()
|
||||
mock_dispatcher = MagicMock()
|
||||
|
||||
endpoint = ConstantEndpoint("/api/test", mock_auth, mock_dispatcher)
|
||||
|
||||
# start() should complete without error
|
||||
await endpoint.start()
|
||||
|
||||
def test_add_routes_registers_post_handler(self):
|
||||
"""Test add_routes method registers POST route"""
|
||||
mock_auth = MagicMock()
|
||||
mock_dispatcher = MagicMock()
|
||||
mock_app = MagicMock()
|
||||
|
||||
endpoint = ConstantEndpoint("/api/test", mock_auth, mock_dispatcher)
|
||||
endpoint.add_routes(mock_app)
|
||||
|
||||
# Verify add_routes was called with POST route
|
||||
mock_app.add_routes.assert_called_once()
|
||||
# The call should include web.post with the path and handler
|
||||
call_args = mock_app.add_routes.call_args[0][0]
|
||||
assert len(call_args) == 1 # One route added
|
||||
89
tests/unit/test_gateway/test_endpoint_manager.py
Normal file
89
tests/unit/test_gateway/test_endpoint_manager.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
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_core_import.return_value = MagicMock()
|
||||
mock_dispatcher_manager.dispatch_core_export.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_core_import.return_value = MagicMock()
|
||||
mock_dispatcher_manager.dispatch_core_export.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 that are actually called
|
||||
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_core_import.return_value = MagicMock()
|
||||
mock_dispatcher_manager.dispatch_core_export.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() # Called twice
|
||||
mock_dispatcher_manager.dispatch_flow_service.assert_called_once()
|
||||
mock_dispatcher_manager.dispatch_flow_import.assert_called_once()
|
||||
mock_dispatcher_manager.dispatch_flow_export.assert_called_once()
|
||||
mock_dispatcher_manager.dispatch_core_import.assert_called_once()
|
||||
mock_dispatcher_manager.dispatch_core_export.assert_called_once()
|
||||
60
tests/unit/test_gateway/test_endpoint_metrics.py
Normal file
60
tests/unit/test_gateway/test_endpoint_metrics.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Tests for Gateway Metrics Endpoint
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from trustgraph.gateway.endpoint.metrics import MetricsEndpoint
|
||||
|
||||
|
||||
class TestMetricsEndpoint:
|
||||
"""Test cases for MetricsEndpoint class"""
|
||||
|
||||
def test_metrics_endpoint_initialization(self):
|
||||
"""Test MetricsEndpoint initialization"""
|
||||
mock_auth = MagicMock()
|
||||
|
||||
endpoint = MetricsEndpoint(
|
||||
prometheus_url="http://prometheus:9090",
|
||||
endpoint_path="/metrics",
|
||||
auth=mock_auth
|
||||
)
|
||||
|
||||
assert endpoint.prometheus_url == "http://prometheus:9090"
|
||||
assert endpoint.path == "/metrics"
|
||||
assert endpoint.auth == mock_auth
|
||||
assert endpoint.operation == "service"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_metrics_endpoint_start_method(self):
|
||||
"""Test MetricsEndpoint start method (should be no-op)"""
|
||||
mock_auth = MagicMock()
|
||||
|
||||
endpoint = MetricsEndpoint(
|
||||
prometheus_url="http://localhost:9090",
|
||||
endpoint_path="/metrics",
|
||||
auth=mock_auth
|
||||
)
|
||||
|
||||
# start() should complete without error
|
||||
await endpoint.start()
|
||||
|
||||
def test_add_routes_registers_get_handler(self):
|
||||
"""Test add_routes method registers GET route with wildcard path"""
|
||||
mock_auth = MagicMock()
|
||||
mock_app = MagicMock()
|
||||
|
||||
endpoint = MetricsEndpoint(
|
||||
prometheus_url="http://prometheus:9090",
|
||||
endpoint_path="/metrics",
|
||||
auth=mock_auth
|
||||
)
|
||||
|
||||
endpoint.add_routes(mock_app)
|
||||
|
||||
# Verify add_routes was called with GET route
|
||||
mock_app.add_routes.assert_called_once()
|
||||
# The call should include web.get with wildcard path pattern
|
||||
call_args = mock_app.add_routes.call_args[0][0]
|
||||
assert len(call_args) == 1 # One route added
|
||||
133
tests/unit/test_gateway/test_endpoint_socket.py
Normal file
133
tests/unit/test_gateway/test_endpoint_socket.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""
|
||||
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 = AsyncMock()
|
||||
mock_ws.__aiter__ = lambda self: 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)
|
||||
# Verify cleanup methods were called
|
||||
mock_running.stop.assert_called_once()
|
||||
mock_ws.close.assert_called_once()
|
||||
|
||||
@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 = AsyncMock()
|
||||
mock_ws.__aiter__ = lambda self: 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)
|
||||
# Verify cleanup methods were called
|
||||
mock_running.stop.assert_called_once()
|
||||
mock_ws.close.assert_called_once()
|
||||
|
||||
@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 = AsyncMock()
|
||||
mock_ws.__aiter__ = lambda self: 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()
|
||||
# Verify cleanup methods were called after break
|
||||
mock_running.stop.assert_called_once()
|
||||
mock_ws.close.assert_called_once()
|
||||
124
tests/unit/test_gateway/test_endpoint_stream.py
Normal file
124
tests/unit/test_gateway/test_endpoint_stream.py
Normal file
|
|
@ -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)
|
||||
53
tests/unit/test_gateway/test_endpoint_variable.py
Normal file
53
tests/unit/test_gateway/test_endpoint_variable.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""
|
||||
Tests for Gateway Variable Endpoint
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from trustgraph.gateway.endpoint.variable_endpoint import VariableEndpoint
|
||||
|
||||
|
||||
class TestVariableEndpoint:
|
||||
"""Test cases for VariableEndpoint class"""
|
||||
|
||||
def test_variable_endpoint_initialization(self):
|
||||
"""Test VariableEndpoint initialization"""
|
||||
mock_auth = MagicMock()
|
||||
mock_dispatcher = MagicMock()
|
||||
|
||||
endpoint = VariableEndpoint(
|
||||
endpoint_path="/api/variable",
|
||||
auth=mock_auth,
|
||||
dispatcher=mock_dispatcher
|
||||
)
|
||||
|
||||
assert endpoint.path == "/api/variable"
|
||||
assert endpoint.auth == mock_auth
|
||||
assert endpoint.dispatcher == mock_dispatcher
|
||||
assert endpoint.operation == "service"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_variable_endpoint_start_method(self):
|
||||
"""Test VariableEndpoint start method (should be no-op)"""
|
||||
mock_auth = MagicMock()
|
||||
mock_dispatcher = MagicMock()
|
||||
|
||||
endpoint = VariableEndpoint("/api/var", mock_auth, mock_dispatcher)
|
||||
|
||||
# start() should complete without error
|
||||
await endpoint.start()
|
||||
|
||||
def test_add_routes_registers_post_handler(self):
|
||||
"""Test add_routes method registers POST route"""
|
||||
mock_auth = MagicMock()
|
||||
mock_dispatcher = MagicMock()
|
||||
mock_app = MagicMock()
|
||||
|
||||
endpoint = VariableEndpoint("/api/variable", mock_auth, mock_dispatcher)
|
||||
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
|
||||
90
tests/unit/test_gateway/test_running.py
Normal file
90
tests/unit/test_gateway/test_running.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""
|
||||
Tests for Gateway Running utility class
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from trustgraph.gateway.running import Running
|
||||
|
||||
|
||||
class TestRunning:
|
||||
"""Test cases for Running class"""
|
||||
|
||||
def test_running_initialization(self):
|
||||
"""Test Running class initialization"""
|
||||
running = Running()
|
||||
|
||||
# Should start with running = True
|
||||
assert running.running is True
|
||||
|
||||
def test_running_get_method(self):
|
||||
"""Test Running.get() method returns current state"""
|
||||
running = Running()
|
||||
|
||||
# Should return True initially
|
||||
assert running.get() is True
|
||||
|
||||
# Should return False after stopping
|
||||
running.stop()
|
||||
assert running.get() is False
|
||||
|
||||
def test_running_stop_method(self):
|
||||
"""Test Running.stop() method sets running to False"""
|
||||
running = Running()
|
||||
|
||||
# Initially should be True
|
||||
assert running.running is True
|
||||
|
||||
# After calling stop(), should be False
|
||||
running.stop()
|
||||
assert running.running is False
|
||||
|
||||
def test_running_stop_is_idempotent(self):
|
||||
"""Test that calling stop() multiple times is safe"""
|
||||
running = Running()
|
||||
|
||||
# Stop multiple times
|
||||
running.stop()
|
||||
assert running.running is False
|
||||
|
||||
running.stop()
|
||||
assert running.running is False
|
||||
|
||||
# get() should still return False
|
||||
assert running.get() is False
|
||||
|
||||
def test_running_state_transitions(self):
|
||||
"""Test the complete state transition from running to stopped"""
|
||||
running = Running()
|
||||
|
||||
# Initial state: running
|
||||
assert running.get() is True
|
||||
assert running.running is True
|
||||
|
||||
# Transition to stopped
|
||||
running.stop()
|
||||
assert running.get() is False
|
||||
assert running.running is False
|
||||
|
||||
def test_running_multiple_instances_independent(self):
|
||||
"""Test that multiple Running instances are independent"""
|
||||
running1 = Running()
|
||||
running2 = Running()
|
||||
|
||||
# Both should start as running
|
||||
assert running1.get() is True
|
||||
assert running2.get() is True
|
||||
|
||||
# Stop only one
|
||||
running1.stop()
|
||||
|
||||
# States should be independent
|
||||
assert running1.get() is False
|
||||
assert running2.get() is True
|
||||
|
||||
# Stop the other
|
||||
running2.stop()
|
||||
|
||||
# Both should now be stopped
|
||||
assert running1.get() is False
|
||||
assert running2.get() is False
|
||||
360
tests/unit/test_gateway/test_service.py
Normal file
360
tests/unit/test_gateway/test_service.py
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
"""
|
||||
Tests for Gateway Service API
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
||||
from aiohttp import web
|
||||
import pulsar
|
||||
|
||||
from trustgraph.gateway.service import Api, run, default_pulsar_host, default_prometheus_url, default_timeout, default_port, default_api_token
|
||||
|
||||
# Tests for Gateway Service API
|
||||
|
||||
|
||||
class TestApi:
|
||||
"""Test cases for Api class"""
|
||||
|
||||
|
||||
def test_api_initialization_with_defaults(self):
|
||||
"""Test Api initialization with default values"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api()
|
||||
|
||||
assert api.port == default_port
|
||||
assert api.timeout == default_timeout
|
||||
assert api.pulsar_host == default_pulsar_host
|
||||
assert api.pulsar_api_key is None
|
||||
assert api.prometheus_url == default_prometheus_url + "/"
|
||||
assert api.auth.allow_all is True
|
||||
|
||||
# Verify Pulsar client was created without API key
|
||||
mock_client.assert_called_once_with(
|
||||
default_pulsar_host,
|
||||
listener_name=None
|
||||
)
|
||||
|
||||
def test_api_initialization_with_custom_config(self):
|
||||
"""Test Api initialization with custom configuration"""
|
||||
config = {
|
||||
"port": 9000,
|
||||
"timeout": 300,
|
||||
"pulsar_host": "pulsar://custom-host:6650",
|
||||
"pulsar_api_key": "test-api-key",
|
||||
"pulsar_listener": "custom-listener",
|
||||
"prometheus_url": "http://custom-prometheus:9090",
|
||||
"api_token": "secret-token"
|
||||
}
|
||||
|
||||
with patch('pulsar.Client') as mock_client, \
|
||||
patch('pulsar.AuthenticationToken') as mock_auth:
|
||||
mock_client.return_value = Mock()
|
||||
mock_auth.return_value = Mock()
|
||||
|
||||
api = Api(**config)
|
||||
|
||||
assert api.port == 9000
|
||||
assert api.timeout == 300
|
||||
assert api.pulsar_host == "pulsar://custom-host:6650"
|
||||
assert api.pulsar_api_key == "test-api-key"
|
||||
assert api.prometheus_url == "http://custom-prometheus:9090/"
|
||||
assert api.auth.token == "secret-token"
|
||||
assert api.auth.allow_all is False
|
||||
|
||||
# Verify Pulsar client was created with API key
|
||||
mock_auth.assert_called_once_with("test-api-key")
|
||||
mock_client.assert_called_once_with(
|
||||
"pulsar://custom-host:6650",
|
||||
listener_name="custom-listener",
|
||||
authentication=mock_auth.return_value
|
||||
)
|
||||
|
||||
def test_api_initialization_with_pulsar_api_key(self):
|
||||
"""Test Api initialization with Pulsar API key authentication"""
|
||||
with patch('pulsar.Client') as mock_client, \
|
||||
patch('pulsar.AuthenticationToken') as mock_auth:
|
||||
mock_client.return_value = Mock()
|
||||
mock_auth.return_value = Mock()
|
||||
|
||||
api = Api(pulsar_api_key="test-key")
|
||||
|
||||
mock_auth.assert_called_once_with("test-key")
|
||||
mock_client.assert_called_once_with(
|
||||
default_pulsar_host,
|
||||
listener_name=None,
|
||||
authentication=mock_auth.return_value
|
||||
)
|
||||
|
||||
def test_api_initialization_prometheus_url_normalization(self):
|
||||
"""Test that prometheus_url gets normalized with trailing slash"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
# Test URL without trailing slash
|
||||
api = Api(prometheus_url="http://prometheus:9090")
|
||||
assert api.prometheus_url == "http://prometheus:9090/"
|
||||
|
||||
# Test URL with trailing slash
|
||||
api = Api(prometheus_url="http://prometheus:9090/")
|
||||
assert api.prometheus_url == "http://prometheus:9090/"
|
||||
|
||||
def test_api_initialization_empty_api_token_means_no_auth(self):
|
||||
"""Test that empty API token results in allow_all authentication"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api(api_token="")
|
||||
assert api.auth.allow_all is True
|
||||
|
||||
def test_api_initialization_none_api_token_means_no_auth(self):
|
||||
"""Test that None API token results in allow_all authentication"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api(api_token=None)
|
||||
assert api.auth.allow_all is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_factory_creates_application(self):
|
||||
"""Test that app_factory creates aiohttp application"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api()
|
||||
|
||||
# Mock the dependencies
|
||||
api.config_receiver = Mock()
|
||||
api.config_receiver.start = AsyncMock()
|
||||
api.endpoint_manager = Mock()
|
||||
api.endpoint_manager.add_routes = Mock()
|
||||
api.endpoint_manager.start = AsyncMock()
|
||||
|
||||
app = await api.app_factory()
|
||||
|
||||
assert isinstance(app, web.Application)
|
||||
assert app._client_max_size == 256 * 1024 * 1024
|
||||
|
||||
# Verify that config receiver was started
|
||||
api.config_receiver.start.assert_called_once()
|
||||
|
||||
# Verify that endpoint manager was configured
|
||||
api.endpoint_manager.add_routes.assert_called_once_with(app)
|
||||
api.endpoint_manager.start.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_factory_with_custom_endpoints(self):
|
||||
"""Test app_factory with custom endpoints"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api()
|
||||
|
||||
# Mock custom endpoints
|
||||
mock_endpoint1 = Mock()
|
||||
mock_endpoint1.add_routes = Mock()
|
||||
mock_endpoint1.start = AsyncMock()
|
||||
|
||||
mock_endpoint2 = Mock()
|
||||
mock_endpoint2.add_routes = Mock()
|
||||
mock_endpoint2.start = AsyncMock()
|
||||
|
||||
api.endpoints = [mock_endpoint1, mock_endpoint2]
|
||||
|
||||
# Mock the dependencies
|
||||
api.config_receiver = Mock()
|
||||
api.config_receiver.start = AsyncMock()
|
||||
api.endpoint_manager = Mock()
|
||||
api.endpoint_manager.add_routes = Mock()
|
||||
api.endpoint_manager.start = AsyncMock()
|
||||
|
||||
app = await api.app_factory()
|
||||
|
||||
# Verify custom endpoints were configured
|
||||
mock_endpoint1.add_routes.assert_called_once_with(app)
|
||||
mock_endpoint1.start.assert_called_once()
|
||||
mock_endpoint2.add_routes.assert_called_once_with(app)
|
||||
mock_endpoint2.start.assert_called_once()
|
||||
|
||||
def test_run_method_calls_web_run_app(self):
|
||||
"""Test that run method calls web.run_app"""
|
||||
with patch('pulsar.Client') as mock_client, \
|
||||
patch('aiohttp.web.run_app') as mock_run_app:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api(port=8080)
|
||||
api.run()
|
||||
|
||||
# Verify run_app was called once with the correct port
|
||||
mock_run_app.assert_called_once()
|
||||
args, kwargs = mock_run_app.call_args
|
||||
assert len(args) == 1 # Should have one positional arg (the coroutine)
|
||||
assert kwargs == {'port': 8080} # Should have port keyword arg
|
||||
|
||||
def test_api_components_initialization(self):
|
||||
"""Test that all API components are properly initialized"""
|
||||
with patch('pulsar.Client') as mock_client:
|
||||
mock_client.return_value = Mock()
|
||||
|
||||
api = Api()
|
||||
|
||||
# Verify all components are initialized
|
||||
assert api.config_receiver is not None
|
||||
assert api.dispatcher_manager is not None
|
||||
assert api.endpoint_manager is not None
|
||||
assert api.endpoints == []
|
||||
|
||||
# Verify component relationships
|
||||
assert api.dispatcher_manager.pulsar_client == api.pulsar_client
|
||||
assert api.dispatcher_manager.config_receiver == api.config_receiver
|
||||
assert api.endpoint_manager.dispatcher_manager == api.dispatcher_manager
|
||||
# EndpointManager doesn't store auth directly, it passes it to individual endpoints
|
||||
|
||||
|
||||
class TestRunFunction:
|
||||
"""Test cases for the run() function"""
|
||||
|
||||
def test_run_function_with_metrics_enabled(self):
|
||||
"""Test run function with metrics enabled"""
|
||||
import warnings
|
||||
# Suppress the specific async warning with a broader pattern
|
||||
warnings.filterwarnings("ignore", message=".*Api.app_factory.*was never awaited", category=RuntimeWarning)
|
||||
|
||||
with patch('argparse.ArgumentParser.parse_args') as mock_parse_args, \
|
||||
patch('trustgraph.gateway.service.start_http_server') as mock_start_http_server:
|
||||
|
||||
# Mock command line arguments
|
||||
mock_args = Mock()
|
||||
mock_args.metrics = True
|
||||
mock_args.metrics_port = 8000
|
||||
mock_parse_args.return_value = mock_args
|
||||
|
||||
# Create a simple mock instance without any async methods
|
||||
mock_api_instance = Mock()
|
||||
mock_api_instance.run = Mock()
|
||||
|
||||
# Create a mock Api class without importing the real one
|
||||
mock_api = Mock(return_value=mock_api_instance)
|
||||
|
||||
# Patch using context manager to avoid importing the real Api class
|
||||
with patch('trustgraph.gateway.service.Api', mock_api):
|
||||
# Mock vars() to return a dict
|
||||
with patch('builtins.vars') as mock_vars:
|
||||
mock_vars.return_value = {
|
||||
'metrics': True,
|
||||
'metrics_port': 8000,
|
||||
'pulsar_host': default_pulsar_host,
|
||||
'timeout': default_timeout
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
# Verify metrics server was started
|
||||
mock_start_http_server.assert_called_once_with(8000)
|
||||
|
||||
# Verify Api was created and run was called
|
||||
mock_api.assert_called_once()
|
||||
mock_api_instance.run.assert_called_once()
|
||||
|
||||
@patch('trustgraph.gateway.service.start_http_server')
|
||||
@patch('argparse.ArgumentParser.parse_args')
|
||||
def test_run_function_with_metrics_disabled(self, mock_parse_args, mock_start_http_server):
|
||||
"""Test run function with metrics disabled"""
|
||||
# Mock command line arguments
|
||||
mock_args = Mock()
|
||||
mock_args.metrics = False
|
||||
mock_parse_args.return_value = mock_args
|
||||
|
||||
# Create a simple mock instance without any async methods
|
||||
mock_api_instance = Mock()
|
||||
mock_api_instance.run = Mock()
|
||||
|
||||
# Patch the Api class inside the test without using decorators
|
||||
with patch('trustgraph.gateway.service.Api') as mock_api:
|
||||
mock_api.return_value = mock_api_instance
|
||||
|
||||
# Mock vars() to return a dict
|
||||
with patch('builtins.vars') as mock_vars:
|
||||
mock_vars.return_value = {
|
||||
'metrics': False,
|
||||
'metrics_port': 8000,
|
||||
'pulsar_host': default_pulsar_host,
|
||||
'timeout': default_timeout
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
# Verify metrics server was NOT started
|
||||
mock_start_http_server.assert_not_called()
|
||||
|
||||
# Verify Api was created and run was called
|
||||
mock_api.assert_called_once()
|
||||
mock_api_instance.run.assert_called_once()
|
||||
|
||||
@patch('argparse.ArgumentParser.parse_args')
|
||||
def test_run_function_argument_parsing(self, mock_parse_args):
|
||||
"""Test that run function properly parses command line arguments"""
|
||||
# Mock command line arguments
|
||||
mock_args = Mock()
|
||||
mock_args.metrics = False
|
||||
mock_parse_args.return_value = mock_args
|
||||
|
||||
# Create a simple mock instance without any async methods
|
||||
mock_api_instance = Mock()
|
||||
mock_api_instance.run = Mock()
|
||||
|
||||
# Mock vars() to return a dict with all expected arguments
|
||||
expected_args = {
|
||||
'pulsar_host': 'pulsar://test:6650',
|
||||
'pulsar_api_key': 'test-key',
|
||||
'pulsar_listener': 'test-listener',
|
||||
'prometheus_url': 'http://test-prometheus:9090',
|
||||
'port': 9000,
|
||||
'timeout': 300,
|
||||
'api_token': 'secret',
|
||||
'log_level': 'INFO',
|
||||
'metrics': False,
|
||||
'metrics_port': 8001
|
||||
}
|
||||
|
||||
# Patch the Api class inside the test without using decorators
|
||||
with patch('trustgraph.gateway.service.Api') as mock_api:
|
||||
mock_api.return_value = mock_api_instance
|
||||
|
||||
with patch('builtins.vars') as mock_vars:
|
||||
mock_vars.return_value = expected_args
|
||||
|
||||
run()
|
||||
|
||||
# Verify Api was created with the parsed arguments
|
||||
mock_api.assert_called_once_with(**expected_args)
|
||||
mock_api_instance.run.assert_called_once()
|
||||
|
||||
def test_run_function_creates_argument_parser(self):
|
||||
"""Test that run function creates argument parser with correct arguments"""
|
||||
with patch('argparse.ArgumentParser') as mock_parser_class:
|
||||
mock_parser = Mock()
|
||||
mock_parser_class.return_value = mock_parser
|
||||
mock_parser.parse_args.return_value = Mock(metrics=False)
|
||||
|
||||
with patch('trustgraph.gateway.service.Api') as mock_api, \
|
||||
patch('builtins.vars') as mock_vars:
|
||||
mock_vars.return_value = {'metrics': False}
|
||||
mock_api.return_value = Mock()
|
||||
|
||||
run()
|
||||
|
||||
# Verify ArgumentParser was created
|
||||
mock_parser_class.assert_called_once()
|
||||
|
||||
# Verify add_argument was called for each expected argument
|
||||
expected_arguments = [
|
||||
'pulsar-host', 'pulsar-api-key', 'pulsar-listener',
|
||||
'prometheus-url', 'port', 'timeout', 'api-token',
|
||||
'log-level', 'metrics', 'metrics-port'
|
||||
]
|
||||
|
||||
# Check that add_argument was called multiple times (once for each arg)
|
||||
assert mock_parser.add_argument.call_count >= len(expected_arguments)
|
||||
Loading…
Add table
Add a link
Reference in a new issue