"""
Module for tests on application controller
This module contains test cases or testing the "Controller" (otherwise called Application Controller) entity
and all related functionality.
Classes:
ApplicationControllerTestCase
AsyncControllerTest
"""
import os
import random
import unittest
from typing import NoReturn, Optional
from unittest.mock import patch, AsyncMock
import controller.kafka_manager_def
import websockets
from starlette.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
from api.endpoints import app
from controller import Controller, KafkaManager
from role import Role
test_client = TestClient(app)
KafkaManager_AIOKafkaProducer_Mock_patch: Optional[unittest.mock._patch] = None
KafkaManager_AIOKafkaConsumer_Mock_patch: Optional[unittest.mock._patch] = None
[docs]
def setUpModule():
global KafkaManager_AIOKafkaProducer_Mock_patch, KafkaManager_AIOKafkaConsumer_Mock_patch
KafkaManager_AIOKafkaProducer_Mock_patch = patch(
"controller.kafka_manager_def.AIOKafkaProducer",
spec=controller.kafka_manager_def.AIOKafkaProducer,
)
KafkaManager_AIOKafkaConsumer_Mock_patch = patch(
"controller.kafka_manager_def.AIOKafkaConsumer",
spec=controller.kafka_manager_def.AIOKafkaConsumer,
)
KafkaManager_AIOKafkaProducer_Mock_patch.start()
KafkaManager_AIOKafkaConsumer_Mock_patch.start()
[docs]
def tearDownModule():
global KafkaManager_AIOKafkaProducer_Mock_patch, KafkaManager_AIOKafkaConsumer_Mock_patch
if isinstance(KafkaManager_AIOKafkaProducer_Mock_patch, unittest.mock._patch):
KafkaManager_AIOKafkaProducer_Mock_patch.stop()
if isinstance(KafkaManager_AIOKafkaConsumer_Mock_patch, unittest.mock._patch):
KafkaManager_AIOKafkaConsumer_Mock_patch.stop()
[docs]
class ApplicationControllerTestCase(unittest.IsolatedAsyncioTestCase):
"""
Test case class for Application Controller
"""
[docs]
@classmethod
def setUpClass(cls):
cls.chat_context = "Some context"
cls.ws_url = "ws://localhost:8000/chat/" + os.getenv("TEST_CHAT_URL")
[docs]
async def test_controller_constructor_has_array_of_unique_users_based_on_constructor_argument(
self,
) -> None:
"""
Test that the controller constructor takes in kwarg for number of users and populates participating_users
attribute with number of unique User objects is specified by the keyword argument.
:return: None
"""
with patch("websockets.connect", new=AsyncMock()):
number_of_participating_users = random.randint(2, 10)
controller = await Controller.initialise(
number_of_participating_users, self.ws_url, self.chat_context
)
self.assertEqual(
len(set(controller.participating_users)), number_of_participating_users
)
[docs]
async def test_that_controller_has_first_publisher_attribute_which_must_have_role_of_publisher(
self,
) -> None:
"""
Test that initialised consumer has 'first_publisher' attribute which must have role of PUBLISHER
:return: None
"""
with patch("websockets.connect", new=AsyncMock()) as websockets_mock:
controller = await Controller.initialise(2, self.ws_url, self.chat_context)
self.assertTrue(hasattr(controller, "first_publisher"))
self.assertEqual(controller.first_publisher.role, Role.PUBLISHER)
websockets_mock.assert_called_once_with(self.ws_url)
[docs]
async def test_controller_has_web_socket_url_attribute_gotten_from_constructor(
self,
) -> None:
"""
Test that controller has web socket url attribute and gets web socket URL as
part of kwargs from constructor.
:return: None
"""
with patch("websockets.connect", new=AsyncMock()) as websockets_mock:
controller = await Controller.initialise(3, self.ws_url, self.chat_context)
self.assertTrue(hasattr(Controller, "ws_url"))
self.assertEqual(Controller.ws_url, None)
self.assertEqual(controller.ws_url, self.ws_url)
websockets_mock.assert_called_once_with(self.ws_url)
[docs]
async def test_controller_has_string_chat_context_attribute_gotten_from_constructor(
self,
) -> None:
"""
Test that the controller has string chat context supplied by the constructor at initialisation.
:return: None
"""
self.assertFalse(hasattr(Controller, "chat_context"))
with self.assertRaises(TypeError):
await Controller.initialise(2, self.ws_url)
with patch("websockets.connect", new=AsyncMock()):
controller = await Controller.initialise(
3, self.ws_url, "Some chat context"
)
self.assertEqual(controller.chat_context, "Some chat context")
[docs]
async def test_initialised_controller_has_kafka_manager_attribute(self) -> None:
"""
Test that the controller has a Kafka Manager attribute on initialisation.
:return: None
"""
with patch("websockets.connect", new=AsyncMock()):
controller = await Controller.initialise(10, self.ws_url, self.chat_context)
controller_2 = await Controller.initialise(
10, self.ws_url, self.chat_context
)
self.assertTrue(hasattr(controller, "kafka_manager"))
self.assertTrue(isinstance(controller.kafka_manager, KafkaManager))
self.assertFalse(controller.kafka_manager is controller_2.kafka_manager)
[docs]
async def test_controller_on_init_initialises_base_kafka_manager_with_correct_arguments(
self,
) -> None:
"""
Test that the controller at initialisation, creates a Kafka Manager via the Kafka Manager factory with correct
arguments.
:return: None
"""
with patch("websockets.connect", new=AsyncMock()):
with patch(
"controller.kafka_manager_def.KafkaManagerFactory.create_base_kafka_manager"
) as Mocked_KafkaManagerFactory_create_base_kafka_manager:
number_of_users = random.SystemRandom().randint(1, 10)
await Controller.initialise(
number_of_users, self.ws_url, self.chat_context
)
Mocked_KafkaManagerFactory_create_base_kafka_manager.assert_called_once_with(
number_of_users
)
[docs]
async def test_controller_connects_to_chat_websocket_on_init(self) -> None:
"""
Test that controller calls method to connect to chat web socket on init.
:return: None
"""
with patch("websockets.connect", new=AsyncMock()) as websockets_mock:
await Controller.initialise(3, self.ws_url, self.chat_context)
websockets_mock.assert_called_once_with(self.ws_url)
[docs]
async def test_controller_fails_to_connect_to_chat_websocket_on_invalid_url(
self,
) -> None:
"""
Test that controller fails to connect to the chat web socket on init when supplied with invalid url
:return: None
"""
with self.assertRaises(websockets.InvalidURI):
await Controller.initialise(2, "some-invalid-uuid", self.chat_context)
[docs]
async def test_controller_has_null_websocket_attribute_which_becomes_populated_on_connection(
self,
) -> None:
"""
Test that controller has null 'websocket' attribute before connection to chat websocket;
after connection Controller.websocket becomes populated ideally with a websockets.WebSocketClientProtocol object.
:return: None
"""
self.assertIsNone(Controller._Controller__websocket)
with patch("websockets.connect", new=AsyncMock()) as websockets_mock:
websockets_mock.return_value = websockets.WebSocketClientProtocol()
controller = await Controller.initialise(2, self.ws_url, self.chat_context)
self.assertIsNotNone(controller.websocket)
self.assertIsNotNone(controller._Controller__websocket)
self.assertIsInstance(
controller.websocket, websockets.WebSocketClientProtocol
)
[docs]
async def test_set_websocket_invalid_type(self) -> None:
"""
Test that setting Controller websocket attribute to invalid type will raise Exception.
:return:
"""
with patch("websockets.connect", new=AsyncMock()):
async def f() -> NoReturn:
"""
This ideally shouldn't work as trying to set Controller.websocket to anything other than
a websockets.WebSocketClientProtocol object will cause an Exception to be raised.
:return: This should ideally never return
"""
controller = await Controller.initialise(
2, self.ws_url, self.chat_context
)
controller.websocket = "invalid object"
with self.assertRaises(AssertionError):
await f()
[docs]
async def test_controller_method_for_connecting_to_websocket_can_send_messages_to_websocket(
self,
) -> None:
"""
Test that Controller websocket connector can send message to websocket.
:return:
"""
try:
with test_client.websocket_connect(
"/ws/" + os.getenv("TEST_CHAT_URL")
) as test_websocket_client:
controller = await Controller.initialise(
2, self.ws_url, self.chat_context
)
await controller.connect_ws("hello world")
data = test_websocket_client.receive_text()
self.assertEqual(data, "hello world")
test_websocket_client.close(reason="Done with test")
except WebSocketDisconnect:
pass
except Exception as e:
self.fail(
f"Exception not expected from this test, raised exception is: {e}"
)
[docs]
async def test_that_on_init_of_controller_all_consumers_in_kafka_manager_are_subscribed_to_group_chat_kafka_topic(
self,
) -> None:
"""
Test that on initialisation of application controller, all consumers in kafka manager are subscribed to the group chat
kafka topic.
:return: None
"""
with patch(
"controller.controller_def.Controller.connect_ws",
new_callable=lambda: AsyncMock(),
):
KafkaManager_AIOKafkaConsumer_Mock_patch.stop()
controller = await Controller.initialise(
random.randint(1, 10), self.ws_url, self.chat_context
)
for consumer in controller.kafka_manager.consumers:
self.assertEqual(len(consumer.subscription()), 1)
self.assertEqual(
set(consumer.subscription()).pop(),
os.getenv("TEST_CHAT_URL").split("/")[-1],
)