import unittest from typing import List from unittest.mock import MagicMock, Mock, call, patch import nio from diskcache import Cache import matrix_alertbot.callback import matrix_alertbot.command from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.command import ( AckAlertCommand, CommandFactory, HelpCommand, UnackAlertCommand, UnknownCommand, ) from matrix_alertbot.errors import ( AlertmanagerError, AlertNotFoundError, SilenceNotFoundError, ) from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher from tests.utils import make_awaitable async def create_silence_raise_alertmanager_error( fingerprint: str, duration: str, user: str, matchers: List[AlertMatcher] ) -> str: if fingerprint == "fingerprint1": raise AlertmanagerError return "silence1" async def create_silence_raise_alert_not_found_error( fingerprint: str, duration: str, user: str, matchers: List[AlertMatcher] ) -> str: if fingerprint == "fingerprint1": raise AlertNotFoundError return "silence1" async def delete_silence_raise_alertmanager_error( fingerprint: str, matchers: List[AlertMatcher] ) -> List[str]: if fingerprint == "fingerprint1": raise AlertmanagerError return ["silence1"] async def delete_silence_raise_silence_not_found_error( fingerprint: str, matchers: List[AlertMatcher] ) -> List[str]: if fingerprint == "fingerprint1": raise SilenceNotFoundError return ["silence1"] class CommandTestCase(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: # Create a Command object and give it some Mock'd objects to use self.fake_client = Mock(spec=nio.AsyncClient) self.fake_client.user = "@fake_user:example.com" # Pretend that attempting to send a message is always successful self.fake_client.room_send.return_value = make_awaitable(None) self.fake_fingerprints = ["fingerprint1", "fingerprint2"] self.fake_silences = ["silence1", "silence2"] self.fake_cache = MagicMock(spec=Cache) self.fake_cache.__getitem__.return_value = self.fake_fingerprints self.fake_cache.__contains__.return_value = True self.fake_alertmanager = Mock(spec=AlertmanagerClient) self.fake_alertmanager.delete_silences.return_value = self.fake_silences # Create a fake room to play with self.fake_room = Mock(spec=nio.MatrixRoom) self.fake_room.room_id = "!abcdefg:example.com" self.fake_room.display_name = "Fake Room" self.fake_room.user_name.side_effect = lambda x: x self.fake_event_id = "some event id" self.fake_sender = "@some_other_fake_user:example.com" self.fake_alert_event_id = "some alert event id" # We don't spec config, as it doesn't currently have well defined attributes self.fake_config = Mock() self.fake_config.room_id = self.fake_room.room_id self.fake_config.command_prefix = "!alert " @patch.object(matrix_alertbot.command.AckAlertCommand, "process") async def test_process_ack_command(self, fake_ack: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = CommandFactory.create( "ack", self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to process the command fake_ack.assert_called_once() @patch.object(matrix_alertbot.command.UnackAlertCommand, "process") async def test_process_unack_command(self, fake_unack: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it for unack_cmd in ("unack", "nack"): command = CommandFactory.create( unack_cmd, self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to process the command fake_unack.assert_has_calls([call(), call()]) @patch.object(matrix_alertbot.command.HelpCommand, "process") async def test_process_help_command(self, fake_help: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = CommandFactory.create( "help", self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to process the command fake_help.assert_called_once() @patch.object(matrix_alertbot.command.UnknownCommand, "process") async def test_process_unknown_command(self, fake_unknown: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = CommandFactory.create( "", self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to process the command fake_unknown.assert_called_once() @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_without_duration_nor_matchers( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_has_calls( [ call(fingerprint, 86400, self.fake_sender, []) for fingerprint in self.fake_fingerprints ] ) fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "Created 2 silences with a duration of 1d.", ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_without_duration_and_with_matchers( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it matchers = [ AlertMatcher(label="alertname", value="alert1"), AlertRegexMatcher(label="severity", regex="critical"), ] command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack alertname=alert1 severity=~critical", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_has_calls( [ call( fingerprint, 86400, self.fake_sender, matchers, ) for fingerprint in self.fake_fingerprints ] ) fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "Created 2 silences with a duration of 1d.", ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_with_duration_and_without_matchers( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack 1w 3d", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_has_calls( [ call(fingerprint, 864000, self.fake_sender, []) for fingerprint in self.fake_fingerprints ] ) fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "Created 2 silences with a duration of 1w 3d.", ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_with_duration_and_matchers( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it matchers = [ AlertMatcher(label="alertname", value="alert1"), AlertMatcher(label="severity", value="critical"), ] command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack 1w 3d alertname=alert1 severity=critical", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_has_calls( [ call( fingerprint, 864000, self.fake_sender, matchers, ) for fingerprint in self.fake_fingerprints ] ) fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "Created 2 silences with a duration of 1w 3d.", ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_raise_alertmanager_error( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) self.fake_alertmanager.create_silence.side_effect = ( create_silence_raise_alertmanager_error ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_has_calls( [ call(fingerprint, 86400, self.fake_sender, []) for fingerprint in self.fake_fingerprints ] ) fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "Created 1 silences with a duration of 1d.", ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_raise_alert_not_found_error( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) self.fake_alertmanager.create_silence.side_effect = ( create_silence_raise_alert_not_found_error ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_has_calls( [ call(fingerprint, 86400, self.fake_sender, []) for fingerprint in self.fake_fingerprints ] ) fake_send_text_to_room.assert_has_calls( [ call( self.fake_client, self.fake_room.room_id, "Sorry, I couldn't find 1 alerts, therefore I couldn't create their silence.", ), call( self.fake_client, self.fake_room.room_id, "Created 1 silences with a duration of 1d.", ), ] ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_with_invalid_duration( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack invalid duration", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_not_called() fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "I tried really hard, but I can't convert the duration 'invalid duration' to a number of seconds.", ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_ack_with_event_not_found_in_cache( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it self.fake_cache.__contains__.return_value = False command = AckAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "ack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_not_called() fake_send_text_to_room.assert_not_called() @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unack_without_matchers(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = UnackAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "unack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.delete_silences.assert_has_calls( [call(fingerprint, []) for fingerprint in self.fake_fingerprints] ) fake_send_text_to_room.assert_called_with( self.fake_client, self.fake_room.room_id, "Removed 4 silences." ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unack_with_matchers(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it matchers = [ AlertMatcher(label="alertname", value="alert1"), AlertRegexMatcher(label="severity", regex="critical"), ] command = UnackAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "unack alertname=alert1 severity=~critical", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.delete_silences.assert_has_calls( [call(fingerprint, matchers) for fingerprint in self.fake_fingerprints] ) fake_send_text_to_room.assert_called_with( self.fake_client, self.fake_room.room_id, "Removed 4 silences." ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unack_silence_raise_alertmanager_error( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = UnackAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "unack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) self.fake_alertmanager.delete_silences.side_effect = ( delete_silence_raise_alertmanager_error ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.delete_silences.assert_has_calls( [call(fingerprint, []) for fingerprint in self.fake_fingerprints] ) fake_send_text_to_room.assert_called_with( self.fake_client, self.fake_room.room_id, "Removed 1 silences." ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unack_raise_silence_not_found_error( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = UnackAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "unack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) self.fake_alertmanager.delete_silences.side_effect = ( delete_silence_raise_silence_not_found_error ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.delete_silences.assert_has_calls( [call(fingerprint, []) for fingerprint in self.fake_fingerprints] ) fake_send_text_to_room.assert_has_calls( [ call( self.fake_client, self.fake_room.room_id, "Sorry, I couldn't find 1 alerts, therefore I couldn't remove their silences.", ), call( self.fake_client, self.fake_room.room_id, "Removed 1 silences.", ), ] ) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unack_with_event_not_found_in_cache( self, fake_send_text_to_room: Mock ) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it self.fake_cache.__contains__.return_value = False command = UnackAlertCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "unack", self.fake_room, self.fake_sender, self.fake_event_id, self.fake_alert_event_id, ) await command.process() # Check that we attempted to create silences self.fake_alertmanager.create_silence.assert_not_called() fake_send_text_to_room.assert_not_called() @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_help_without_topic(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "help", self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to create silences fake_send_text_to_room.assert_called_once() _, _, text = fake_send_text_to_room.call_args.args self.assertIn("help commands", text) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_help_with_rules_topic(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "help rules", self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to create silences fake_send_text_to_room.assert_called_once() _, _, text = fake_send_text_to_room.call_args.args self.assertIn("rules!", text) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_help_with_commands_topic(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "help commands", self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to create silences fake_send_text_to_room.assert_called_once() _, _, text = fake_send_text_to_room.call_args.args self.assertIn("Available commands", text) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_help_with_unknown_topic(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "help unknown", self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to create silences fake_send_text_to_room.assert_called_once() _, _, text = fake_send_text_to_room.call_args.args self.assertEqual("Unknown help topic!", text) @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unknown_command(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" # Tests that the bot attempts to join a room after being invited to it command = UnknownCommand( self.fake_client, self.fake_cache, self.fake_alertmanager, self.fake_config, "", self.fake_room, self.fake_sender, self.fake_event_id, ) await command.process() # Check that we attempted to create silences fake_send_text_to_room.assert_called_once_with( self.fake_client, self.fake_room.room_id, "Unknown command ''. Try the 'help' command for more information.", ) if __name__ == "__main__": unittest.main()