516 lines
19 KiB
Python
516 lines
19 KiB
Python
import asyncio
|
|
import unittest
|
|
from enum import Enum
|
|
from typing import Any, List, Optional
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from aio_pika.abc import AbstractChannel, AbstractRobustConnection
|
|
from aiohttp import ClientResponse
|
|
from aiohttp.web_fileresponse import FileResponse
|
|
from pydantic import BaseModel
|
|
|
|
from amqp.adapter.cleverthis_service_adapter import (
|
|
AMQMessage,
|
|
CleverThisServiceAdapter,
|
|
_convert_file_response,
|
|
_create_reply_exchange_and_queue,
|
|
is_likely_json,
|
|
)
|
|
from amqp.adapter.file_handler import FileHandler
|
|
from amqp.config.amq_configuration import AMQConfiguration
|
|
from amqp.model.model import DataResponse
|
|
from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE
|
|
|
|
|
|
class NestedModel(BaseModel):
|
|
id: int
|
|
name: str
|
|
|
|
|
|
class ComplexModel(BaseModel):
|
|
nested: NestedModel
|
|
items: List[str]
|
|
|
|
|
|
class TestEnum(Enum):
|
|
VALUE1 = "value1"
|
|
VALUE2 = "value2"
|
|
|
|
|
|
class TestModel(BaseModel):
|
|
name: str
|
|
value: int
|
|
|
|
|
|
class TestCleverThisServiceAdapter(unittest.IsolatedAsyncioTestCase):
|
|
|
|
async def test_create_reply_exchange_and_queue_success(self):
|
|
connection_mock = AsyncMock()
|
|
channel_mock = AsyncMock()
|
|
exchange_mock = AsyncMock()
|
|
queue_mock = AsyncMock()
|
|
|
|
connection_mock.channel.return_value = channel_mock
|
|
channel_mock.get_exchange.return_value = exchange_mock
|
|
channel_mock.declare_queue.return_value = queue_mock
|
|
queue_mock.name = "test_queue_name"
|
|
|
|
channel, exchange, queue_name = await _create_reply_exchange_and_queue(connection_mock)
|
|
|
|
assert channel == channel_mock
|
|
assert exchange == exchange_mock
|
|
assert queue_name == "test_queue_name"
|
|
channel_mock.get_exchange.assert_called_once_with(name=AMQ_REPLY_TO_EXCHANGE, ensure=True)
|
|
channel_mock.declare_queue.assert_called_once_with(exclusive=False)
|
|
queue_mock.bind.assert_called_once_with(
|
|
exchange=AMQ_REPLY_TO_EXCHANGE, routing_key="test_queue_name"
|
|
)
|
|
|
|
async def test_convert_file_response_successful(self):
|
|
connection_mock = AsyncMock(spec=AbstractRobustConnection)
|
|
channel_mock: AbstractChannel = AsyncMock(spec=AbstractChannel)
|
|
exchange_mock = AsyncMock()
|
|
queue_mock = AsyncMock()
|
|
|
|
connection_mock.channel = AsyncMock(return_value=channel_mock)
|
|
channel_mock.get_exchange.return_value = exchange_mock
|
|
channel_mock.declare_queue.return_value = queue_mock
|
|
channel_mock.close = AsyncMock()
|
|
queue_mock.name = "test_queue_name"
|
|
|
|
mock_file_handler = MagicMock(spec=FileHandler)
|
|
mock_file_handler.publish_file = AsyncMock(return_value=("test_stream", 12345))
|
|
mock_file_response = MagicMock(spec=FileResponse)
|
|
mock_file_response.path = "/test/path"
|
|
mock_file_response.filename = "test_file.txt"
|
|
mock_loop = asyncio.get_event_loop()
|
|
|
|
json_response = await _convert_file_response(
|
|
mock_file_response, connection_mock, mock_file_handler, mock_loop
|
|
)
|
|
|
|
assert '"filename": "test_file.txt"' in json_response
|
|
assert '"streamName": "test_stream"' in json_response
|
|
assert '"fileSize": 12345' in json_response
|
|
|
|
def test_is_likely_json(self):
|
|
assert is_likely_json("{ }") is True
|
|
assert is_likely_json("[ ]") is True
|
|
assert is_likely_json("{]") is False
|
|
assert is_likely_json("[}") is False
|
|
assert is_likely_json("something else") is False
|
|
|
|
def setUp(self):
|
|
# Mock AMQConfiguration
|
|
self.mock_amq_config = AMQConfiguration("./application.properties.local")
|
|
|
|
# Mock ClientSession
|
|
self.mock_session = AsyncMock()
|
|
|
|
# Create adapter instance
|
|
self.adapter = CleverThisServiceAdapter(
|
|
amq_configuration=self.mock_amq_config, routes=[], session=self.mock_session
|
|
)
|
|
self.adapter.service_host = "testhost"
|
|
self.adapter.authenticated_user_arg_name = "user"
|
|
|
|
def test_init(self):
|
|
self.assertEqual(self.adapter.service_host, "testhost")
|
|
self.assertEqual(self.adapter.service_port, 8080)
|
|
self.assertFalse(self.adapter.require_authenticated_user)
|
|
|
|
def test_get_content_type(self):
|
|
test_cases = [
|
|
({"Content-Type": ["application/json"]}, "application/json"),
|
|
({"content-type": ["text/xml"]}, "text/xml"),
|
|
({"CONTENT-TYPE": ["text/plain; charset=utf-8"]}, "text/plain"),
|
|
({}, "application/json"),
|
|
({"Other-Header": ["value"]}, "application/json"),
|
|
]
|
|
|
|
for headers, expected in test_cases:
|
|
with self.subTest(headers=headers):
|
|
result = self.adapter.get_content_type(headers)
|
|
self.assertEqual(result, expected)
|
|
|
|
async def test_get_current_user(self):
|
|
# Base implementation should return None
|
|
result = await self.adapter.get_current_user("test_token")
|
|
self.assertIsNone(result)
|
|
|
|
@patch.object(CleverThisServiceAdapter, "get_current_user")
|
|
async def test_on_message_unauthenticated(self, mock_get_user):
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.headers.return_value = {}
|
|
mock_message.path.return_value = "/some/path"
|
|
mock_message.method.return_value = "GET"
|
|
mock_message.id.return_value = "123"
|
|
mock_message.trace_info.return_value = {}
|
|
|
|
# Test when authentication is required but not provided
|
|
self.adapter.require_authenticated_user = True
|
|
response = await self.adapter.on_message(mock_message)
|
|
self.assertEqual(response.response_code(), "401")
|
|
mock_get_user.assert_not_called()
|
|
|
|
@patch.object(CleverThisServiceAdapter, "get_current_user")
|
|
@patch.object(CleverThisServiceAdapter, "get")
|
|
async def test_on_message_authenticated(self, mock_get: MagicMock, mock_get_user):
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.headers.return_value = {"Authorization": ["Bearer valid_token"]}
|
|
mock_message.path.return_value = "/some/path"
|
|
mock_message.method.return_value = "GET"
|
|
mock_message.id.return_value = "123"
|
|
mock_message.trace_info.return_value = {}
|
|
|
|
mock_user = MagicMock()
|
|
mock_get_user.return_value = mock_user
|
|
|
|
mock_response = MagicMock(spec=DataResponse)
|
|
mock_get.return_value = mock_response
|
|
|
|
self.adapter.require_authenticated_user = True
|
|
response = await self.adapter.on_message(mock_message)
|
|
|
|
self.assertEqual(response, mock_response)
|
|
mock_get_user.assert_called_once_with("valid_token")
|
|
mock_get.assert_called_once_with(mock_message, mock_user)
|
|
|
|
@patch.object(CleverThisServiceAdapter, "get_current_user")
|
|
async def test_on_message_login_path(self, mock_get_user):
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.headers.return_value = {}
|
|
mock_message.path.return_value = "/login"
|
|
mock_message.method.return_value = "POST"
|
|
mock_message.id.return_value = "123"
|
|
mock_message.trace_info.return_value = {}
|
|
|
|
mock_response = MagicMock(spec=DataResponse)
|
|
with patch.object(self.adapter, "post", return_value=mock_response) as mock_post:
|
|
response = await self.adapter.on_message(mock_message)
|
|
self.assertEqual(response, mock_response)
|
|
mock_get_user.assert_not_called()
|
|
mock_post.assert_called_once()
|
|
|
|
@patch.object(CleverThisServiceAdapter, "get_current_user")
|
|
async def test_on_message_all_methods(self, mock_get_user):
|
|
mock_user = MagicMock()
|
|
mock_get_user.return_value = mock_user
|
|
|
|
mock_response = MagicMock(spec=DataResponse)
|
|
|
|
methods = ["GET", "PUT", "POST", "DELETE", "HEAD"]
|
|
for method in methods:
|
|
with self.subTest(method=method):
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.headers.return_value = {"Authorization": ["Bearer valid_token"]}
|
|
mock_message.path.return_value = "/some/path"
|
|
mock_message.method.return_value = method
|
|
mock_message.id.return_value = "123"
|
|
mock_message.trace_info.return_value = {}
|
|
|
|
with patch.object(
|
|
self.adapter, method.lower(), return_value=mock_response
|
|
) as mock_method:
|
|
response = await self.adapter.on_message(mock_message)
|
|
self.assertEqual(response, mock_response)
|
|
mock_method.assert_called_once()
|
|
|
|
async def test_get_with_session(self):
|
|
mock_message: AMQMessage = MagicMock(spec=AMQMessage)
|
|
mock_message.path.return_value = "/api/test"
|
|
mock_message.headers.return_value = {"X-Test": "value"}
|
|
mock_message.trace_info.return_value = {"trace-id": "123"}
|
|
mock_message.id.return_value = "msg123"
|
|
|
|
mock_response = MagicMock(spec=ClientResponse)
|
|
mock_response.status = 200
|
|
mock_response.content_type = "application/json"
|
|
mock_response.read = AsyncMock(return_value=b'{"result": "success"}')
|
|
|
|
self.mock_session.get.return_value = mock_response
|
|
|
|
response = await self.adapter.get(mock_message, None)
|
|
|
|
self.assertEqual(response.response_code(), "200")
|
|
self.mock_session.get.assert_called_once_with(
|
|
"http://testhost:8080/api/test",
|
|
headers={"X-Test": "value", "trace-id": "123"},
|
|
)
|
|
|
|
async def test_get_without_session(self):
|
|
self.adapter.session = None
|
|
|
|
mock_message: AMQMessage = MagicMock(spec=AMQMessage)
|
|
mock_message.path.return_value = "/api/test"
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.trace_info.return_value = {}
|
|
|
|
response = await self.adapter.get(mock_message, None)
|
|
|
|
self.assertEqual(response.response_code(), "404")
|
|
|
|
async def test_put_with_form_handling(self):
|
|
mock_message: AMQMessage = MagicMock(spec=AMQMessage)
|
|
mock_message.path.return_value = "/api/test"
|
|
mock_message.headers.return_value = {}
|
|
mock_message.trace_info.return_value = {}
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.body.return_value = b"test=value"
|
|
|
|
mock_response: DataResponse = MagicMock(spec=DataResponse)
|
|
mock_response.response_code.return_value = "200"
|
|
mock_response.content_type.return_value = "application/json"
|
|
mock_response.read = AsyncMock(return_value=b'{"result": "success"}')
|
|
|
|
self.mock_session.put.return_value = mock_response
|
|
|
|
with patch.object(
|
|
self.adapter,
|
|
"_authorize_forward_request",
|
|
wraps=self.adapter._authorize_forward_request,
|
|
) as mock_handler:
|
|
response = await self.adapter.put(mock_message, None)
|
|
|
|
self.assertEqual(response.response_code(), "200")
|
|
mock_handler.assert_called_once()
|
|
|
|
async def test_post_with_form_handling(self):
|
|
mock_message: AMQMessage = MagicMock(spec=AMQMessage)
|
|
mock_message.path.return_value = "/api/test"
|
|
mock_message.headers.return_value = {}
|
|
mock_message.trace_info.return_value = {}
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.body.return_value = b"test=value"
|
|
|
|
mock_response: DataResponse = MagicMock(spec=DataResponse)
|
|
mock_response.response_code.return_value = "201"
|
|
mock_response.content_type = "application/json"
|
|
mock_response.read = AsyncMock(return_value=b'{"result": "created"}')
|
|
|
|
self.mock_session.post.return_value = mock_response
|
|
|
|
with patch.object(
|
|
self.adapter,
|
|
"_authorize_forward_request",
|
|
wraps=self.adapter._authorize_forward_request,
|
|
) as mock_handler:
|
|
response = await self.adapter.post(mock_message, None)
|
|
self.assertEqual(response.response_code(), "201")
|
|
mock_handler.assert_called_once()
|
|
|
|
async def test_head_not_implemented(self):
|
|
mock_message: AMQMessage = MagicMock(spec=AMQMessage)
|
|
mock_message.id = "msg123"
|
|
|
|
with self.assertRaises(NotImplementedError):
|
|
await self.adapter.head(mock_message, None)
|
|
|
|
def test_basic_type_conversion(self):
|
|
async def test_method(x: int, y: float, z: bool, text: str):
|
|
pass
|
|
|
|
args = {"x": "123", "y": "45.67", "z": "true", "text": "hello"}
|
|
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["x"], 123)
|
|
self.assertEqual(result["y"], 45.67)
|
|
self.assertEqual(result["z"], True)
|
|
self.assertEqual(result["text"], "hello")
|
|
|
|
def test_enum_conversion(self):
|
|
async def test_method(status: TestEnum):
|
|
pass
|
|
|
|
args = {"status": "value1"}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["status"], TestEnum.VALUE1)
|
|
|
|
def test_optional_parameter(self):
|
|
async def test_method(required: str, optional: Optional[str] = None):
|
|
pass
|
|
|
|
args = {"required": "test"}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["required"], "test")
|
|
self.assertIsNone(result["optional"])
|
|
|
|
def test_list_parameter(self):
|
|
async def test_method(items: List[str]):
|
|
pass
|
|
|
|
args = {"items": "item1,item2,item3"}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["items"], ["item1", "item2", "item3"])
|
|
|
|
def test_pydantic_model_parameter(self):
|
|
async def test_method(model: TestModel):
|
|
pass
|
|
|
|
args = {"model": '{"name": "test", "value": 42}'}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertIsInstance(result["model"], TestModel)
|
|
self.assertEqual(result["model"].name, "test")
|
|
self.assertEqual(result["model"].value, 42)
|
|
|
|
def test_authenticated_user_parameter(self):
|
|
async def test_method(user: Any):
|
|
pass
|
|
|
|
user_obj = MagicMock()
|
|
args = {"user": user_obj}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["user"], user_obj)
|
|
|
|
def test_missing_required_parameter(self):
|
|
async def test_method(required: str):
|
|
pass
|
|
|
|
args = {}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertNotIn("required", result)
|
|
|
|
def test_invalid_enum_value(self):
|
|
async def test_method(status: TestEnum):
|
|
pass
|
|
|
|
args = {"status": "invalid_value"}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["status"], "invalid_value")
|
|
|
|
def test_invalid_type_conversion(self):
|
|
async def test_method(x: int):
|
|
pass
|
|
|
|
args = {"x": "not_a_number"}
|
|
result = {}
|
|
try:
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
except Exception as e:
|
|
self.assertTrue(isinstance(e, TypeError))
|
|
|
|
self.assertEqual(len(result), 0)
|
|
|
|
def test_complex_nested_model(self):
|
|
|
|
async def test_method(model: ComplexModel):
|
|
pass
|
|
|
|
args = {"model": {"nested": {"id": 1, "name": "test"}, "items": ["item1", "item2"]}}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertIsInstance(result["model"], ComplexModel)
|
|
self.assertEqual(result["model"].nested.id, 1)
|
|
self.assertEqual(result["model"].nested.name, "test")
|
|
self.assertEqual(result["model"].items, ["item1", "item2"])
|
|
|
|
def test_default_factory_parameter(self):
|
|
class DefaultFactory:
|
|
@staticmethod
|
|
def default_factory():
|
|
return "default_value"
|
|
|
|
async def test_method(param: str = DefaultFactory()):
|
|
pass
|
|
|
|
args = {}
|
|
result = self.adapter._verify_and_instantiate_arguments(args, test_method)
|
|
|
|
self.assertEqual(result["param"], "default_value")
|
|
|
|
async def test_call_cleverthis_api_success_string_result(self):
|
|
"""Test call_cleverthis_api with a string result."""
|
|
|
|
async def test_api_method(param1: str, param2: int):
|
|
return f"Result: {param1}, {param2}"
|
|
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.trace_info.return_value = {"trace-id": "123"}
|
|
|
|
args = {"param1": "test", "param2": 42}
|
|
|
|
response = await self.adapter.call_cleverthis_api(
|
|
message=mock_message, args=args, api_method=test_api_method, success_code=200
|
|
)
|
|
|
|
self.assertEqual(response.response_code(), "200")
|
|
self.assertEqual(response.content_type(), "application/json")
|
|
# String results are not JSON encoded, they're returned as-is
|
|
self.assertEqual(response.body(), b"Result: test, 42")
|
|
|
|
async def test_call_cleverthis_api_success_pydantic_model_result(self):
|
|
"""Test call_cleverthis_api with a Pydantic model result."""
|
|
|
|
async def test_api_method(param1: str):
|
|
return TestModel(name=param1, value=42)
|
|
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.trace_info.return_value = {"trace-id": "123"}
|
|
|
|
args = {"param1": "test_name"}
|
|
|
|
response = await self.adapter.call_cleverthis_api(
|
|
message=mock_message, args=args, api_method=test_api_method, success_code=201
|
|
)
|
|
|
|
self.assertEqual(response.response_code(), "201")
|
|
self.assertEqual(response.content_type(), "application/json")
|
|
# Pydantic JSON output doesn't have spaces around colons
|
|
self.assertIn(b'"name":"test_name"', response.body())
|
|
self.assertIn(b'"value":42', response.body())
|
|
|
|
async def test_call_cleverthis_api_http_exception(self):
|
|
"""Test call_cleverthis_api with HTTPException."""
|
|
from fastapi import HTTPException
|
|
|
|
async def test_api_method(param1: str):
|
|
raise HTTPException(status_code=400, detail="Bad request")
|
|
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.trace_info.return_value = {"trace-id": "123"}
|
|
|
|
args = {"param1": "test"}
|
|
|
|
response = await self.adapter.call_cleverthis_api(
|
|
message=mock_message, args=args, api_method=test_api_method, success_code=200
|
|
)
|
|
|
|
self.assertEqual(response.response_code(), "400")
|
|
self.assertEqual(response.content_type(), "application/json")
|
|
self.assertIn(b'"Bad request"', response.body())
|
|
|
|
async def test_call_cleverthis_api_general_exception(self):
|
|
"""Test call_cleverthis_api with general exception."""
|
|
|
|
async def test_api_method(param1: str):
|
|
raise ValueError("Something went wrong")
|
|
|
|
mock_message = MagicMock(spec=AMQMessage)
|
|
mock_message.id.return_value = "msg123"
|
|
mock_message.trace_info.return_value = {"trace-id": "123"}
|
|
|
|
args = {"param1": "test"}
|
|
|
|
response = await self.adapter.call_cleverthis_api(
|
|
message=mock_message, args=args, api_method=test_api_method, success_code=200
|
|
)
|
|
|
|
self.assertEqual(response.response_code(), "500")
|
|
self.assertEqual(response.content_type(), "application/json")
|
|
self.assertIn(b'"Something went wrong"', response.body())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|