add more tests

This commit is contained in:
HgO 2024-04-17 14:59:37 +02:00
parent 27828ec3c7
commit 6ae3355f3c
9 changed files with 925 additions and 77 deletions

View file

@ -5,7 +5,6 @@ import logging
from diskcache import Cache
from nio.client import AsyncClient
from nio.events import (
Event,
InviteMemberEvent,
KeyVerificationCancel,
KeyVerificationKey,
@ -470,10 +469,3 @@ class Callbacks:
raise SendRetryError(
f"{response_event.status_code} - {response_event.message}"
)
async def debug(self, room: MatrixRoom, event: Event) -> None:
logger.debug(
f"Bot {self.matrix_client.user_id} | Room ID {room.room_id} | "
f"Event ID {event.event_id} | Sender {event.sender} | "
f"Received some event: {event.source}"
)

View file

@ -1,2 +1,3 @@
[pytest]
asyncio_mode=strict
addopts=--cov=matrix_alertbot --cov-report=lcov:lcov.info --cov-report=term

View file

@ -50,7 +50,8 @@ test =
flake8-comprehensions>=3.10.0
isort>=5.10.1
mypy>=0.961
pytest>=7.1.2
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-asyncio>=0.18.3
freezegun>=1.2.1
types-PyYAML>=6.0.9

View file

@ -24,12 +24,6 @@ from matrix_alertbot.errors import (
)
async def update_silence_raise_silence_not_found(
fingerprint: str, user: str, duration_seconds: int, *, force: bool = False
) -> str:
raise SilenceNotFoundError
class FakeCache:
def __init__(self, cache_dict: Optional[Dict] = None) -> None:
if cache_dict is None:
@ -533,14 +527,20 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
)
self.assertEqual({"fingerprint1": ("silence2", 864000)}, fake_cache.cache)
@patch.object(matrix_alertbot.alertmanager.AlertmanagerClient, "update_silence")
@patch.object(matrix_alertbot.alertmanager.AlertmanagerClient, "create_silence")
@patch.object(
matrix_alertbot.alertmanager.AlertmanagerClient,
"update_silence",
side_effect=SilenceNotFoundError,
)
@patch.object(
matrix_alertbot.alertmanager.AlertmanagerClient,
"create_silence",
return_value="silence1",
)
async def test_create_or_update_silence_with_duration_and_silence_not_found(
self, fake_create_silence: Mock, fake_update_silence: Mock
) -> None:
fake_cache = Mock(spec=Cache)
fake_update_silence.side_effect = update_silence_raise_silence_not_found
fake_create_silence.return_value = "silence1"
alertmanager_client = AlertmanagerClient("http://localhost", fake_cache)
async with aiotools.closing_async(alertmanager_client):
@ -651,14 +651,20 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
)
self.assertEqual({"fingerprint1": ("silence2", None)}, fake_cache.cache)
@patch.object(matrix_alertbot.alertmanager.AlertmanagerClient, "update_silence")
@patch.object(matrix_alertbot.alertmanager.AlertmanagerClient, "create_silence")
@patch.object(
matrix_alertbot.alertmanager.AlertmanagerClient,
"update_silence",
side_effect=SilenceNotFoundError,
)
@patch.object(
matrix_alertbot.alertmanager.AlertmanagerClient,
"create_silence",
return_value="silence1",
)
async def test_create_or_update_silence_without_duration_and_silence_not_found(
self, fake_create_silence: Mock, fake_update_silence: Mock
) -> None:
fake_cache = Mock(spec=Cache)
fake_update_silence.side_effect = update_silence_raise_silence_not_found
fake_create_silence.return_value = "silence1"
alertmanager_client = AlertmanagerClient("http://localhost", fake_cache)
async with aiotools.closing_async(alertmanager_client):

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import unittest
from typing import Dict
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import MagicMock, Mock, call, patch
import nio
import nio.crypto
@ -14,10 +14,6 @@ import matrix_alertbot.command
import matrix_alertbot.matrix
def key_verification_get_mac_raise_protocol_error():
raise nio.LocalProtocolError
class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
# Create a Callbacks object and give it some Mock'd objects to use
@ -67,6 +63,42 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to join the room
self.fake_matrix_client.join.assert_called_once_with(self.fake_room.room_id)
async def test_invite_in_unauthorized_room(self) -> None:
"""Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it
fake_invite_event = Mock(spec=nio.InviteMemberEvent)
fake_invite_event.sender = "@some_other_fake_user:example.com"
self.fake_room.room_id = "!unauthorizedroom@example.com"
# Pretend that we received an invite event
await self.callbacks.invite(self.fake_room, fake_invite_event)
# Check that we attempted to join the room
self.fake_matrix_client.join.assert_not_called()
async def test_invite_raise_join_error(self) -> None:
"""Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it
fake_invite_event = Mock(spec=nio.InviteMemberEvent)
fake_invite_event.sender = "@some_other_fake_user:example.com"
fake_join_error = Mock(spec=nio.JoinError)
fake_join_error.message = "error message"
self.fake_matrix_client.join.return_value = fake_join_error
# Pretend that we received an invite event
await self.callbacks.invite(self.fake_room, fake_invite_event)
# Check that we attempted to join the room
self.fake_matrix_client.join.assert_has_calls(
[
call("!abcdefg:example.com"),
call("!abcdefg:example.com"),
call("!abcdefg:example.com"),
]
)
@patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True)
async def test_message_without_prefix(self, fake_command_create: Mock) -> None:
"""Tests the callback for RoomMessageText without any command prefix"""
@ -82,6 +114,24 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that the command was not executed
fake_command_create.assert_not_called()
@patch.object(matrix_alertbot.command, "HelpCommand", autospec=True)
async def test_message_help_client_not_in_pool(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText without any command prefix"""
# Tests that the bot process messages in the room
fake_message_event = Mock(spec=nio.RoomMessageText)
fake_message_event.event_id = "some event id"
fake_message_event.sender = "@some_other_fake_user:example.com"
fake_message_event.body = "!alert help"
fake_message_event.source = {"content": {}}
self.fake_matrix_client_pool.matrix_client = None
# Pretend that we received a text message event
await self.callbacks.message(self.fake_room, fake_message_event)
# Check that the command was not executed
fake_command.assert_not_called()
@patch.object(matrix_alertbot.command, "HelpCommand", autospec=True)
async def test_message_help_not_in_reply_with_prefix(
self, fake_command: Mock
@ -271,6 +321,72 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
)
fake_command.return_value.process.assert_called_once()
@patch.object(matrix_alertbot.callback, "logger", autospec=True)
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
async def test_message_raise_exception(
self, fake_command: Mock, fake_logger
) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_message_event = Mock(spec=nio.RoomMessageText)
fake_message_event.event_id = "some event id"
fake_message_event.sender = "@some_other_fake_user:example.com"
fake_message_event.body = "!alert ack"
fake_message_event.source = {
"content": {
"m.relates_to": {"m.in_reply_to": {"event_id": "some alert event id"}}
}
}
fake_command.return_value.process.side_effect = (
nio.exceptions.LocalProtocolError
)
# Pretend that we received a text message event
await self.callbacks.message(self.fake_room, fake_message_event)
# Check that the command was not executed
fake_command.assert_called_once_with(
self.fake_matrix_client,
self.fake_cache,
self.fake_alertmanager_client,
self.fake_config,
self.fake_room,
fake_message_event.sender,
fake_message_event.event_id,
"some alert event id",
(),
)
fake_command.return_value.process.assert_called_once()
fake_logger.exception.assert_called_once()
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_client_not_in_pool(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event = Mock(spec=nio.RoomMessageText)
fake_alert_event.event_id = "some alert event id"
fake_alert_event.sender = self.fake_matrix_client.user_id
fake_reaction_event = Mock(spec=nio.ReactionEvent)
fake_reaction_event.event_id = "some event id"
fake_reaction_event.sender = "@some_other_fake_user:example.com"
fake_reaction_event.reacts_to = fake_alert_event.event_id
fake_reaction_event.key = "🤫"
fake_event_response = Mock(spec=nio.RoomGetEventResponse)
fake_event_response.event = fake_alert_event
self.fake_matrix_client.room_get_event.return_value = fake_event_response
self.fake_matrix_client_pool.matrix_client = None
# Pretend that we received a text message event
await self.callbacks.reaction(self.fake_room, fake_reaction_event)
# Check that we attempted to execute the command
fake_command.assert_not_called()
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_existing_alert(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
@ -365,6 +481,52 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_room.room_id, fake_alert_event.event_id
)
@patch.object(matrix_alertbot.callback, "logger", autospec=True)
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_raise_exception(
self, fake_command: Mock, fake_logger: Mock
) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event = Mock(spec=nio.RoomMessageText)
fake_alert_event.event_id = "some alert event id"
fake_alert_event.sender = self.fake_matrix_client.user_id
fake_reaction_event = Mock(spec=nio.ReactionEvent)
fake_reaction_event.event_id = "some event id"
fake_reaction_event.sender = "@some_other_fake_user:example.com"
fake_reaction_event.reacts_to = fake_alert_event.event_id
fake_reaction_event.key = "🤫"
fake_event_response = Mock(spec=nio.RoomGetEventResponse)
fake_event_response.event = fake_alert_event
self.fake_matrix_client.room_get_event.return_value = fake_event_response
fake_command.return_value.process.side_effect = (
nio.exceptions.LocalProtocolError
)
# Pretend that we received a text message event
await self.callbacks.reaction(self.fake_room, fake_reaction_event)
# Check that we attempted to execute the command
fake_command.assert_called_once_with(
self.fake_matrix_client,
self.fake_cache,
self.fake_alertmanager_client,
self.fake_config,
self.fake_room,
fake_reaction_event.sender,
fake_reaction_event.event_id,
"some alert event id",
)
fake_command.return_value.process.assert_called_once()
self.fake_matrix_client.room_get_event.assert_called_once_with(
self.fake_room.room_id, fake_alert_event.event_id
)
fake_logger.exception.assert_called_once()
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_unknown(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
@ -429,6 +591,28 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_command.assert_not_called()
self.fake_matrix_client.room_get_event.assert_not_called()
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_redaction_client_not_in_pool(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event_id = "some alert event id"
fake_redaction_event = Mock(spec=nio.RedactionEvent)
fake_redaction_event.redacts = "some other event id"
fake_redaction_event.event_id = "some event id"
fake_redaction_event.sender = "@some_other_fake_user:example.com"
fake_cache_dict = {fake_redaction_event.redacts: fake_alert_event_id}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
self.fake_matrix_client_pool.matrix_client = None
# Pretend that we received a text message event
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
# Check that we attempted to execute the command
fake_command.assert_not_called()
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_redaction(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
@ -459,6 +643,45 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
)
fake_command.return_value.process.assert_called_once()
@patch.object(matrix_alertbot.callback, "logger", autospec=True)
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_redaction_raise_exception(
self, fake_command: Mock, fake_logger
) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event_id = "some alert event id"
fake_redaction_event = Mock(spec=nio.RedactionEvent)
fake_redaction_event.redacts = "some other event id"
fake_redaction_event.event_id = "some event id"
fake_redaction_event.sender = "@some_other_fake_user:example.com"
fake_cache_dict = {fake_redaction_event.redacts: fake_alert_event_id}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
fake_command.return_value.process.side_effect = (
nio.exceptions.LocalProtocolError
)
# Pretend that we received a text message event
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
# Check that we attempted to execute the command
fake_command.assert_called_once_with(
self.fake_matrix_client,
self.fake_cache,
self.fake_alertmanager_client,
self.fake_config,
self.fake_room,
fake_redaction_event.sender,
fake_redaction_event.event_id,
fake_redaction_event.redacts,
)
fake_command.return_value.process.assert_called_once()
fake_logger.exception.assert_called_once()
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_ignore_redaction_sent_by_bot_user(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
@ -718,7 +941,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_key_verification_event.transaction_id = fake_transaction_id
fake_sas = Mock()
fake_sas.get_mac.side_effect = key_verification_get_mac_raise_protocol_error
fake_sas.get_mac.side_effect = nio.exceptions.LocalProtocolError
fake_transactions_dict = {fake_transaction_id: fake_sas}
self.fake_matrix_client.key_verifications = fake_transactions_dict
@ -751,6 +974,81 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_sas.get_mac.assert_called_once_with()
self.fake_matrix_client.to_device.assert_called_once_with(fake_sas.get_mac())
@patch.object(matrix_alertbot.callback, "logger", autospec=True)
async def test_decryption_failure(self, fake_logger) -> None:
fake_megolm_event = Mock(spec=nio.MegolmEvent)
fake_megolm_event.sender = "@some_other_fake_user:example.com"
fake_megolm_event.event_id = "some event id"
await self.callbacks.decryption_failure(self.fake_room, fake_megolm_event)
fake_logger.error.assert_called_once()
@patch.object(matrix_alertbot.callback, "logger", autospec=True)
async def test_decryption_failure_in_unauthorized_room(self, fake_logger) -> None:
fake_megolm_event = Mock(spec=nio.MegolmEvent)
fake_megolm_event.sender = "@some_other_fake_user:example.com"
fake_megolm_event.event_id = "some event id"
self.fake_room.room_id = "!unauthorizedroom@example.com"
await self.callbacks.decryption_failure(self.fake_room, fake_megolm_event)
fake_logger.error.assert_not_called()
async def test_unknown_message(self) -> None:
fake_room_unknown_event = Mock(spec=nio.RoomMessageUnknown)
fake_room_unknown_event.source = {
"content": {
"msgtype": "m.key.verification.request",
"methods": ["m.sas.v1"],
}
}
fake_room_unknown_event.event_id = "some event id"
await self.callbacks.unknown_message(self.fake_room, fake_room_unknown_event)
self.fake_matrix_client.room_send.assert_called_once_with(
self.fake_room.room_id,
"m.room.message",
{
"msgtype": "m.key.verification.ready",
"methods": ["m.sas.v1"],
"m.relates_to": {
"rel_type": "m.reference",
"event_id": fake_room_unknown_event.event_id,
},
},
)
async def test_unknown_message_with_msgtype_not_verification_request(self) -> None:
fake_room_unknown_event = Mock(spec=nio.RoomMessageUnknown)
fake_room_unknown_event.source = {
"content": {
"msgtype": "unknown",
"methods": ["m.sas.v1"],
}
}
fake_room_unknown_event.event_id = "some event id"
await self.callbacks.unknown_message(self.fake_room, fake_room_unknown_event)
self.fake_matrix_client.room_send.assert_not_called()
async def test_unknown_message_with_method_not_sas_v1(self) -> None:
fake_room_unknown_event = Mock(spec=nio.RoomMessageUnknown)
fake_room_unknown_event.source = {
"content": {
"msgtype": "m.key.verification.request",
"methods": [],
}
}
fake_room_unknown_event.event_id = "some event id"
await self.callbacks.unknown_message(self.fake_room, fake_room_unknown_event)
self.fake_matrix_client.room_send.assert_not_called()
if __name__ == "__main__":
unittest.main()

View file

@ -1,5 +1,4 @@
import unittest
from typing import Any, Dict, Optional
from unittest.mock import Mock
import nio
@ -11,16 +10,6 @@ from matrix_alertbot.chat_functions import (
)
async def send_room_raise_send_retry_error(
room_id: str,
message_type: str,
content: Dict[Any, Any],
tx_id: Optional[str] = None,
ignore_unverified_devices: bool = False,
) -> nio.RoomSendResponse:
raise nio.SendRetryError
class ChatFunctionsTestCase(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
pass
@ -179,7 +168,7 @@ class ChatFunctionsTestCase(unittest.IsolatedAsyncioTestCase):
async def test_send_text_to_room_raise_send_retry_error(self) -> None:
fake_matrix_client = Mock(spec=nio.AsyncClient)
fake_matrix_client.room_send.side_effect = send_room_raise_send_retry_error
fake_matrix_client.room_send.side_effect = nio.exceptions.SendRetryError
fake_room_id = "!abcdefgh:example.com"
fake_plaintext_body = "some plaintext message"

View file

@ -1,10 +1,12 @@
import os
import sys
import unittest
from datetime import timedelta
from unittest.mock import Mock, patch
import yaml
import matrix_alertbot.config
from matrix_alertbot.config import DEFAULT_REACTIONS, Config
from matrix_alertbot.errors import (
InvalidConfigError,
@ -38,8 +40,15 @@ class ConfigTestCase(unittest.TestCase):
@patch("os.path.isdir")
@patch("os.path.exists")
@patch("os.mkdir")
@patch.object(matrix_alertbot.config, "logger", autospec=True)
@patch.object(matrix_alertbot.config, "logging", autospec=True)
def test_read_minimal_config(
self, fake_mkdir: Mock, fake_path_exists: Mock, fake_path_isdir: Mock
self,
fake_logging: Mock,
fake_logger: Mock,
fake_mkdir: Mock,
fake_path_exists: Mock,
fake_path_isdir: Mock,
) -> None:
fake_path_isdir.return_value = False
fake_path_exists.return_value = False
@ -51,6 +60,11 @@ class ConfigTestCase(unittest.TestCase):
fake_path_exists.assert_called_once_with("data/store")
fake_mkdir.assert_called_once_with("data/store")
fake_logger.setLevel.assert_called_once_with("INFO")
fake_logger.addHandler.assert_called_once()
fake_logging.StreamHandler.return_value.setLevel("INFO")
fake_logging.StreamHandler.assert_called_once_with(sys.stdout)
self.assertEqual({"@fakes_user:matrix.example.com"}, config.user_ids)
self.assertEqual(1, len(config.accounts))
self.assertEqual("password", config.accounts[0].password)
@ -82,8 +96,15 @@ class ConfigTestCase(unittest.TestCase):
@patch("os.path.isdir")
@patch("os.path.exists")
@patch("os.mkdir")
@patch.object(matrix_alertbot.config, "logger", autospec=True)
@patch.object(matrix_alertbot.config, "logging", autospec=True)
def test_read_full_config(
self, fake_mkdir: Mock, fake_path_exists: Mock, fake_path_isdir: Mock
self,
fake_logging: Mock,
fake_logger: Mock,
fake_mkdir: Mock,
fake_path_exists: Mock,
fake_path_isdir: Mock,
) -> None:
fake_path_isdir.return_value = False
fake_path_exists.return_value = False
@ -95,6 +116,11 @@ class ConfigTestCase(unittest.TestCase):
fake_path_exists.assert_called_once_with("data/store")
fake_mkdir.assert_called_once_with("data/store")
fake_logger.setLevel.assert_called_once_with("DEBUG")
fake_logger.addHandler.assert_called_once()
fake_logging.FileHandler.return_value.setLevel("DEBUG")
fake_logging.FileHandler.assert_called_once_with("fake.log")
self.assertEqual(
{"@fakes_user:matrix.example.com", "@other_user:matrix.domain.tld"},
config.user_ids,
@ -333,6 +359,60 @@ class ConfigTestCase(unittest.TestCase):
with self.assertRaises(InvalidConfigError):
config._parse_config_values()
@patch("os.path.isdir")
@patch("os.path.exists")
@patch("os.mkdir")
@patch.object(matrix_alertbot.config, "logger")
def test_parse_config_with_both_logging_disabled(
self,
fake_logger: Mock,
fake_mkdir: Mock,
fake_path_exists: Mock,
fake_path_isdir: Mock,
) -> None:
fake_path_isdir.return_value = False
fake_path_exists.return_value = False
config_path = os.path.join(CONFIG_RESOURCES_DIR, "config.full.yml")
config = DummyConfig(config_path)
config.config_dict["logging"]["file_logging"]["enabled"] = False
config.config_dict["logging"]["console_logging"]["enabled"] = False
config._parse_config_values()
fake_logger.addHandler.assert_not_called()
fake_logger.setLevel.assert_called_once_with("DEBUG")
@patch("os.path.isdir")
@patch("os.path.exists")
@patch("os.mkdir")
@patch.object(matrix_alertbot.config, "logger", autospec=True)
@patch.object(matrix_alertbot.config, "logging", autospec=True)
def test_parse_config_with_level_logging_different(
self,
fake_logging: Mock,
fake_logger: Mock,
fake_mkdir: Mock,
fake_path_exists: Mock,
fake_path_isdir: Mock,
) -> None:
fake_path_isdir.return_value = False
fake_path_exists.return_value = False
config_path = os.path.join(CONFIG_RESOURCES_DIR, "config.full.yml")
config = DummyConfig(config_path)
config.config_dict["logging"]["file_logging"]["enabled"] = True
config.config_dict["logging"]["file_logging"]["level"] = "WARN"
config.config_dict["logging"]["console_logging"]["enabled"] = True
config.config_dict["logging"]["console_logging"]["level"] = "ERROR"
config._parse_config_values()
self.assertEqual(2, fake_logger.addHandler.call_count)
fake_logger.setLevel.assert_called_once_with("DEBUG")
fake_logging.FileHandler.return_value.setLevel.assert_called_with("WARN")
fake_logging.StreamHandler.return_value.setLevel.assert_called_with("ERROR")
if __name__ == "__main__":
unittest.main()

302
tests/test_matrix.py Normal file
View file

@ -0,0 +1,302 @@
from __future__ import annotations
import random
import unittest
from unittest.mock import Mock, call, patch
import nio
from diskcache import Cache
import matrix_alertbot
import matrix_alertbot.matrix
from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.config import AccountConfig, Config
from matrix_alertbot.matrix import MatrixClientPool
def mock_create_matrix_client(
matrix_client_pool: MatrixClientPool,
account: AccountConfig,
alertmanager_client: AlertmanagerClient,
cache: Cache,
config: Config,
) -> nio.AsyncClient:
fake_matrix_client = Mock(spec=nio.AsyncClient)
fake_matrix_client.logged_in = True
return fake_matrix_client
class FakeAsyncClientConfig:
def __init__(
self,
max_limit_exceeded: int,
max_timeouts: int,
store_sync_tokens: bool,
encryption_enabled: bool,
) -> None:
if encryption_enabled:
raise ImportWarning()
self.max_limit_exceeded = max_limit_exceeded
self.max_timeouts = max_timeouts
self.store_sync_tokens = store_sync_tokens
self.encryption_enabled = encryption_enabled
class MatrixClientPoolTestCase(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None:
random.seed(42)
self.fake_alertmanager_client = Mock(spec=AlertmanagerClient)
self.fake_cache = Mock(spec=Cache)
self.fake_account_config_1 = Mock(spec=AccountConfig)
self.fake_account_config_1.id = "@fake_user:matrix.example.com"
self.fake_account_config_1.homeserver_url = "https://matrix.example.com"
self.fake_account_config_1.device_id = "ABCDEFGH"
self.fake_account_config_1.token_file = "account1.token.secret"
self.fake_account_config_2 = Mock(spec=AccountConfig)
self.fake_account_config_2.id = "@other_user:chat.example.com"
self.fake_account_config_2.homeserver_url = "https://chat.example.com"
self.fake_account_config_2.device_id = "IJKLMNOP"
self.fake_account_config_2.token_file = "account2.token.secret"
self.fake_config = Mock(spec=Config)
self.fake_config.store_dir = "/dev/null"
self.fake_config.command_prefix = "!alert"
self.fake_config.accounts = [
self.fake_account_config_1,
self.fake_account_config_2,
]
@patch.object(
matrix_alertbot.matrix.MatrixClientPool, "_create_matrix_client", autospec=True
)
async def test_init_matrix_client_pool(self, fake_create_matrix_client) -> None:
fake_matrix_client = Mock(spec=nio.AsyncClient)
fake_create_matrix_client.return_value = fake_matrix_client
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
fake_create_matrix_client.assert_has_calls(
[
call(
matrix_client_pool,
self.fake_account_config_1,
self.fake_alertmanager_client,
self.fake_cache,
self.fake_config,
),
call(
matrix_client_pool,
self.fake_account_config_2,
self.fake_alertmanager_client,
self.fake_cache,
self.fake_config,
),
]
)
self.assertEqual(self.fake_account_config_1, matrix_client_pool.account)
self.assertEqual(fake_matrix_client, matrix_client_pool.matrix_client)
self.assertEqual(2, len(matrix_client_pool._accounts))
self.assertEqual(2, len(matrix_client_pool._matrix_clients))
@patch.object(
matrix_alertbot.matrix.MatrixClientPool, "_create_matrix_client", autospec=True
)
async def test_close_matrix_client_pool(self, fake_create_matrix_client) -> None:
fake_matrix_client = Mock(spec=nio.AsyncClient)
fake_create_matrix_client.return_value = fake_matrix_client
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
await matrix_client_pool.close()
fake_matrix_client.close.assert_has_calls([(call(), call())])
@patch.object(
matrix_alertbot.matrix.MatrixClientPool,
"_create_matrix_client",
autospec=True,
side_effect=mock_create_matrix_client,
)
async def test_switch_active_client(self, fake_create_matrix_client) -> None:
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
fake_matrix_client_1 = matrix_client_pool.matrix_client
await matrix_client_pool.switch_active_client()
fake_matrix_client_2 = matrix_client_pool.matrix_client
self.assertEqual(self.fake_account_config_2, matrix_client_pool.account)
self.assertNotEqual(fake_matrix_client_2, fake_matrix_client_1)
await matrix_client_pool.switch_active_client()
fake_matrix_client_3 = matrix_client_pool.matrix_client
self.assertEqual(self.fake_account_config_1, matrix_client_pool.account)
self.assertEqual(fake_matrix_client_3, fake_matrix_client_1)
@patch.object(
matrix_alertbot.matrix.MatrixClientPool,
"_create_matrix_client",
autospec=True,
side_effect=mock_create_matrix_client,
)
async def test_switch_active_client_with_whoami_raise_exception(
self, fake_create_matrix_client
) -> None:
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
for fake_matrix_client in matrix_client_pool._matrix_clients.values():
fake_matrix_client.whoami.side_effect = Exception
fake_matrix_client_1 = matrix_client_pool.matrix_client
await matrix_client_pool.switch_active_client()
fake_matrix_client_2 = matrix_client_pool.matrix_client
self.assertEqual(self.fake_account_config_1, matrix_client_pool.account)
self.assertEqual(fake_matrix_client_2, fake_matrix_client_1)
@patch.object(
matrix_alertbot.matrix.MatrixClientPool,
"_create_matrix_client",
autospec=True,
side_effect=mock_create_matrix_client,
)
async def test_switch_active_client_with_whoami_error(
self, fake_create_matrix_client
) -> None:
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
for fake_matrix_client in matrix_client_pool._matrix_clients.values():
fake_matrix_client.whoami.return_value = Mock(
spec=nio.responses.WhoamiError
)
fake_matrix_client_1 = matrix_client_pool.matrix_client
await matrix_client_pool.switch_active_client()
fake_matrix_client_2 = matrix_client_pool.matrix_client
self.assertEqual(self.fake_account_config_1, matrix_client_pool.account)
self.assertEqual(fake_matrix_client_2, fake_matrix_client_1)
@patch.object(
matrix_alertbot.matrix.MatrixClientPool,
"_create_matrix_client",
autospec=True,
side_effect=mock_create_matrix_client,
)
async def test_switch_active_client_with_whoami_error_and_not_logged_in(
self, fake_create_matrix_client
) -> None:
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
for fake_matrix_client in matrix_client_pool._matrix_clients.values():
fake_matrix_client.whoami.return_value = Mock(
spec=nio.responses.WhoamiError
)
fake_matrix_client.logged_in = False
fake_matrix_client_1 = matrix_client_pool.matrix_client
await matrix_client_pool.switch_active_client()
fake_matrix_client_2 = matrix_client_pool.matrix_client
self.assertEqual(self.fake_account_config_1, matrix_client_pool.account)
self.assertEqual(fake_matrix_client_2, fake_matrix_client_1)
@patch.object(
matrix_alertbot.matrix, "AsyncClientConfig", spec=nio.AsyncClientConfig
)
async def test_create_matrix_client(self, fake_async_client_config: Mock) -> None:
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
matrix_client_1 = matrix_client_pool._matrix_clients[self.fake_account_config_1]
self.assertEqual(self.fake_account_config_1.id, matrix_client_1.user)
self.assertEqual(
self.fake_account_config_1.device_id, matrix_client_1.device_id
)
self.assertEqual(
self.fake_account_config_1.homeserver_url, matrix_client_1.homeserver
)
self.assertEqual(self.fake_config.store_dir, matrix_client_1.store_path)
self.assertEqual(6, len(matrix_client_1.event_callbacks))
self.assertEqual(4, len(matrix_client_1.to_device_callbacks))
fake_async_client_config.assert_has_calls(
[
call(
max_limit_exceeded=5,
max_timeouts=3,
store_sync_tokens=True,
encryption_enabled=True,
),
call(
max_limit_exceeded=5,
max_timeouts=3,
store_sync_tokens=True,
encryption_enabled=True,
),
]
)
@patch.object(
matrix_alertbot.matrix,
"AsyncClientConfig",
spec=nio.AsyncClientConfig,
side_effect=FakeAsyncClientConfig,
)
async def test_create_matrix_client_with_encryption_disabled(
self, fake_async_client_config: Mock
) -> None:
matrix_client_pool = MatrixClientPool(
alertmanager_client=self.fake_alertmanager_client,
cache=self.fake_cache,
config=self.fake_config,
)
matrix_client_1 = matrix_client_pool._matrix_clients[self.fake_account_config_1]
self.assertEqual(self.fake_account_config_1.id, matrix_client_1.user)
self.assertEqual(
self.fake_account_config_1.device_id, matrix_client_1.device_id
)
self.assertEqual(
self.fake_account_config_1.homeserver_url, matrix_client_1.homeserver
)
self.assertEqual(self.fake_config.store_dir, matrix_client_1.store_path)
self.assertEqual(6, len(matrix_client_1.event_callbacks))
self.assertEqual(4, len(matrix_client_1.to_device_callbacks))
self.assertEqual(5, matrix_client_1.config.max_limit_exceeded)
self.assertEqual(3, matrix_client_1.config.max_timeouts)
self.assertTrue(matrix_client_1.config.store_sync_tokens)
self.assertFalse(matrix_client_1.config.encryption_enabled)
if __name__ == "__main__":
unittest.main()

View file

@ -4,27 +4,21 @@ from unittest.mock import Mock, call, patch
import aiohttp.test_utils
import nio
from aiohttp import web
from aiohttp import web, web_request
from diskcache import Cache
from nio.exceptions import LocalProtocolError
from nio.responses import RoomSendResponse
import matrix_alertbot.webhook
from matrix_alertbot.alert import Alert, AlertRenderer
from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.config import Config
from matrix_alertbot.errors import (
AlertmanagerError,
MatrixClientError,
SilenceExtendError,
SilenceNotFoundError,
)
from matrix_alertbot.matrix import MatrixClientPool
from matrix_alertbot.webhook import Webhook
def send_text_to_room_raise_error(
client: nio.AsyncClient, room_id: str, plaintext: str, html: str, notice: bool
) -> RoomSendResponse:
raise LocalProtocolError
from matrix_alertbot.webhook import Webhook, create_alert
def update_silence_raise_silence_not_found(fingerprint: str) -> str:
@ -45,6 +39,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
self.fake_matrix_client_pool = Mock(spec=MatrixClientPool)
self.fake_matrix_client_pool.matrix_client = self.fake_matrix_client
self.fake_alertmanager_client = Mock(spec=AlertmanagerClient)
self.fake_alert_renderer = Mock(spec=AlertRenderer)
self.fake_cache = Mock(spec=Cache)
self.fake_room_id = "!abcdefg:example.com"
@ -57,9 +52,16 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
self.fake_config.cache_expire_time = 0
self.fake_config.template_dir = None
self.fake_alerts = {
"alerts": [
{
self.fake_request = Mock(spec=web_request.Request)
self.fake_request.app = {
"alertmanager_client": self.fake_alertmanager_client,
"alert_renderer": self.fake_alert_renderer,
"matrix_client_pool": self.fake_matrix_client_pool,
"cache": self.fake_cache,
"config": self.fake_config,
}
self.fake_alert_1 = {
"fingerprint": "fingerprint1",
"generatorURL": "http://example.com/alert1",
"status": "firing",
@ -69,8 +71,8 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
"job": "job1",
},
"annotations": {"description": "some description1"},
},
{
}
self.fake_alert_2 = {
"fingerprint": "fingerprint2",
"generatorURL": "http://example.com/alert2",
"status": "resolved",
@ -80,7 +82,11 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
"job": "job2",
},
"annotations": {"description": "some description2"},
},
}
self.fake_alerts = {
"alerts": [
self.fake_alert_1,
self.fake_alert_2,
]
}
@ -314,13 +320,14 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
self.fake_cache.set.assert_not_called()
self.fake_cache.delete.assert_not_called()
@patch.object(matrix_alertbot.webhook, "logger", autospec=True)
@patch.object(
matrix_alertbot.webhook,
"send_text_to_room",
side_effect=send_text_to_room_raise_error,
side_effect=nio.exceptions.LocalProtocolError("Local protocol error"),
)
async def test_post_alerts_raise_send_error(
self, fake_send_text_to_room: Mock
self, fake_send_text_to_room: Mock, fake_logger: Mock
) -> None:
self.fake_alertmanager_client.update_silence.side_effect = (
update_silence_raise_silence_not_found
@ -341,6 +348,178 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
self.fake_cache.set.assert_not_called()
self.fake_cache.delete.assert_called_once_with("fingerprint1")
fake_logger.error.assert_called_once_with(
"Unable to send alert fingerprint1 to Matrix room !abcdefg:example.com: Local protocol error"
)
@patch.object(matrix_alertbot.webhook, "logger", autospec=True)
@patch.object(
matrix_alertbot.webhook,
"create_alert",
side_effect=MatrixClientError("Matrix client error"),
)
async def test_post_alerts_raise_matrix_client_error(
self, fake_create_alert: Mock, fake_logger: Mock
) -> None:
self.fake_alertmanager_client.update_silence.side_effect = (
update_silence_raise_silence_not_found
)
data = self.fake_alerts
async with self.client.request(
"POST", f"/alerts/{self.fake_room_id}", json=data
) as response:
self.assertEqual(500, response.status)
error_msg = await response.text()
self.assertEqual(
"An error occured when sending alert with fingerprint 'fingerprint1' to Matrix room.",
error_msg,
)
fake_create_alert.assert_called_once()
fake_logger.error.assert_called_once_with(
"Unable to send alert fingerprint1 to Matrix room !abcdefg:example.com: Matrix client error"
)
@patch.object(matrix_alertbot.webhook, "logger", autospec=True)
@patch.object(
matrix_alertbot.webhook,
"send_text_to_room",
side_effect=Exception("Exception"),
)
async def test_post_alerts_raise_exception(
self, fake_send_text_to_room: Mock, fake_logger: Mock
) -> None:
self.fake_alertmanager_client.update_silence.side_effect = (
update_silence_raise_silence_not_found
)
data = self.fake_alerts
async with self.client.request(
"POST", f"/alerts/{self.fake_room_id}", json=data
) as response:
self.assertEqual(500, response.status)
error_msg = await response.text()
self.assertEqual(
"An exception occured when sending alert with fingerprint 'fingerprint1' to Matrix room.",
error_msg,
)
fake_send_text_to_room.assert_called_once()
self.fake_cache.set.assert_not_called()
self.fake_cache.delete.assert_called_once_with("fingerprint1")
fake_logger.error.assert_called_once_with(
"Unable to send alert fingerprint1 to Matrix room !abcdefg:example.com: Exception"
)
async def test_create_alert_update_silence(self) -> None:
fake_alert = Mock(spec=Alert)
fake_alert.firing = True
fake_alert.fingerprint = "fingerprint"
await create_alert(fake_alert, self.fake_room_id, self.fake_request)
self.fake_alertmanager_client.update_silence.assert_called_once_with(
fake_alert.fingerprint
)
self.fake_alert_renderer.render.assert_not_called()
@patch.object(matrix_alertbot.webhook, "send_text_to_room", autospec=True)
async def test_create_alert_with_silence_not_found_error(
self, fake_send_text_to_room: Mock
) -> None:
fake_alert = Mock(spec=Alert)
fake_alert.firing = True
fake_alert.fingerprint = "fingerprint"
self.fake_alertmanager_client.update_silence.side_effect = SilenceNotFoundError
await create_alert(fake_alert, self.fake_room_id, self.fake_request)
self.fake_alertmanager_client.update_silence.assert_called_once_with(
fake_alert.fingerprint
)
self.fake_alert_renderer.render.assert_has_calls(
[call(fake_alert, html=False), call(fake_alert, html=True)]
)
fake_send_text_to_room.assert_called_once()
self.fake_cache.set.assert_called_once_with(
fake_send_text_to_room.return_value.event_id,
fake_alert.fingerprint,
expire=self.fake_config.cache_expire_time,
)
self.fake_cache.delete.assert_called_once_with(fake_alert.fingerprint)
@patch.object(matrix_alertbot.webhook, "send_text_to_room", autospec=True)
async def test_create_alert_with_silence_extend_error(
self, fake_send_text_to_room: Mock
) -> None:
fake_alert = Mock(spec=Alert)
fake_alert.firing = True
fake_alert.fingerprint = "fingerprint"
self.fake_alertmanager_client.update_silence.side_effect = SilenceExtendError
await create_alert(fake_alert, self.fake_room_id, self.fake_request)
self.fake_alertmanager_client.update_silence.assert_called_once_with(
fake_alert.fingerprint
)
self.fake_alert_renderer.render.assert_has_calls(
[call(fake_alert, html=False), call(fake_alert, html=True)]
)
fake_send_text_to_room.assert_called_once()
self.fake_cache.set.assert_called_once_with(
fake_send_text_to_room.return_value.event_id,
fake_alert.fingerprint,
expire=self.fake_config.cache_expire_time,
)
self.fake_cache.delete.assert_not_called()
@patch.object(matrix_alertbot.webhook, "send_text_to_room", autospec=True)
async def test_create_alert_not_firing(self, fake_send_text_to_room: Mock) -> None:
fake_alert = Mock(spec=Alert)
fake_alert.firing = False
fake_alert.fingerprint = "fingerprint"
await create_alert(fake_alert, self.fake_room_id, self.fake_request)
self.fake_alertmanager_client.update_silence.assert_not_called()
self.fake_alert_renderer.render.assert_has_calls(
[call(fake_alert, html=False), call(fake_alert, html=True)]
)
fake_send_text_to_room.assert_called_once()
self.fake_cache.set.assert_not_called()
self.fake_cache.delete.assert_called_once_with(fake_alert.fingerprint)
@patch.object(matrix_alertbot.webhook, "send_text_to_room", autospec=True)
async def test_create_alert_not_firing_raise_matrix_client_error(
self, fake_send_text_to_room: Mock
) -> None:
fake_alert = Mock(spec=Alert)
fake_alert.firing = False
fake_alert.fingerprint = "fingerprint"
self.fake_matrix_client_pool.matrix_client = None
with self.assertRaises(MatrixClientError):
await create_alert(fake_alert, self.fake_room_id, self.fake_request)
self.fake_alertmanager_client.update_silence.assert_not_called()
self.fake_alert_renderer.render.assert_has_calls(
[call(fake_alert, html=False), call(fake_alert, html=True)]
)
fake_send_text_to_room.assert_not_called()
async def test_health(self) -> None:
async with self.client.request("GET", "/health") as response:
self.assertEqual(200, response.status)