diff --git a/matrix_alertbot/alertmanager.py b/matrix_alertbot/alertmanager.py index 304861a..9cab57a 100644 --- a/matrix_alertbot/alertmanager.py +++ b/matrix_alertbot/alertmanager.py @@ -12,10 +12,12 @@ from matrix_alertbot.errors import ( AlertmanagerServerError, AlertNotFoundError, SilenceExpiredError, + SilenceExtendError, SilenceNotFoundError, ) -MAX_DURATION_DAYS = 3652 +DEFAULT_DURATION = timedelta(hours=3) +MAX_DURATION = timedelta(days=3652) class AlertmanagerClient: @@ -60,7 +62,6 @@ class AlertmanagerClient: fingerprint: str, user: str, duration_seconds: Optional[int] = None, - silence_id: Optional[str] = None, ) -> str: alert = await self.get_alert(fingerprint) @@ -69,13 +70,50 @@ class AlertmanagerClient: for label, value in alert["labels"].items() ] - start_time = datetime.now() - max_duration = timedelta(days=MAX_DURATION_DAYS) - if duration_seconds is None or duration_seconds > max_duration.total_seconds(): - end_time = start_time + max_duration + return await self._create_or_update_silence( + fingerprint, silence_matchers, user, duration_seconds + ) + + async def update_silence(self, fingerprint: str) -> str: + try: + silence_id: Optional[str] + expire_time: Optional[int] + silence_id, expire_time = self.cache.get(fingerprint, expire_time=True) + except TypeError: + silence_id = None + + if silence_id is None: + raise SilenceNotFoundError( + f"Cannot find silence for alert with fingerprint {fingerprint} in cache." + ) + + if expire_time is not None: + raise SilenceExtendError( + f"Cannot extend silence ID {silence_id} with static duration." + ) + + silence = await self.get_silence(silence_id) + user = silence["createdBy"] + silence_matchers = silence["matchers"] + + return await self._create_or_update_silence(fingerprint, silence_matchers, user) + + async def _create_or_update_silence( + self, + fingerprint: str, + silence_matchers: List, + user: str, + duration_seconds: Optional[int] = None, + silence_id: Optional[str] = None, + ) -> str: + if duration_seconds is None: + duration_delta = DEFAULT_DURATION + elif duration_seconds > MAX_DURATION.total_seconds(): + duration_delta = MAX_DURATION else: duration_delta = timedelta(seconds=duration_seconds) - end_time = start_time + duration_delta + start_time = datetime.now() + end_time = start_time + duration_delta silence = { "id": silence_id, @@ -97,6 +135,8 @@ class AlertmanagerClient: f"Cannot create silence for alert fingerprint {fingerprint}" ) from e + self.cache.set(fingerprint, data["silenceID"], expire=duration_seconds) + return data["silenceID"] async def delete_silence(self, silence_id: str) -> None: diff --git a/matrix_alertbot/callback.py b/matrix_alertbot/callback.py index f984e35..25d8d37 100644 --- a/matrix_alertbot/callback.py +++ b/matrix_alertbot/callback.py @@ -24,8 +24,8 @@ logger = logging.getLogger(__name__) class Callbacks: def __init__( self, - client: AsyncClient, - alertmanager: AlertmanagerClient, + matrix_client: AsyncClient, + alertmanager_client: AlertmanagerClient, cache: Cache, config: Config, ): @@ -39,9 +39,9 @@ class Callbacks: config: Bot configuration parameters. """ - self.client = client + self.matrix_client = matrix_client self.cache = cache - self.alertmanager = alertmanager + self.alertmanager_client = alertmanager_client self.config = config self.command_prefix = config.command_prefix @@ -54,7 +54,7 @@ class Callbacks: event: The event defining the message. """ # Ignore messages from ourselves - if event.sender == self.client.user: + if event.sender == self.matrix_client.user: return # Ignore messages from unauthorized room @@ -91,9 +91,9 @@ class Callbacks: try: command = CommandFactory.create( cmd, - self.client, + self.matrix_client, self.cache, - self.alertmanager, + self.alertmanager_client, self.config, room, event.sender, @@ -122,7 +122,7 @@ class Callbacks: # Attempt to join 3 times before giving up for attempt in range(3): - result = await self.client.join(room.room_id) + result = await self.matrix_client.join(room.room_id) if type(result) == JoinError: logger.error( f"Error joining room {room.room_id} (attempt %d): %s", @@ -146,7 +146,7 @@ class Callbacks: not actually our own invite event (such as the inviter's membership). This makes sure we only call `callbacks.invite` with our own invite events. """ - if event.state_key == self.client.user_id: + if event.state_key == self.matrix_client.user_id: # This is our own membership (invite) event await self.invite(room, event) @@ -167,7 +167,7 @@ class Callbacks: return # Ignore reactions from ourselves - if event.sender == self.client.user: + if event.sender == self.matrix_client.user: return reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key") @@ -178,7 +178,9 @@ class Callbacks: return # Get the original event that was reacted to - event_response = await self.client.room_get_event(room.room_id, alert_event_id) + event_response = await self.matrix_client.room_get_event( + room.room_id, alert_event_id + ) if isinstance(event_response, RoomGetEventError): logger.warning( f"Error getting event that was reacted to ({alert_event_id})" @@ -192,9 +194,9 @@ class Callbacks: # Send a message acknowledging the reaction command = AckAlertCommand( - self.client, + self.matrix_client, self.cache, - self.alertmanager, + self.alertmanager_client, self.config, room, event.sender, @@ -210,15 +212,15 @@ class Callbacks: return # Ignore redactions from ourselves - if event.sender == self.client.user: + if event.sender == self.matrix_client.user: return logger.debug(f"Received event to remove event ID {event.redacts}") command = UnackAlertCommand( - self.client, + self.matrix_client, self.cache, - self.alertmanager, + self.alertmanager_client, self.config, room, event.sender, diff --git a/matrix_alertbot/command.py b/matrix_alertbot/command.py index cc42784..a4c23d7 100644 --- a/matrix_alertbot/command.py +++ b/matrix_alertbot/command.py @@ -21,9 +21,9 @@ logger = logging.getLogger(__name__) class BaseCommand: def __init__( self, - client: AsyncClient, + matrix_client: AsyncClient, cache: Cache, - alertmanager: AlertmanagerClient, + alertmanager_client: AlertmanagerClient, config: Config, room: MatrixRoom, sender: str, @@ -49,9 +49,9 @@ class BaseCommand: event_id: The ID of the event describing the command. """ - self.client = client + self.matrix_client = matrix_client self.cache = cache - self.alertmanager = alertmanager + self.alertmanager_client = alertmanager_client self.config = config self.room = room self.sender = sender @@ -98,7 +98,7 @@ class AckAlertCommand(BaseAlertCommand): if duration_seconds is None: logger.error(f"Unable to create silence: Invalid duration '{duration}'") await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"I tried really hard, but I can't convert the duration '{duration}' to a number of seconds.", ) @@ -108,16 +108,13 @@ class AckAlertCommand(BaseAlertCommand): f"Unable to create silence: Duration must be positive, got '{duration}'" ) await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, "I can't create a silence with a negative duration!", ) return - - cache_expire_time = duration_seconds else: duration_seconds = None - cache_expire_time = self.config.cache_expire_time logger.debug( "Receiving a command to create a silence for an indefinite period" ) @@ -133,27 +130,14 @@ class AckAlertCommand(BaseAlertCommand): ) return - cached_silence_id = self.cache.get(alert_fingerprint) - if cached_silence_id is None: - logger.debug( - f"Creating silence for alert with fingerprint {alert_fingerprint}." - ) - else: - logger.debug( - f"Updating silence with ID {cached_silence_id} for alert with fingerprint {alert_fingerprint}." - ) - try: - silence_id = await self.alertmanager.create_silence( - alert_fingerprint, - self.room.user_name(self.sender), - duration_seconds, - cached_silence_id, + silence_id = await self.alertmanager_client.create_silence( + alert_fingerprint, self.room.user_name(self.sender), duration_seconds ) except AlertNotFoundError as e: logger.warning(f"Unable to create silence: {e}") await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"Sorry, I couldn't create silence for alert with fingerprint {alert_fingerprint}: {e}", ) @@ -161,18 +145,17 @@ class AckAlertCommand(BaseAlertCommand): except AlertmanagerError as e: logger.exception(f"Unable to create silence: {e}", exc_info=e) await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"Sorry, I couldn't create silence for alert with fingerprint {alert_fingerprint} " f"because something went wrong with Alertmanager: {e}", ) return - self.cache.set(self.event_id, alert_fingerprint, expire=cache_expire_time) - self.cache.set(alert_fingerprint, silence_id, expire=duration_seconds) + self.cache.set(self.event_id, alert_fingerprint, expire=duration_seconds) await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"Created silence with ID {silence_id}.", ) @@ -212,11 +195,11 @@ class UnackAlertCommand(BaseAlertCommand): ) try: - await self.alertmanager.delete_silence(silence_id) + await self.alertmanager_client.delete_silence(silence_id) except (SilenceNotFoundError, SilenceExpiredError) as e: logger.error(f"Unable to delete silence: {e}") await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"Sorry, I couldn't remove silence for alert with fingerprint {alert_fingerprint}: {e}", ) @@ -224,7 +207,7 @@ class UnackAlertCommand(BaseAlertCommand): except AlertmanagerError as e: logger.exception(f"Unable to delete silence: {e}", exc_info=e) await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"Sorry, I couldn't remove silence for alert with fingerprint {alert_fingerprint} " f"because something went wrong with Alertmanager: {e}", @@ -234,7 +217,7 @@ class UnackAlertCommand(BaseAlertCommand): self.cache.delete(alert_fingerprint) await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, f"Removed silence with ID {silence_id}.", ) @@ -249,7 +232,7 @@ class HelpCommand(BaseCommand): "Hello, I am a bot made with matrix-nio! Use `help commands` to view " "available commands." ) - await send_text_to_room(self.client, self.room.room_id, text) + await send_text_to_room(self.matrix_client, self.room.room_id, text) return topic = self.args[0] @@ -259,7 +242,7 @@ class HelpCommand(BaseCommand): text = "Available commands: ..." else: text = "Unknown help topic!" - await send_text_to_room(self.client, self.room.room_id, text) + await send_text_to_room(self.matrix_client, self.room.room_id, text) class UnknownCommand(BaseCommand): @@ -268,7 +251,7 @@ class UnknownCommand(BaseCommand): f"Sending unknown command response to room {self.room.display_name}" ) await send_text_to_room( - self.client, + self.matrix_client, self.room.room_id, "Unknown command. Try the 'help' command for more information.", ) diff --git a/matrix_alertbot/errors.py b/matrix_alertbot/errors.py index 2715b18..8e06487 100644 --- a/matrix_alertbot/errors.py +++ b/matrix_alertbot/errors.py @@ -49,6 +49,12 @@ class SilenceExpiredError(AlertmanagerError): pass +class SilenceExtendError(AlertmanagerError): + """An error encountered when a silence cannot be extended.""" + + pass + + class AlertmanagerServerError(AlertmanagerError): """An error encountered with Alertmanager server.""" diff --git a/matrix_alertbot/main.py b/matrix_alertbot/main.py index 87d0b4c..1b246df 100644 --- a/matrix_alertbot/main.py +++ b/matrix_alertbot/main.py @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) def create_matrix_client(config: Config) -> AsyncClient: # Configuration options for the AsyncClient - client_config = AsyncClientConfig( + matrix_client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, store_sync_tokens=True, @@ -36,38 +36,38 @@ def create_matrix_client(config: Config) -> AsyncClient: ) # Initialize the matrix client - client = AsyncClient( + matrix_client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, store_path=config.store_dir, - config=client_config, + config=matrix_client_config, ) if config.user_token: - client.access_token = config.user_token - client.user_id = config.user_id + matrix_client.access_token = config.user_token + matrix_client.user_id = config.user_id - return client + return matrix_client async def start_matrix_client( - client: AsyncClient, cache: Cache, config: Config + matrix_client: AsyncClient, cache: Cache, config: Config ) -> bool: # Keep trying to reconnect on failure (with some time in-between) while True: try: if config.user_token: # Use token to log in - client.load_store() + matrix_client.load_store() # Sync encryption keys with the server - if client.should_upload_keys: - await client.keys_upload() + if matrix_client.should_upload_keys: + await matrix_client.keys_upload() else: # Try to login with the configured username/password try: - login_response = await client.login( + login_response = await matrix_client.login( password=config.user_password, device_name=config.device_name, ) @@ -90,14 +90,14 @@ async def start_matrix_client( # Login succeeded! logger.info(f"Logged in as {config.user_id}") - await client.sync_forever(timeout=30000, full_state=True) + await matrix_client.sync_forever(timeout=30000, full_state=True) except (ClientConnectionError, ServerDisconnectedError, TimeoutError): logger.warning("Unable to connect to homeserver, retrying in 15s...") # Sleep so we don't bombard the server with login requests await asyncio.sleep(15) finally: - await client.close() + await matrix_client.close() def main() -> None: @@ -113,30 +113,30 @@ def main() -> None: # Read the parsed config file and create a Config object config = Config(config_path) - client = create_matrix_client(config) + matrix_client = create_matrix_client(config) # Configure the cache cache = Cache(config.cache_dir) # Configure Alertmanager client - alertmanager = AlertmanagerClient(config.alertmanager_url, cache) + alertmanager_client = AlertmanagerClient(config.alertmanager_url, cache) # Set up event callbacks - callbacks = Callbacks(client, alertmanager, cache, config) - client.add_event_callback(callbacks.message, (RoomMessageText,)) - client.add_event_callback( + callbacks = Callbacks(matrix_client, alertmanager_client, cache, config) + matrix_client.add_event_callback(callbacks.message, (RoomMessageText,)) + matrix_client.add_event_callback( callbacks.invite_event_filtered_callback, (InviteMemberEvent,) ) - client.add_event_callback(callbacks.decryption_failure, (MegolmEvent,)) - client.add_event_callback(callbacks.unknown, (UnknownEvent,)) - client.add_event_callback(callbacks.redaction, (RedactionEvent,)) + matrix_client.add_event_callback(callbacks.decryption_failure, (MegolmEvent,)) + matrix_client.add_event_callback(callbacks.unknown, (UnknownEvent,)) + matrix_client.add_event_callback(callbacks.redaction, (RedactionEvent,)) # Configure webhook server - webhook_server = Webhook(client, cache, config) + webhook_server = Webhook(matrix_client, alertmanager_client, cache, config) loop = asyncio.get_event_loop() loop.create_task(webhook_server.start()) - loop.create_task(start_matrix_client(client, cache, config)) + loop.create_task(start_matrix_client(matrix_client, cache, config)) try: loop.run_forever() @@ -144,6 +144,6 @@ def main() -> None: logger.error(e) finally: loop.run_until_complete(webhook_server.close()) - loop.run_until_complete(alertmanager.close()) - loop.run_until_complete(client.close()) + loop.run_until_complete(alertmanager_client.close()) + loop.run_until_complete(matrix_client.close()) cache.close() diff --git a/matrix_alertbot/webhook.py b/matrix_alertbot/webhook.py index d1541ed..c43035e 100644 --- a/matrix_alertbot/webhook.py +++ b/matrix_alertbot/webhook.py @@ -10,8 +10,14 @@ from diskcache import Cache from nio import AsyncClient, LocalProtocolError from matrix_alertbot.alert import Alert, AlertRenderer +from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.chat_functions import send_text_to_room from matrix_alertbot.config import Config +from matrix_alertbot.errors import ( + AlertmanagerError, + SilenceExtendError, + SilenceNotFoundError, +) logger = logging.getLogger(__name__) @@ -28,10 +34,7 @@ async def create_alerts(request: web_request.Request) -> web.Response: data = await request.json() room_id = request.match_info["room_id"] - client: AsyncClient = request.app["client"] config: Config = request.app["config"] - cache: Cache = request.app["cache"] - alert_renderer: AlertRenderer = request.app["alert_renderer"] if room_id not in config.allowed_rooms: logger.error("Cannot send alerts to room ID {room_id}.") @@ -43,33 +46,39 @@ async def create_alerts(request: web_request.Request) -> web.Response: logger.error("Received data without 'alerts' key") return web.Response(status=400, body="Data must contain 'alerts' key.") - alerts = data["alerts"] + alert_dicts = data["alerts"] if not isinstance(data["alerts"], list): - alerts_type = type(alerts).__name__ + alerts_type = type(alert_dicts).__name__ logger.error(f"Received data with invalid alerts type '{alerts_type}'.") return web.Response( status=400, body=f"Alerts must be a list, got '{alerts_type}'." ) - logger.info(f"Received {len(alerts)} alerts for room ID {room_id}: {data}") + logger.info(f"Received {len(alert_dicts)} alerts for room ID {room_id}: {data}") if len(data["alerts"]) == 0: return web.Response(status=400, body="Alerts cannot be empty.") - for alert in alerts: + alerts = [] + for alert in alert_dicts: try: alert = Alert.from_dict(alert) except KeyError as e: logger.error(f"Cannot parse alert dict: {e}") return web.Response(status=400, body=f"Invalid alert: {alert}.") + alerts.append(alert) - plaintext = alert_renderer.render(alert, html=False) - html = alert_renderer.render(alert, html=True) - + for alert in alerts: try: - event = await send_text_to_room( - client, room_id, plaintext, html, notice=False + await create_alert(alert, room_id, request) + except AlertmanagerError as e: + logger.error( + f"An error occured with Alertmanager when handling alert with fingerprint {alert.fingerprint}: {e}" + ) + return web.Response( + status=500, + body=f"An error occured with Alertmanager when handling alert with fingerprint {alert.fingerprint}.", ) except (LocalProtocolError, ClientError) as e: logger.error( @@ -80,18 +89,60 @@ async def create_alerts(request: web_request.Request) -> web.Response: body=f"An error occured when sending alert with fingerprint '{alert.fingerprint}' to Matrix room.", ) - if alert.firing: - cache.set( - event.event_id, alert.fingerprint, expire=config.cache_expire_time - ) - return web.Response(status=200) +async def create_alert( + alert: Alert, room_id: str, request: web_request.Request +) -> None: + alertmanager_client: AlertmanagerClient = request.app["alertmanager_client"] + alert_renderer: AlertRenderer = request.app["alert_renderer"] + matrix_client: AsyncClient = request.app["matrix_client"] + cache: Cache = request.app["cache"] + config: Config = request.app["config"] + + if alert.firing: + try: + silence_id = await alertmanager_client.update_silence(alert.fingerprint) + logger.debug( + f"Extended silence ID {silence_id} for alert with fingerprint {alert.fingerprint}" + ) + return + except SilenceNotFoundError as e: + logger.debug( + f"Unable to extend silence for alert with fingerprint {alert.fingerprint}: {e}" + ) + cache.delete(alert.fingerprint) + except SilenceExtendError as e: + logger.debug( + f"Unable to extend silence for alert with fingerprint {alert.fingerprint}: {e}" + ) + + plaintext = alert_renderer.render(alert, html=False) + html = alert_renderer.render(alert, html=True) + + event = await send_text_to_room( + matrix_client, room_id, plaintext, html, notice=False + ) + + if alert.firing: + cache.set(event.event_id, alert.fingerprint, expire=config.cache_expire_time) + else: + cache.delete(event.event_id) + cache.delete(alert.fingerprint) + + class Webhook: - def __init__(self, client: AsyncClient, cache: Cache, config: Config) -> None: + def __init__( + self, + matrix_client: AsyncClient, + alertmanager_client: AlertmanagerClient, + cache: Cache, + config: Config, + ) -> None: self.app = web.Application(logger=logger) - self.app["client"] = client + self.app["matrix_client"] = matrix_client + self.app["alertmanager_client"] = alertmanager_client self.app["config"] = config self.app["cache"] = cache self.app["alert_renderer"] = AlertRenderer(config.template_dir) diff --git a/tests/test_alertmanager.py b/tests/test_alertmanager.py index 97427b0..d125266 100644 --- a/tests/test_alertmanager.py +++ b/tests/test_alertmanager.py @@ -3,8 +3,8 @@ from __future__ import annotations import json import unittest from datetime import datetime, timedelta -from typing import Any -from unittest.mock import MagicMock, Mock +from typing import Any, Dict, Optional, Tuple +from unittest.mock import Mock import aiohttp import aiohttp.test_utils @@ -18,16 +18,25 @@ from matrix_alertbot.errors import ( AlertmanagerServerError, AlertNotFoundError, SilenceExpiredError, + SilenceExtendError, SilenceNotFoundError, ) -class FakeTimeDelta: - def __init__(self, seconds: int) -> None: - self.seconds = seconds +class FakeCache: + def __init__(self, cache_dict: Optional[Dict] = None) -> None: + if cache_dict is None: + cache_dict = {} + self.cache = cache_dict - def __radd__(self, other: Any) -> datetime: - return datetime.utcfromtimestamp(self.seconds) + def get( + self, key: str, expire_time: bool = False + ) -> Optional[Tuple[str, Optional[int]] | str]: + return self.cache.get(key) + + def set(self, key: str, value: str, expire: int) -> None: + self.cache[key] = value, expire + print(self.cache) class AbstractFakeAlertmanagerServer: @@ -42,8 +51,18 @@ class AbstractFakeAlertmanagerServer: ] ) self.app["silences"] = [ - {"id": "silence1", "status": {"state": "active"}}, - {"id": "silence2", "status": {"state": "expired"}}, + { + "id": "silence1", + "createdBy": "user1", + "status": {"state": "active"}, + "matchers": [], + }, + { + "id": "silence2", + "createdBy": "user2", + "status": {"state": "expired"}, + "matchers": [], + }, ] self.runner = web.AppRunner(self.app) @@ -111,8 +130,9 @@ class FakeAlertmanagerServer(AbstractFakeAlertmanagerServer): silences = self.app["silences"] silence = await request.json() - if silence["id"] is None: - silence["id"] = "silence1" + + silence_count = len(silences) + 1 + silence["id"] = f"silence{silence_count}" silence["status"] = {"state": "active"} silences.append(silence) @@ -165,17 +185,17 @@ class FakeAlertmanagerServerWithErrorDeleteSilence(FakeAlertmanagerServer): class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self.fake_fingerprints = Mock(return_value=["fingerprint1", "fingerprint2"]) - self.fake_cache = MagicMock(spec=Cache) - self.fake_cache.__getitem__ = self.fake_fingerprints async def test_get_alerts_happy(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServer() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - alerts = await alertmanager.get_alerts() + async with aiotools.closing_async(alertmanager_client): + alerts = await alertmanager_client.get_alerts() self.assertEqual( [ @@ -197,72 +217,94 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): ) async def test_get_alerts_empty(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - alerts = await alertmanager.get_alerts() + async with aiotools.closing_async(alertmanager_client): + alerts = await alertmanager_client.get_alerts() self.assertEqual([], alerts) async def test_get_alerts_raise_alertmanager_error(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(AlertmanagerServerError): - await alertmanager.get_alerts() + await alertmanager_client.get_alerts() async def test_get_silences_happy(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServer() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - silences = await alertmanager.get_silences() + async with aiotools.closing_async(alertmanager_client): + silences = await alertmanager_client.get_silences() self.assertEqual( [ - {"id": "silence1", "status": {"state": "active"}}, - {"id": "silence2", "status": {"state": "expired"}}, + { + "id": "silence1", + "createdBy": "user1", + "status": {"state": "active"}, + "matchers": [], + }, + { + "id": "silence2", + "createdBy": "user2", + "status": {"state": "expired"}, + "matchers": [], + }, ], silences, ) async def test_get_silences_empty(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - silences = await alertmanager.get_silences() + async with aiotools.closing_async(alertmanager_client): + silences = await alertmanager_client.get_silences() self.assertEqual([], silences) async def test_get_silences_raise_alertmanager_error(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(AlertmanagerServerError): - await alertmanager.get_silences() + await alertmanager_client.get_silences() async def test_get_alert_happy(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServer() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - alert = await alertmanager.get_alert("fingerprint1") + async with aiotools.closing_async(alertmanager_client): + alert = await alertmanager_client.get_alert("fingerprint1") self.assertEqual( { @@ -274,76 +316,98 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): ) async def test_get_alert_raise_alert_not_found(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(AlertNotFoundError): - await alertmanager.get_alert("fingerprint1") + await alertmanager_client.get_alert("fingerprint1") async def test_get_alert_raise_alertmanager_error(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(AlertmanagerServerError): - await alertmanager.get_alert("fingerprint1") + await alertmanager_client.get_alert("fingerprint1") async def test_get_silence_happy(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServer() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - silence1 = await alertmanager.get_silence("silence1") - silence2 = await alertmanager.get_silence("silence2") + async with aiotools.closing_async(alertmanager_client): + silence1 = await alertmanager_client.get_silence("silence1") + silence2 = await alertmanager_client.get_silence("silence2") self.assertEqual( - {"id": "silence1", "status": {"state": "active"}}, + { + "id": "silence1", + "createdBy": "user1", + "status": {"state": "active"}, + "matchers": [], + }, silence1, ) self.assertEqual( - {"id": "silence2", "status": {"state": "expired"}}, + { + "id": "silence2", + "createdBy": "user2", + "status": {"state": "expired"}, + "matchers": [], + }, silence2, ) async def test_get_silence_raise_silence_not_found(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(SilenceNotFoundError): - await alertmanager.get_silence("silence1") + await alertmanager_client.get_silence("silence1") async def test_get_silence_raise_alertmanager_error(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(AlertmanagerServerError): - await alertmanager.get_silence("silence1") + await alertmanager_client.get_silence("silence1") @freeze_time(datetime.utcfromtimestamp(0)) - async def test_create_silence(self) -> None: + async def test_create_silence_with_duration(self) -> None: + fake_cache = Mock(return_value=FakeCache()) + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - silence_id = await alertmanager.create_silence( + async with aiotools.closing_async(alertmanager_client): + silence_id = await alertmanager_client.create_silence( "fingerprint1", "user", 86400 ) - silence = await alertmanager.get_silence("silence1") + silence = await alertmanager_client.get_silence("silence1") self.assertEqual("silence1", silence_id) self.assertEqual( @@ -365,21 +429,81 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): }, silence, ) + fake_cache.set.assert_called_once_with("fingerprint1", "silence1", expire=86400) @freeze_time(datetime.utcfromtimestamp(0)) - async def test_create_silence_with_id(self) -> None: + async def test_update_silence_with_duration(self) -> None: + fake_cache = FakeCache() + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - silence_id = await alertmanager.create_silence( - "fingerprint1", "user", 86400, "silence2" - ) - silence = await alertmanager.get_silence("silence2") + async with aiotools.closing_async(alertmanager_client): + await alertmanager_client.create_silence("fingerprint1", "user", 86400) + with self.assertRaises(SilenceExtendError): + await alertmanager_client.update_silence("fingerprint1") + with self.assertRaises(SilenceNotFoundError): + await alertmanager_client.get_silence("silence2") + self.assertEqual({"fingerprint1": ("silence1", 86400)}, fake_cache.cache) - self.assertEqual("silence2", silence_id) + @freeze_time(datetime.utcfromtimestamp(0)) + async def test_create_silence_without_duration(self) -> None: + fake_cache = Mock(spec=Cache) + fake_cache.get.return_value = None + + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: + port = fake_alertmanager_server.port + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache + ) + async with aiotools.closing_async(alertmanager_client): + silence_id = await alertmanager_client.create_silence( + "fingerprint1", "user" + ) + silence = await alertmanager_client.get_silence("silence1") + + self.assertEqual("silence1", silence_id) + self.assertEqual( + { + "id": "silence1", + "status": {"state": "active"}, + "matchers": [ + { + "name": "alertname", + "value": "alert1", + "isRegex": False, + "isEqual": True, + } + ], + "createdBy": "user", + "startsAt": "1970-01-01T00:00:00", + "endsAt": "1970-01-01T03:00:00", + "comment": "Acknowledge alert from Matrix", + }, + silence, + ) + fake_cache.set.assert_called_once_with("fingerprint1", "silence1", expire=None) + + @freeze_time(datetime.utcfromtimestamp(0)) + async def test_update_silence_without_duration(self) -> None: + fake_cache = FakeCache() + + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: + port = fake_alertmanager_server.port + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache + ) + async with aiotools.closing_async(alertmanager_client): + silence1_id = await alertmanager_client.create_silence( + "fingerprint1", "user" + ) + silence2_id = await alertmanager_client.update_silence("fingerprint1") + silence = await alertmanager_client.get_silence("silence2") + + self.assertEqual("silence1", silence1_id) + self.assertEqual("silence2", silence2_id) self.assertEqual( { "id": "silence2", @@ -394,56 +518,27 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): ], "createdBy": "user", "startsAt": "1970-01-01T00:00:00", - "endsAt": "1970-01-02T00:00:00", - "comment": "Acknowledge alert from Matrix", - }, - silence, - ) - - @freeze_time(datetime.utcfromtimestamp(0)) - async def test_create_silence_with_indefinite_duration(self) -> None: - async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: - port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache - ) - async with aiotools.closing_async(alertmanager): - silence_id = await alertmanager.create_silence("fingerprint1", "user") - silence = await alertmanager.get_silence("silence1") - - self.assertEqual("silence1", silence_id) - self.assertEqual( - { - "id": "silence1", - "status": {"state": "active"}, - "matchers": [ - { - "name": "alertname", - "value": "alert1", - "isRegex": False, - "isEqual": True, - } - ], - "createdBy": "user", - "startsAt": "1970-01-01T00:00:00", - "endsAt": "1980-01-01T00:00:00", + "endsAt": "1970-01-01T03:00:00", "comment": "Acknowledge alert from Matrix", }, silence, ) + self.assertEqual({"fingerprint1": ("silence2", None)}, fake_cache.cache) @freeze_time(datetime.utcfromtimestamp(0)) async def test_create_silence_with_max_duration(self) -> None: + fake_cache = Mock(spec=Cache) + fake_cache.get.return_value = None async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - silence_id = await alertmanager.create_silence( - "fingerprint1", "user", int(timedelta.max.total_seconds()) + async with aiotools.closing_async(alertmanager_client): + silence_id = await alertmanager_client.create_silence( + "fingerprint1", "user", int(timedelta.max.total_seconds()) + 1 ) - silence = await alertmanager.get_silence("silence1") + silence = await alertmanager_client.get_silence("silence1") self.assertEqual("silence1", silence_id) self.assertEqual( @@ -467,99 +562,165 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): ) async def test_create_silence_raise_alert_not_found(self) -> None: + fake_cache = Mock(spec=Cache) + fake_cache.get.return_value = None + async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): + async with aiotools.closing_async(alertmanager_client): with self.assertRaises(AlertNotFoundError): - await alertmanager.create_silence("fingerprint1", "user") + await alertmanager_client.create_silence("fingerprint1", "user") async def test_create_silence_raise_alertmanager_error(self) -> None: + fake_cache = Mock(spec=Cache) + fake_cache.get.return_value = None + async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - await alertmanager.get_alert("fingerprint1") + async with aiotools.closing_async(alertmanager_client): + await alertmanager_client.get_alert("fingerprint1") with self.assertRaises(AlertmanagerServerError): - await alertmanager.create_silence("fingerprint1", "user") + await alertmanager_client.create_silence("fingerprint1", "user") + + async def test_update_silence_raise_silence_not_found(self) -> None: + fake_cache = FakeCache({"fingerprint1": ("silence1", None)}) + + async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: + port = fake_alertmanager_server.port + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache + ) + async with aiotools.closing_async(alertmanager_client): + with self.assertRaises(SilenceNotFoundError): + await alertmanager_client.update_silence("fingerprint1") + with self.assertRaises(SilenceNotFoundError): + await alertmanager_client.update_silence("fingerprint2") + + async def test_update_silence_raise_silence_extend_error(self) -> None: + fake_cache = FakeCache({"fingerprint1": ("silence1", 86400)}) + + async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: + port = fake_alertmanager_server.port + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache + ) + async with aiotools.closing_async(alertmanager_client): + with self.assertRaises(SilenceExtendError): + await alertmanager_client.update_silence("fingerprint1") + + async def test_update_silence_raise_alertmanager_error(self) -> None: + fake_cache = FakeCache({"fingerprint1": ("silence1", None)}) + + async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server: + port = fake_alertmanager_server.port + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache + ) + async with aiotools.closing_async(alertmanager_client): + await alertmanager_client.get_alert("fingerprint1") + + with self.assertRaises(AlertmanagerServerError): + await alertmanager_client.update_silence("fingerprint1") async def test_delete_silence(self) -> None: + fake_cache = Mock(spec=Cache) + async with FakeAlertmanagerServer() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - await alertmanager.delete_silence("silence1") - silences = await alertmanager.get_silences() - - self.assertEqual([{"id": "silence2", "status": {"state": "expired"}}], silences) - - async def test_delete_silence_raise_silence_expired(self) -> None: - async with FakeAlertmanagerServer() as fake_alertmanager_server: - port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache - ) - async with aiotools.closing_async(alertmanager): - with self.assertRaises(SilenceExpiredError): - await alertmanager.delete_silence("silence2") - silences = await alertmanager.get_silences() + async with aiotools.closing_async(alertmanager_client): + await alertmanager_client.delete_silence("silence1") + silences = await alertmanager_client.get_silences() self.assertEqual( [ - {"id": "silence1", "status": {"state": "active"}}, - {"id": "silence2", "status": {"state": "expired"}}, + { + "id": "silence2", + "createdBy": "user2", + "status": {"state": "expired"}, + "matchers": [], + } ], silences, ) + async def test_delete_silence_raise_silence_expired(self) -> None: + fake_cache = Mock(spec=Cache) + + async with FakeAlertmanagerServer() as fake_alertmanager_server: + port = fake_alertmanager_server.port + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache + ) + async with aiotools.closing_async(alertmanager_client): + with self.assertRaises(SilenceExpiredError): + await alertmanager_client.delete_silence("silence2") + silences = await alertmanager_client.get_silences() + + self.assertEqual(2, len(silences)) + async def test_delete_silence_raise_alertmanager_error(self) -> None: + fake_cache = Mock(spec=Cache) + async with FakeAlertmanagerServerWithErrorDeleteSilence() as fake_alertmanager_server: port = fake_alertmanager_server.port - alertmanager = AlertmanagerClient( - f"http://localhost:{port}", self.fake_cache + alertmanager_client = AlertmanagerClient( + f"http://localhost:{port}", fake_cache ) - async with aiotools.closing_async(alertmanager): - await alertmanager.get_alert("fingerprint1") + async with aiotools.closing_async(alertmanager_client): + await alertmanager_client.get_alert("fingerprint1") with self.assertRaises(AlertmanagerServerError): - await alertmanager.delete_silence("silence1") + await alertmanager_client.delete_silence("silence1") async def test_find_alert_happy(self) -> None: - alertmanager = AlertmanagerClient("http://localhost", self.fake_cache) - alert = alertmanager._find_alert( + fake_cache = Mock(spec=Cache) + + alertmanager_client = AlertmanagerClient("http://localhost", fake_cache) + alert = alertmanager_client._find_alert( "fingerprint1", [{"fingerprint": "fingerprint1"}] ) self.assertEqual({"fingerprint": "fingerprint1"}, alert) async def test_find_alert_raise_alert_not_found(self) -> None: - alertmanager = AlertmanagerClient("http://localhost", self.fake_cache) + fake_cache = Mock(spec=Cache) + + alertmanager_client = AlertmanagerClient("http://localhost", fake_cache) with self.assertRaises(AlertNotFoundError): - alertmanager._find_alert("fingerprint1", []) + alertmanager_client._find_alert("fingerprint1", []) with self.assertRaises(AlertNotFoundError): - alertmanager._find_alert("fingerprint2", [{"fingerprint": "fingerprint1"}]) + alertmanager_client._find_alert( + "fingerprint2", [{"fingerprint": "fingerprint1"}] + ) async def test_find_silence_happy(self) -> None: - alertmanager = AlertmanagerClient("http://localhost", self.fake_cache) - silence = alertmanager._find_silence("silence1", [{"id": "silence1"}]) + fake_cache = Mock(spec=Cache) + + alertmanager_client = AlertmanagerClient("http://localhost", fake_cache) + silence = alertmanager_client._find_silence("silence1", [{"id": "silence1"}]) self.assertEqual({"id": "silence1"}, silence) async def test_find_silence_raise_silence_not_found(self) -> None: - alertmanager = AlertmanagerClient("http://localhost", self.fake_cache) + fake_cache = Mock(spec=Cache) + + alertmanager_client = AlertmanagerClient("http://localhost", fake_cache) with self.assertRaises(SilenceNotFoundError): - alertmanager._find_silence("silence1", []) + alertmanager_client._find_silence("silence1", []) with self.assertRaises(SilenceNotFoundError): - alertmanager._find_silence("silence2", [{"id": "silence1"}]) + alertmanager_client._find_silence("silence2", [{"id": "silence1"}]) if __name__ == "__main__": diff --git a/tests/test_callback.py b/tests/test_callback.py index 31d8273..3f67fe2 100644 --- a/tests/test_callback.py +++ b/tests/test_callback.py @@ -17,11 +17,11 @@ from tests.utils import make_awaitable class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: # Create a Callbacks 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" + self.fake_matrix_client = Mock(spec=nio.AsyncClient) + self.fake_matrix_client.user = "@fake_user:example.com" self.fake_cache = MagicMock(spec=Cache) - self.fake_alertmanager = Mock(spec=AlertmanagerClient) + self.fake_alertmanager_client = Mock(spec=AlertmanagerClient) # Create a fake room to play with self.fake_room = Mock(spec=nio.MatrixRoom) @@ -35,7 +35,10 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): self.fake_config.command_prefix = "!alert " self.callbacks = Callbacks( - self.fake_client, self.fake_alertmanager, self.fake_cache, self.fake_config + self.fake_matrix_client, + self.fake_alertmanager_client, + self.fake_cache, + self.fake_config, ) async def test_invite(self) -> None: @@ -45,13 +48,13 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_invite_event.sender = "@some_other_fake_user:example.com" # Pretend that attempting to join a room is always successful - self.fake_client.join.return_value = make_awaitable(None) + self.fake_matrix_client.join.return_value = make_awaitable(None) # 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_client.join.assert_called_once_with(self.fake_room.room_id) + self.fake_matrix_client.join.assert_called_once_with(self.fake_room.room_id) @patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True) async def test_message_without_prefix(self, fake_command_create: Mock) -> None: @@ -84,9 +87,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that the command was not executed fake_command.assert_called_with( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, fake_message_event.sender, @@ -115,9 +118,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, fake_message_event.sender, @@ -132,7 +135,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Tests that the bot process messages in the room that contain a command fake_message_event = Mock(spec=nio.RoomMessageText) - fake_message_event.sender = self.fake_client.user + fake_message_event.sender = self.fake_matrix_client.user # Pretend that we received a text message event await self.callbacks.message(self.fake_room, fake_message_event) @@ -195,9 +198,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that the command was not executed fake_command.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, fake_message_event.sender, @@ -244,9 +247,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that the command was not executed fake_command.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, fake_message_event.sender, @@ -280,7 +283,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_event_response = Mock(spec=nio.RoomGetEventResponse) fake_event_response.event = fake_alert_event - self.fake_client.room_get_event.return_value = make_awaitable( + self.fake_matrix_client.room_get_event.return_value = make_awaitable( fake_event_response ) @@ -289,9 +292,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, fake_reaction_event.sender, @@ -299,7 +302,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): "some alert event id", ) fake_command.return_value.process.assert_called_once() - self.fake_client.room_get_event.assert_called_once_with( + self.fake_matrix_client.room_get_event.assert_called_once_with( self.fake_room.room_id, fake_alert_event.event_id ) @@ -324,7 +327,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): } fake_event_response = Mock(spec=nio.RoomGetEventError) - self.fake_client.room_get_event.return_value = make_awaitable( + self.fake_matrix_client.room_get_event.return_value = make_awaitable( fake_event_response ) @@ -334,7 +337,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_not_called() self.fake_cache.set.assert_not_called() - self.fake_client.room_get_event.assert_called_once_with( + self.fake_matrix_client.room_get_event.assert_called_once_with( self.fake_room.room_id, fake_alert_event_id ) @@ -364,7 +367,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_event_response = Mock(spec=nio.RoomGetEventResponse) fake_event_response.event = fake_alert_event - self.fake_client.room_get_event.return_value = make_awaitable( + self.fake_matrix_client.room_get_event.return_value = make_awaitable( fake_event_response ) @@ -374,7 +377,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_not_called() self.fake_cache.set.assert_not_called() - self.fake_client.room_get_event.assert_called_once_with( + self.fake_matrix_client.room_get_event.assert_called_once_with( self.fake_room.room_id, fake_alert_event.event_id ) @@ -403,7 +406,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_not_called() - self.fake_client.room_get_event.assert_not_called() + self.fake_matrix_client.room_get_event.assert_not_called() @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) async def test_ignore_reaction_sent_by_bot_user(self, fake_command: Mock) -> None: @@ -414,7 +417,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): fake_reaction_event = Mock(spec=nio.UnknownEvent) fake_reaction_event.type = "m.reaction" fake_reaction_event.event_id = "some event id" - fake_reaction_event.sender = self.fake_client.user + fake_reaction_event.sender = self.fake_matrix_client.user fake_reaction_event.source = { "content": { "m.relates_to": { @@ -433,7 +436,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_not_called() - self.fake_client.room_get_event.assert_not_called() + self.fake_matrix_client.room_get_event.assert_not_called() @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True) async def test_ignore_reaction_in_unauthorized_room( @@ -467,7 +470,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_not_called() - self.fake_client.room_get_event.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(self, fake_command: Mock) -> None: @@ -488,9 +491,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to execute the command fake_command.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, fake_redaction_event.sender, @@ -504,7 +507,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): """Tests the callback for RoomMessageText with the command prefix""" # Tests that the bot process messages in the room that contain a command fake_redaction_event = Mock(spec=nio.RedactionEvent) - fake_redaction_event.sender = self.fake_client.user + fake_redaction_event.sender = self.fake_matrix_client.user fake_cache_dict: Dict = {} self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ diff --git a/tests/test_command.py b/tests/test_command.py index daad2f6..d578933 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -32,10 +32,7 @@ def cache_get_item(key: str) -> str: async def create_silence( - fingerprint: str, - user: str, - seconds: Optional[int] = None, - silence_id: Optional[str] = None, + fingerprint: str, user: str, seconds: Optional[int] = None ) -> str: if fingerprint == "fingerprint1": return "silence1" @@ -45,10 +42,7 @@ async def create_silence( async def create_silence_raise_alertmanager_error( - fingerprint: str, - user: str, - seconds: Optional[int] = None, - silence_id: Optional[str] = None, + fingerprint: str, user: str, seconds: Optional[int] = None ) -> str: if fingerprint == "fingerprint1": raise AlertmanagerError @@ -56,10 +50,7 @@ async def create_silence_raise_alertmanager_error( async def create_silence_raise_alert_not_found_error( - fingerprint: str, - user: str, - seconds: Optional[int] = None, - silence_id: Optional[str] = None, + fingerprint: str, user: str, seconds: Optional[int] = None ) -> str: if fingerprint == "fingerprint1": raise AlertNotFoundError @@ -79,17 +70,17 @@ async def delete_silence_raise_silence_not_found_error(silence_id: str) -> None: 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" + self.fake_matrix_client = Mock(spec=nio.AsyncClient) + self.fake_matrix_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_matrix_client.room_send.return_value = make_awaitable(None) self.fake_cache = MagicMock(spec=Cache) self.fake_cache.__getitem__.side_effect = cache_get_item self.fake_cache.__contains__.return_value = True - self.fake_alertmanager = Mock(spec=AlertmanagerClient) - self.fake_alertmanager.create_silence.side_effect = create_silence + self.fake_alertmanager_client = Mock(spec=AlertmanagerClient) + self.fake_alertmanager_client.create_silence.side_effect = create_silence # Create a fake room to play with self.fake_room = Mock(spec=nio.MatrixRoom) @@ -113,9 +104,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): command = CommandFactory.create( "ack", - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -134,9 +125,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): command = CommandFactory.create( "ack 1w 3d", - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -156,9 +147,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): for unack_cmd in ("unack", "nack"): command = CommandFactory.create( unack_cmd, - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -177,9 +168,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): command = CommandFactory.create( "help", - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -197,9 +188,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): command = CommandFactory.create( "", - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -219,12 +210,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): } self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ - self.fake_cache.get.side_effect = fake_cache_dict.get command = AckAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -234,25 +224,17 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.create_silence.assert_called_once_with( - "fingerprint1", self.fake_sender, None, None + self.fake_alertmanager_client.create_silence.assert_called_once_with( + "fingerprint1", self.fake_sender, None ) fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Created silence with ID silence1.", ) self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) - self.fake_cache.get.assert_called_once_with("fingerprint1") - self.fake_cache.set.assert_has_calls( - [ - call( - "some event id", - "fingerprint1", - expire=self.fake_config.cache_expire_time, - ), - call("fingerprint1", "silence1", expire=None), - ] + self.fake_cache.set.assert_called_once_with( + "some event id", "fingerprint1", expire=None ) @patch.object(matrix_alertbot.command, "send_text_to_room") @@ -264,12 +246,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): } self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ - self.fake_cache.get.side_effect = fake_cache_dict.get command = AckAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -280,21 +261,17 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.create_silence.assert_called_once_with( - "fingerprint1", self.fake_sender, 864000, None + self.fake_alertmanager_client.create_silence.assert_called_once_with( + "fingerprint1", self.fake_sender, 864000 ) fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Created silence with ID silence1.", ) self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) - self.fake_cache.get.assert_called_once_with("fingerprint1") - self.fake_cache.set.assert_has_calls( - [ - call("some event id", "fingerprint1", expire=864000), - call("fingerprint1", "silence1", expire=864000), - ] + self.fake_cache.set.assert_called_once_with( + "some event id", "fingerprint1", expire=864000 ) @patch.object(matrix_alertbot.command, "send_text_to_room") @@ -308,12 +285,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): } self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ - self.fake_cache.get.side_effect = fake_cache_dict.get command = AckAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -321,22 +297,21 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_alert_event_id, ) - self.fake_alertmanager.create_silence.side_effect = ( + self.fake_alertmanager_client.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_called_once_with( - "fingerprint1", self.fake_sender, None, None + self.fake_alertmanager_client.create_silence.assert_called_once_with( + "fingerprint1", self.fake_sender, None ) fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Sorry, I couldn't create silence for alert with fingerprint fingerprint1 because something went wrong with Alertmanager: ", ) self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) - self.fake_cache.get.assert_called_once_with("fingerprint1") self.fake_cache.set.assert_not_called() @patch.object(matrix_alertbot.command, "send_text_to_room") @@ -350,12 +325,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): } self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ - self.fake_cache.get.side_effect = fake_cache_dict.get command = AckAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -363,22 +337,21 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_alert_event_id, ) - self.fake_alertmanager.create_silence.side_effect = ( + self.fake_alertmanager_client.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_called_once_with( - "fingerprint1", self.fake_sender, None, None + self.fake_alertmanager_client.create_silence.assert_called_once_with( + "fingerprint1", self.fake_sender, None ) fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Sorry, I couldn't create silence for alert with fingerprint fingerprint1: ", ) self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) - self.fake_cache.get.assert_called_once_with("fingerprint1") self.fake_cache.set.assert_not_called() @patch.object(matrix_alertbot.command, "send_text_to_room") @@ -388,9 +361,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): """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_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -402,9 +375,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.create_silence.assert_not_called() + self.fake_alertmanager_client.create_silence.assert_not_called() fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "I tried really hard, but I can't convert the duration 'invalid duration' to a number of seconds.", ) @@ -419,9 +392,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): """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_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -433,9 +406,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.create_silence.assert_not_called() + self.fake_alertmanager_client.create_silence.assert_not_called() fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "I can't create a silence with a negative duration!", ) @@ -455,9 +428,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.get.side_effect = fake_cache_dict.get command = AckAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -468,61 +441,12 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.create_silence.assert_not_called() + self.fake_alertmanager_client.create_silence.assert_not_called() fake_send_text_to_room.assert_not_called() self.fake_cache.__getitem__.assert_called_once_with("some alert event id") self.fake_cache.get.assert_not_called() self.fake_cache.set.assert_not_called() - @patch.object(matrix_alertbot.command, "send_text_to_room") - async def test_ack_with_silence_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 - fake_cache_dict = { - self.fake_alert_event_id: "fingerprint1", - "fingerprint1": "silence2", - } - - self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ - self.fake_cache.get.side_effect = fake_cache_dict.get - - command = AckAlertCommand( - 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 create silences - self.fake_alertmanager.create_silence.assert_called_once_with( - "fingerprint1", self.fake_sender, None, "silence2" - ) - fake_send_text_to_room.assert_called_once_with( - self.fake_client, - self.fake_room.room_id, - "Created silence with ID silence1.", - ) - self.fake_cache.__getitem__.assert_called_once_with("some alert event id") - self.fake_cache.get.assert_called_once_with("fingerprint1") - self.fake_cache.set.assert_has_calls( - [ - call( - self.fake_event_id, - "fingerprint1", - expire=self.fake_config.cache_expire_time, - ), - call("fingerprint1", "silence1", expire=None), - ] - ) - @patch.object(matrix_alertbot.command, "send_text_to_room") async def test_unack(self, fake_send_text_to_room: Mock) -> None: """Tests the callback for InviteMemberEvents""" @@ -535,9 +459,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ command = UnackAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -547,9 +471,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.delete_silence.assert_called_once_with("silence1") + self.fake_alertmanager_client.delete_silence.assert_called_once_with("silence1") fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Removed silence with ID silence1.", ) @@ -571,9 +495,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ command = UnackAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -581,15 +505,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_alert_event_id, ) - self.fake_alertmanager.delete_silence.side_effect = ( + self.fake_alertmanager_client.delete_silence.side_effect = ( delete_silence_raise_alertmanager_error ) await command.process() # Check that we attempted to create silences - self.fake_alertmanager.delete_silence.assert_called_once_with("silence1") + self.fake_alertmanager_client.delete_silence.assert_called_once_with("silence1") fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Sorry, I couldn't remove silence for alert with fingerprint fingerprint1 because something went wrong with Alertmanager: ", ) @@ -611,9 +535,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ command = UnackAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -621,15 +545,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_alert_event_id, ) - self.fake_alertmanager.delete_silence.side_effect = ( + self.fake_alertmanager_client.delete_silence.side_effect = ( delete_silence_raise_silence_not_found_error ) await command.process() # Check that we attempted to create silences - self.fake_alertmanager.delete_silence.assert_called_once_with("silence1") + self.fake_alertmanager_client.delete_silence.assert_called_once_with("silence1") fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Sorry, I couldn't remove silence for alert with fingerprint fingerprint1: ", ) @@ -648,9 +572,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ command = UnackAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -661,7 +585,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.delete_silence.assert_not_called() + self.fake_alertmanager_client.delete_silence.assert_not_called() fake_send_text_to_room.assert_not_called() self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) @@ -676,9 +600,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ command = UnackAlertCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -689,7 +613,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): await command.process() # Check that we attempted to create silences - self.fake_alertmanager.delete_silence.assert_not_called() + self.fake_alertmanager_client.delete_silence.assert_not_called() fake_send_text_to_room.assert_not_called() self.fake_cache.__getitem__.assert_has_calls( [call(self.fake_alert_event_id), call("fingerprint1")] @@ -701,9 +625,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -723,9 +647,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -746,9 +670,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -769,9 +693,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Tests that the bot attempts to join a room after being invited to it command = HelpCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -792,9 +716,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Tests that the bot attempts to join a room after being invited to it command = UnknownCommand( - self.fake_client, + self.fake_matrix_client, self.fake_cache, - self.fake_alertmanager, + self.fake_alertmanager_client, self.fake_config, self.fake_room, self.fake_sender, @@ -805,7 +729,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): # Check that we attempted to create silences fake_send_text_to_room.assert_called_once_with( - self.fake_client, + self.fake_matrix_client, self.fake_room.room_id, "Unknown command. Try the 'help' command for more information.", ) diff --git a/tests/test_webhook.py b/tests/test_webhook.py index fbb4368..bee3d4b 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -9,7 +9,13 @@ from diskcache import Cache from nio import LocalProtocolError, RoomSendResponse import matrix_alertbot.webhook +from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.config import Config +from matrix_alertbot.errors import ( + AlertmanagerError, + SilenceExtendError, + SilenceNotFoundError, +) from matrix_alertbot.webhook import Webhook @@ -19,9 +25,22 @@ def send_text_to_room_raise_error( raise LocalProtocolError() +def update_silence_raise_silence_not_found(fingerprint: str) -> str: + raise SilenceNotFoundError + + +def update_silence_raise_silence_extend_error(fingerprint: str) -> str: + raise SilenceExtendError + + +def update_silence_raise_alertmanager_error(fingerprint: str) -> str: + raise AlertmanagerError + + class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): async def get_application(self) -> web.Application: - self.fake_client = Mock(spec=nio.AsyncClient) + self.fake_matrix_client = Mock(spec=nio.AsyncClient) + self.fake_alertmanager_client = Mock(spec=AlertmanagerClient) self.fake_cache = Mock(spec=Cache) self.fake_room_id = "!abcdefg:example.com" @@ -61,21 +80,36 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): ] } - webhook = Webhook(self.fake_client, self.fake_cache, self.fake_config) + webhook = Webhook( + self.fake_matrix_client, + self.fake_alertmanager_client, + self.fake_cache, + self.fake_config, + ) return webhook.app @patch.object(matrix_alertbot.webhook, "send_text_to_room") - async def test_post_alerts(self, fake_send_text_to_room: Mock) -> None: + async def test_post_alerts_with_silence_not_found( + self, fake_send_text_to_room: 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(200, response.status) + self.fake_alertmanager_client.update_silence.assert_called_once_with( + "fingerprint1" + ) + self.assertEqual(2, fake_send_text_to_room.call_count) fake_send_text_to_room.assert_has_calls( [ call( - self.fake_client, + self.fake_matrix_client, self.fake_room_id, "[🔥 CRITICAL] alert1: some description1", '\n [🔥 CRITICAL]\n ' @@ -84,7 +118,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): notice=False, ), call( - self.fake_client, + self.fake_matrix_client, self.fake_room_id, "[🥦 RESOLVED] alert2: some description2", '\n [🥦 RESOLVED]\n ' @@ -99,6 +133,118 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): "fingerprint1", expire=self.fake_config.cache_expire_time, ) + self.assertEqual(3, self.fake_cache.delete.call_count) + self.fake_cache.delete.assert_has_calls( + [ + call("fingerprint1"), + call(fake_send_text_to_room.return_value.event_id), + call("fingerprint2"), + ] + ) + + @patch.object(matrix_alertbot.webhook, "send_text_to_room") + async def test_post_alerts_with_silence_extend_error( + self, fake_send_text_to_room: Mock + ) -> None: + self.fake_alertmanager_client.update_silence.side_effect = ( + update_silence_raise_silence_extend_error + ) + + data = self.fake_alerts + async with self.client.request( + "POST", f"/alerts/{self.fake_room_id}", json=data + ) as response: + self.assertEqual(200, response.status) + + self.fake_alertmanager_client.update_silence.assert_called_once_with( + "fingerprint1" + ) + self.assertEqual(2, fake_send_text_to_room.call_count) + fake_send_text_to_room.assert_has_calls( + [ + call( + self.fake_matrix_client, + self.fake_room_id, + "[🔥 CRITICAL] alert1: some description1", + '\n [🔥 CRITICAL]\n ' + 'alert1\n (job1)
\n' + "some description1", + notice=False, + ), + call( + self.fake_matrix_client, + self.fake_room_id, + "[🥦 RESOLVED] alert2: some description2", + '\n [🥦 RESOLVED]\n ' + 'alert2\n (job2)
\n' + "some description2", + notice=False, + ), + ] + ) + self.fake_cache.set.assert_called_once_with( + fake_send_text_to_room.return_value.event_id, + "fingerprint1", + expire=self.fake_config.cache_expire_time, + ) + self.assertEqual(2, self.fake_cache.delete.call_count) + self.fake_cache.delete.assert_has_calls( + [call(fake_send_text_to_room.return_value.event_id), call("fingerprint2")] + ) + + @patch.object(matrix_alertbot.webhook, "send_text_to_room") + async def test_post_alerts_with_alertmanager_error( + self, fake_send_text_to_room: Mock + ) -> None: + self.fake_alertmanager_client.update_silence.side_effect = ( + update_silence_raise_alertmanager_error + ) + + 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) + + self.fake_alertmanager_client.update_silence.assert_called_once_with( + "fingerprint1" + ) + fake_send_text_to_room.assert_not_called() + self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_not_called() + + @patch.object(matrix_alertbot.webhook, "send_text_to_room") + async def test_post_alerts_with_existing_silence( + self, fake_send_text_to_room: Mock + ) -> None: + self.fake_alertmanager_client.update_silence.return_value = "silence1" + + data = self.fake_alerts + async with self.client.request( + "POST", f"/alerts/{self.fake_room_id}", json=data + ) as response: + self.assertEqual(200, response.status) + + self.fake_alertmanager_client.update_silence.assert_called_once_with( + "fingerprint1" + ) + fake_send_text_to_room.assert_called_once_with( + self.fake_matrix_client, + self.fake_room_id, + "[🥦 RESOLVED] alert2: some description2", + '\n [🥦 RESOLVED]\n ' + 'alert2\n (job2)
\n' + "some description2", + notice=False, + ) + self.fake_cache.set.assert_not_called() + self.assertEqual(2, self.fake_cache.delete.call_count) + self.fake_cache.delete.assert_has_calls( + [ + call(fake_send_text_to_room.return_value.event_id), + call("fingerprint2"), + ] + ) @patch.object(matrix_alertbot.webhook, "send_text_to_room") async def test_post_alerts_in_unauthorized_room( @@ -116,6 +262,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): ) fake_send_text_to_room.assert_not_called() self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_not_called() @patch.object(matrix_alertbot.webhook, "send_text_to_room") async def test_post_alerts_with_empty_data( @@ -130,6 +277,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): self.assertEqual("Data must contain 'alerts' key.", error_msg) fake_send_text_to_room.assert_not_called() self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_not_called() @patch.object(matrix_alertbot.webhook, "send_text_to_room") async def test_post_empty_alerts(self, fake_send_text_to_room: Mock) -> None: @@ -143,6 +291,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): self.assertEqual("Alerts cannot be empty.", error_msg) fake_send_text_to_room.assert_not_called() self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_not_called() @patch.object(matrix_alertbot.webhook, "send_text_to_room") async def test_post_invalid_alerts(self, fake_send_text_to_room: Mock) -> None: @@ -156,6 +305,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): self.assertEqual("Alerts must be a list, got 'str'.", error_msg) fake_send_text_to_room.assert_not_called() self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_not_called() @patch.object(matrix_alertbot.webhook, "send_text_to_room") async def test_post_alerts_with_empty_items( @@ -171,6 +321,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): self.assertEqual("Invalid alert: {}.", error_msg) fake_send_text_to_room.assert_not_called() self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_not_called() @patch.object( matrix_alertbot.webhook, @@ -180,6 +331,10 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): async def test_post_alerts_raise_send_error( self, fake_send_text_to_room: 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 @@ -193,6 +348,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): ) fake_send_text_to_room.assert_called_once() self.fake_cache.set.assert_not_called() + self.fake_cache.delete.assert_called_once_with("fingerprint1") async def test_health(self) -> None: async with self.client.request("GET", "/health") as response: @@ -205,7 +361,8 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase): class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: - self.fake_client = Mock(spec=nio.AsyncClient) + self.fake_matrix_client = Mock(spec=nio.AsyncClient) + self.fake_alertmanager_client = Mock(spec=AlertmanagerClient) self.fake_cache = Mock(spec=Cache) self.fake_config = Mock(spec=Config) @@ -217,7 +374,12 @@ class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase): @patch.object(matrix_alertbot.webhook.web, "TCPSite", autospec=True) async def test_webhook_start_address_port(self, fake_tcp_site: Mock) -> None: - webhook = Webhook(self.fake_client, self.fake_cache, self.fake_config) + webhook = Webhook( + self.fake_matrix_client, + self.fake_alertmanager_client, + self.fake_cache, + self.fake_config, + ) await webhook.start() fake_tcp_site.assert_called_once_with( @@ -231,7 +393,12 @@ class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase): self.fake_config.address = None self.fake_config.port = None - webhook = Webhook(self.fake_client, self.fake_cache, self.fake_config) + webhook = Webhook( + self.fake_matrix_client, + self.fake_alertmanager_client, + self.fake_cache, + self.fake_config, + ) await webhook.start() fake_unix_site.assert_called_once_with(webhook.runner, self.fake_config.socket)