auto extend silence without duration

This commit is contained in:
HgO 2022-08-08 00:28:36 +02:00
parent 6781bc82fa
commit f381eac689
10 changed files with 804 additions and 467 deletions

View file

@ -12,10 +12,12 @@ from matrix_alertbot.errors import (
AlertmanagerServerError, AlertmanagerServerError,
AlertNotFoundError, AlertNotFoundError,
SilenceExpiredError, SilenceExpiredError,
SilenceExtendError,
SilenceNotFoundError, SilenceNotFoundError,
) )
MAX_DURATION_DAYS = 3652 DEFAULT_DURATION = timedelta(hours=3)
MAX_DURATION = timedelta(days=3652)
class AlertmanagerClient: class AlertmanagerClient:
@ -60,7 +62,6 @@ class AlertmanagerClient:
fingerprint: str, fingerprint: str,
user: str, user: str,
duration_seconds: Optional[int] = None, duration_seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
alert = await self.get_alert(fingerprint) alert = await self.get_alert(fingerprint)
@ -69,13 +70,50 @@ class AlertmanagerClient:
for label, value in alert["labels"].items() for label, value in alert["labels"].items()
] ]
start_time = datetime.now() return await self._create_or_update_silence(
max_duration = timedelta(days=MAX_DURATION_DAYS) fingerprint, silence_matchers, user, duration_seconds
if duration_seconds is None or duration_seconds > max_duration.total_seconds(): )
end_time = start_time + max_duration
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: else:
duration_delta = timedelta(seconds=duration_seconds) duration_delta = timedelta(seconds=duration_seconds)
end_time = start_time + duration_delta start_time = datetime.now()
end_time = start_time + duration_delta
silence = { silence = {
"id": silence_id, "id": silence_id,
@ -97,6 +135,8 @@ class AlertmanagerClient:
f"Cannot create silence for alert fingerprint {fingerprint}" f"Cannot create silence for alert fingerprint {fingerprint}"
) from e ) from e
self.cache.set(fingerprint, data["silenceID"], expire=duration_seconds)
return data["silenceID"] return data["silenceID"]
async def delete_silence(self, silence_id: str) -> None: async def delete_silence(self, silence_id: str) -> None:

View file

@ -24,8 +24,8 @@ logger = logging.getLogger(__name__)
class Callbacks: class Callbacks:
def __init__( def __init__(
self, self,
client: AsyncClient, matrix_client: AsyncClient,
alertmanager: AlertmanagerClient, alertmanager_client: AlertmanagerClient,
cache: Cache, cache: Cache,
config: Config, config: Config,
): ):
@ -39,9 +39,9 @@ class Callbacks:
config: Bot configuration parameters. config: Bot configuration parameters.
""" """
self.client = client self.matrix_client = matrix_client
self.cache = cache self.cache = cache
self.alertmanager = alertmanager self.alertmanager_client = alertmanager_client
self.config = config self.config = config
self.command_prefix = config.command_prefix self.command_prefix = config.command_prefix
@ -54,7 +54,7 @@ class Callbacks:
event: The event defining the message. event: The event defining the message.
""" """
# Ignore messages from ourselves # Ignore messages from ourselves
if event.sender == self.client.user: if event.sender == self.matrix_client.user:
return return
# Ignore messages from unauthorized room # Ignore messages from unauthorized room
@ -91,9 +91,9 @@ class Callbacks:
try: try:
command = CommandFactory.create( command = CommandFactory.create(
cmd, cmd,
self.client, self.matrix_client,
self.cache, self.cache,
self.alertmanager, self.alertmanager_client,
self.config, self.config,
room, room,
event.sender, event.sender,
@ -122,7 +122,7 @@ class Callbacks:
# Attempt to join 3 times before giving up # Attempt to join 3 times before giving up
for attempt in range(3): 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: if type(result) == JoinError:
logger.error( logger.error(
f"Error joining room {room.room_id} (attempt %d): %s", 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). 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. 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 # This is our own membership (invite) event
await self.invite(room, event) await self.invite(room, event)
@ -167,7 +167,7 @@ class Callbacks:
return return
# Ignore reactions from ourselves # Ignore reactions from ourselves
if event.sender == self.client.user: if event.sender == self.matrix_client.user:
return return
reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key") reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key")
@ -178,7 +178,9 @@ class Callbacks:
return return
# Get the original event that was reacted to # 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): if isinstance(event_response, RoomGetEventError):
logger.warning( logger.warning(
f"Error getting event that was reacted to ({alert_event_id})" f"Error getting event that was reacted to ({alert_event_id})"
@ -192,9 +194,9 @@ class Callbacks:
# Send a message acknowledging the reaction # Send a message acknowledging the reaction
command = AckAlertCommand( command = AckAlertCommand(
self.client, self.matrix_client,
self.cache, self.cache,
self.alertmanager, self.alertmanager_client,
self.config, self.config,
room, room,
event.sender, event.sender,
@ -210,15 +212,15 @@ class Callbacks:
return return
# Ignore redactions from ourselves # Ignore redactions from ourselves
if event.sender == self.client.user: if event.sender == self.matrix_client.user:
return return
logger.debug(f"Received event to remove event ID {event.redacts}") logger.debug(f"Received event to remove event ID {event.redacts}")
command = UnackAlertCommand( command = UnackAlertCommand(
self.client, self.matrix_client,
self.cache, self.cache,
self.alertmanager, self.alertmanager_client,
self.config, self.config,
room, room,
event.sender, event.sender,

View file

@ -21,9 +21,9 @@ logger = logging.getLogger(__name__)
class BaseCommand: class BaseCommand:
def __init__( def __init__(
self, self,
client: AsyncClient, matrix_client: AsyncClient,
cache: Cache, cache: Cache,
alertmanager: AlertmanagerClient, alertmanager_client: AlertmanagerClient,
config: Config, config: Config,
room: MatrixRoom, room: MatrixRoom,
sender: str, sender: str,
@ -49,9 +49,9 @@ class BaseCommand:
event_id: The ID of the event describing the command. event_id: The ID of the event describing the command.
""" """
self.client = client self.matrix_client = matrix_client
self.cache = cache self.cache = cache
self.alertmanager = alertmanager self.alertmanager_client = alertmanager_client
self.config = config self.config = config
self.room = room self.room = room
self.sender = sender self.sender = sender
@ -98,7 +98,7 @@ class AckAlertCommand(BaseAlertCommand):
if duration_seconds is None: if duration_seconds is None:
logger.error(f"Unable to create silence: Invalid duration '{duration}'") logger.error(f"Unable to create silence: Invalid duration '{duration}'")
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"I tried really hard, but I can't convert the duration '{duration}' to a number of seconds.", 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}'" f"Unable to create silence: Duration must be positive, got '{duration}'"
) )
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
"I can't create a silence with a negative duration!", "I can't create a silence with a negative duration!",
) )
return return
cache_expire_time = duration_seconds
else: else:
duration_seconds = None duration_seconds = None
cache_expire_time = self.config.cache_expire_time
logger.debug( logger.debug(
"Receiving a command to create a silence for an indefinite period" "Receiving a command to create a silence for an indefinite period"
) )
@ -133,27 +130,14 @@ class AckAlertCommand(BaseAlertCommand):
) )
return 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: try:
silence_id = await self.alertmanager.create_silence( silence_id = await self.alertmanager_client.create_silence(
alert_fingerprint, alert_fingerprint, self.room.user_name(self.sender), duration_seconds
self.room.user_name(self.sender),
duration_seconds,
cached_silence_id,
) )
except AlertNotFoundError as e: except AlertNotFoundError as e:
logger.warning(f"Unable to create silence: {e}") logger.warning(f"Unable to create silence: {e}")
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"Sorry, I couldn't create silence for alert with fingerprint {alert_fingerprint}: {e}", 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: except AlertmanagerError as e:
logger.exception(f"Unable to create silence: {e}", exc_info=e) logger.exception(f"Unable to create silence: {e}", exc_info=e)
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"Sorry, I couldn't create silence for alert with fingerprint {alert_fingerprint} " f"Sorry, I couldn't create silence for alert with fingerprint {alert_fingerprint} "
f"because something went wrong with Alertmanager: {e}", f"because something went wrong with Alertmanager: {e}",
) )
return return
self.cache.set(self.event_id, alert_fingerprint, expire=cache_expire_time) self.cache.set(self.event_id, alert_fingerprint, expire=duration_seconds)
self.cache.set(alert_fingerprint, silence_id, expire=duration_seconds)
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"Created silence with ID {silence_id}.", f"Created silence with ID {silence_id}.",
) )
@ -212,11 +195,11 @@ class UnackAlertCommand(BaseAlertCommand):
) )
try: try:
await self.alertmanager.delete_silence(silence_id) await self.alertmanager_client.delete_silence(silence_id)
except (SilenceNotFoundError, SilenceExpiredError) as e: except (SilenceNotFoundError, SilenceExpiredError) as e:
logger.error(f"Unable to delete silence: {e}") logger.error(f"Unable to delete silence: {e}")
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"Sorry, I couldn't remove silence for alert with fingerprint {alert_fingerprint}: {e}", 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: except AlertmanagerError as e:
logger.exception(f"Unable to delete silence: {e}", exc_info=e) logger.exception(f"Unable to delete silence: {e}", exc_info=e)
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"Sorry, I couldn't remove silence for alert with fingerprint {alert_fingerprint} " f"Sorry, I couldn't remove silence for alert with fingerprint {alert_fingerprint} "
f"because something went wrong with Alertmanager: {e}", f"because something went wrong with Alertmanager: {e}",
@ -234,7 +217,7 @@ class UnackAlertCommand(BaseAlertCommand):
self.cache.delete(alert_fingerprint) self.cache.delete(alert_fingerprint)
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
f"Removed silence with ID {silence_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 " "Hello, I am a bot made with matrix-nio! Use `help commands` to view "
"available commands." "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 return
topic = self.args[0] topic = self.args[0]
@ -259,7 +242,7 @@ class HelpCommand(BaseCommand):
text = "Available commands: ..." text = "Available commands: ..."
else: else:
text = "Unknown help topic!" 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): class UnknownCommand(BaseCommand):
@ -268,7 +251,7 @@ class UnknownCommand(BaseCommand):
f"Sending unknown command response to room {self.room.display_name}" f"Sending unknown command response to room {self.room.display_name}"
) )
await send_text_to_room( await send_text_to_room(
self.client, self.matrix_client,
self.room.room_id, self.room.room_id,
"Unknown command. Try the 'help' command for more information.", "Unknown command. Try the 'help' command for more information.",
) )

View file

@ -49,6 +49,12 @@ class SilenceExpiredError(AlertmanagerError):
pass pass
class SilenceExtendError(AlertmanagerError):
"""An error encountered when a silence cannot be extended."""
pass
class AlertmanagerServerError(AlertmanagerError): class AlertmanagerServerError(AlertmanagerError):
"""An error encountered with Alertmanager server.""" """An error encountered with Alertmanager server."""

View file

@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
def create_matrix_client(config: Config) -> AsyncClient: def create_matrix_client(config: Config) -> AsyncClient:
# Configuration options for the AsyncClient # Configuration options for the AsyncClient
client_config = AsyncClientConfig( matrix_client_config = AsyncClientConfig(
max_limit_exceeded=0, max_limit_exceeded=0,
max_timeouts=0, max_timeouts=0,
store_sync_tokens=True, store_sync_tokens=True,
@ -36,38 +36,38 @@ def create_matrix_client(config: Config) -> AsyncClient:
) )
# Initialize the matrix client # Initialize the matrix client
client = AsyncClient( matrix_client = AsyncClient(
config.homeserver_url, config.homeserver_url,
config.user_id, config.user_id,
device_id=config.device_id, device_id=config.device_id,
store_path=config.store_dir, store_path=config.store_dir,
config=client_config, config=matrix_client_config,
) )
if config.user_token: if config.user_token:
client.access_token = config.user_token matrix_client.access_token = config.user_token
client.user_id = config.user_id matrix_client.user_id = config.user_id
return client return matrix_client
async def start_matrix_client( async def start_matrix_client(
client: AsyncClient, cache: Cache, config: Config matrix_client: AsyncClient, cache: Cache, config: Config
) -> bool: ) -> bool:
# Keep trying to reconnect on failure (with some time in-between) # Keep trying to reconnect on failure (with some time in-between)
while True: while True:
try: try:
if config.user_token: if config.user_token:
# Use token to log in # Use token to log in
client.load_store() matrix_client.load_store()
# Sync encryption keys with the server # Sync encryption keys with the server
if client.should_upload_keys: if matrix_client.should_upload_keys:
await client.keys_upload() await matrix_client.keys_upload()
else: else:
# Try to login with the configured username/password # Try to login with the configured username/password
try: try:
login_response = await client.login( login_response = await matrix_client.login(
password=config.user_password, password=config.user_password,
device_name=config.device_name, device_name=config.device_name,
) )
@ -90,14 +90,14 @@ async def start_matrix_client(
# Login succeeded! # Login succeeded!
logger.info(f"Logged in as {config.user_id}") 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): except (ClientConnectionError, ServerDisconnectedError, TimeoutError):
logger.warning("Unable to connect to homeserver, retrying in 15s...") logger.warning("Unable to connect to homeserver, retrying in 15s...")
# Sleep so we don't bombard the server with login requests # Sleep so we don't bombard the server with login requests
await asyncio.sleep(15) await asyncio.sleep(15)
finally: finally:
await client.close() await matrix_client.close()
def main() -> None: def main() -> None:
@ -113,30 +113,30 @@ def main() -> None:
# Read the parsed config file and create a Config object # Read the parsed config file and create a Config object
config = Config(config_path) config = Config(config_path)
client = create_matrix_client(config) matrix_client = create_matrix_client(config)
# Configure the cache # Configure the cache
cache = Cache(config.cache_dir) cache = Cache(config.cache_dir)
# Configure Alertmanager client # Configure Alertmanager client
alertmanager = AlertmanagerClient(config.alertmanager_url, cache) alertmanager_client = AlertmanagerClient(config.alertmanager_url, cache)
# Set up event callbacks # Set up event callbacks
callbacks = Callbacks(client, alertmanager, cache, config) callbacks = Callbacks(matrix_client, alertmanager_client, cache, config)
client.add_event_callback(callbacks.message, (RoomMessageText,)) matrix_client.add_event_callback(callbacks.message, (RoomMessageText,))
client.add_event_callback( matrix_client.add_event_callback(
callbacks.invite_event_filtered_callback, (InviteMemberEvent,) callbacks.invite_event_filtered_callback, (InviteMemberEvent,)
) )
client.add_event_callback(callbacks.decryption_failure, (MegolmEvent,)) matrix_client.add_event_callback(callbacks.decryption_failure, (MegolmEvent,))
client.add_event_callback(callbacks.unknown, (UnknownEvent,)) matrix_client.add_event_callback(callbacks.unknown, (UnknownEvent,))
client.add_event_callback(callbacks.redaction, (RedactionEvent,)) matrix_client.add_event_callback(callbacks.redaction, (RedactionEvent,))
# Configure webhook server # Configure webhook server
webhook_server = Webhook(client, cache, config) webhook_server = Webhook(matrix_client, alertmanager_client, cache, config)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(webhook_server.start()) 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: try:
loop.run_forever() loop.run_forever()
@ -144,6 +144,6 @@ def main() -> None:
logger.error(e) logger.error(e)
finally: finally:
loop.run_until_complete(webhook_server.close()) loop.run_until_complete(webhook_server.close())
loop.run_until_complete(alertmanager.close()) loop.run_until_complete(alertmanager_client.close())
loop.run_until_complete(client.close()) loop.run_until_complete(matrix_client.close())
cache.close() cache.close()

View file

@ -10,8 +10,14 @@ from diskcache import Cache
from nio import AsyncClient, LocalProtocolError from nio import AsyncClient, LocalProtocolError
from matrix_alertbot.alert import Alert, AlertRenderer 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.chat_functions import send_text_to_room
from matrix_alertbot.config import Config from matrix_alertbot.config import Config
from matrix_alertbot.errors import (
AlertmanagerError,
SilenceExtendError,
SilenceNotFoundError,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,10 +34,7 @@ async def create_alerts(request: web_request.Request) -> web.Response:
data = await request.json() data = await request.json()
room_id = request.match_info["room_id"] room_id = request.match_info["room_id"]
client: AsyncClient = request.app["client"]
config: Config = request.app["config"] 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: if room_id not in config.allowed_rooms:
logger.error("Cannot send alerts to room ID {room_id}.") 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") logger.error("Received data without 'alerts' key")
return web.Response(status=400, body="Data must contain '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): 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}'.") logger.error(f"Received data with invalid alerts type '{alerts_type}'.")
return web.Response( return web.Response(
status=400, body=f"Alerts must be a list, got '{alerts_type}'." 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: if len(data["alerts"]) == 0:
return web.Response(status=400, body="Alerts cannot be empty.") return web.Response(status=400, body="Alerts cannot be empty.")
for alert in alerts: alerts = []
for alert in alert_dicts:
try: try:
alert = Alert.from_dict(alert) alert = Alert.from_dict(alert)
except KeyError as e: except KeyError as e:
logger.error(f"Cannot parse alert dict: {e}") logger.error(f"Cannot parse alert dict: {e}")
return web.Response(status=400, body=f"Invalid alert: {alert}.") return web.Response(status=400, body=f"Invalid alert: {alert}.")
alerts.append(alert)
plaintext = alert_renderer.render(alert, html=False) for alert in alerts:
html = alert_renderer.render(alert, html=True)
try: try:
event = await send_text_to_room( await create_alert(alert, room_id, request)
client, room_id, plaintext, html, notice=False 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: except (LocalProtocolError, ClientError) as e:
logger.error( 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.", 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) 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: 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 = 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["config"] = config
self.app["cache"] = cache self.app["cache"] = cache
self.app["alert_renderer"] = AlertRenderer(config.template_dir) self.app["alert_renderer"] = AlertRenderer(config.template_dir)

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import json import json
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any, Dict, Optional, Tuple
from unittest.mock import MagicMock, Mock from unittest.mock import Mock
import aiohttp import aiohttp
import aiohttp.test_utils import aiohttp.test_utils
@ -18,16 +18,25 @@ from matrix_alertbot.errors import (
AlertmanagerServerError, AlertmanagerServerError,
AlertNotFoundError, AlertNotFoundError,
SilenceExpiredError, SilenceExpiredError,
SilenceExtendError,
SilenceNotFoundError, SilenceNotFoundError,
) )
class FakeTimeDelta: class FakeCache:
def __init__(self, seconds: int) -> None: def __init__(self, cache_dict: Optional[Dict] = None) -> None:
self.seconds = seconds if cache_dict is None:
cache_dict = {}
self.cache = cache_dict
def __radd__(self, other: Any) -> datetime: def get(
return datetime.utcfromtimestamp(self.seconds) 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: class AbstractFakeAlertmanagerServer:
@ -42,8 +51,18 @@ class AbstractFakeAlertmanagerServer:
] ]
) )
self.app["silences"] = [ 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) self.runner = web.AppRunner(self.app)
@ -111,8 +130,9 @@ class FakeAlertmanagerServer(AbstractFakeAlertmanagerServer):
silences = self.app["silences"] silences = self.app["silences"]
silence = await request.json() 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"} silence["status"] = {"state": "active"}
silences.append(silence) silences.append(silence)
@ -165,17 +185,17 @@ class FakeAlertmanagerServerWithErrorDeleteSilence(FakeAlertmanagerServer):
class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase): class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None: async def asyncSetUp(self) -> None:
self.fake_fingerprints = Mock(return_value=["fingerprint1", "fingerprint2"]) 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: async def test_get_alerts_happy(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServer() as fake_alertmanager_server: async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
alerts = await alertmanager.get_alerts() alerts = await alertmanager_client.get_alerts()
self.assertEqual( self.assertEqual(
[ [
@ -197,72 +217,94 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async def test_get_alerts_empty(self) -> None: async def test_get_alerts_empty(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
alerts = await alertmanager.get_alerts() alerts = await alertmanager_client.get_alerts()
self.assertEqual([], alerts) self.assertEqual([], alerts)
async def test_get_alerts_raise_alertmanager_error(self) -> None: async def test_get_alerts_raise_alertmanager_error(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_alerts() await alertmanager_client.get_alerts()
async def test_get_silences_happy(self) -> None: async def test_get_silences_happy(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServer() as fake_alertmanager_server: async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
silences = await alertmanager.get_silences() silences = await alertmanager_client.get_silences()
self.assertEqual( 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, silences,
) )
async def test_get_silences_empty(self) -> None: async def test_get_silences_empty(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
silences = await alertmanager.get_silences() silences = await alertmanager_client.get_silences()
self.assertEqual([], silences) self.assertEqual([], silences)
async def test_get_silences_raise_alertmanager_error(self) -> None: async def test_get_silences_raise_alertmanager_error(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_silences() await alertmanager_client.get_silences()
async def test_get_alert_happy(self) -> None: async def test_get_alert_happy(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServer() as fake_alertmanager_server: async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
alert = await alertmanager.get_alert("fingerprint1") alert = await alertmanager_client.get_alert("fingerprint1")
self.assertEqual( self.assertEqual(
{ {
@ -274,76 +316,98 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async def test_get_alert_raise_alert_not_found(self) -> None: async def test_get_alert_raise_alert_not_found(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(AlertNotFoundError): 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: async def test_get_alert_raise_alertmanager_error(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_alert("fingerprint1") await alertmanager_client.get_alert("fingerprint1")
async def test_get_silence_happy(self) -> None: async def test_get_silence_happy(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServer() as fake_alertmanager_server: async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
silence1 = await alertmanager.get_silence("silence1") silence1 = await alertmanager_client.get_silence("silence1")
silence2 = await alertmanager.get_silence("silence2") silence2 = await alertmanager_client.get_silence("silence2")
self.assertEqual( self.assertEqual(
{"id": "silence1", "status": {"state": "active"}}, {
"id": "silence1",
"createdBy": "user1",
"status": {"state": "active"},
"matchers": [],
},
silence1, silence1,
) )
self.assertEqual( self.assertEqual(
{"id": "silence2", "status": {"state": "expired"}}, {
"id": "silence2",
"createdBy": "user2",
"status": {"state": "expired"},
"matchers": [],
},
silence2, silence2,
) )
async def test_get_silence_raise_silence_not_found(self) -> None: async def test_get_silence_raise_silence_not_found(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(SilenceNotFoundError): 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: async def test_get_silence_raise_alertmanager_error(self) -> None:
fake_cache = FakeCache()
async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_silence("silence1") await alertmanager_client.get_silence("silence1")
@freeze_time(datetime.utcfromtimestamp(0)) @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: async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
silence_id = await alertmanager.create_silence( silence_id = await alertmanager_client.create_silence(
"fingerprint1", "user", 86400 "fingerprint1", "user", 86400
) )
silence = await alertmanager.get_silence("silence1") silence = await alertmanager_client.get_silence("silence1")
self.assertEqual("silence1", silence_id) self.assertEqual("silence1", silence_id)
self.assertEqual( self.assertEqual(
@ -365,21 +429,81 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
}, },
silence, silence,
) )
fake_cache.set.assert_called_once_with("fingerprint1", "silence1", expire=86400)
@freeze_time(datetime.utcfromtimestamp(0)) @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: async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
silence_id = await alertmanager.create_silence( await alertmanager_client.create_silence("fingerprint1", "user", 86400)
"fingerprint1", "user", 86400, "silence2" with self.assertRaises(SilenceExtendError):
) await alertmanager_client.update_silence("fingerprint1")
silence = await alertmanager.get_silence("silence2") 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( self.assertEqual(
{ {
"id": "silence2", "id": "silence2",
@ -394,56 +518,27 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
], ],
"createdBy": "user", "createdBy": "user",
"startsAt": "1970-01-01T00:00:00", "startsAt": "1970-01-01T00:00:00",
"endsAt": "1970-01-02T00:00:00", "endsAt": "1970-01-01T03: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",
"comment": "Acknowledge alert from Matrix", "comment": "Acknowledge alert from Matrix",
}, },
silence, silence,
) )
self.assertEqual({"fingerprint1": ("silence2", None)}, fake_cache.cache)
@freeze_time(datetime.utcfromtimestamp(0)) @freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_with_max_duration(self) -> None: 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: async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
silence_id = await alertmanager.create_silence( silence_id = await alertmanager_client.create_silence(
"fingerprint1", "user", int(timedelta.max.total_seconds()) "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("silence1", silence_id)
self.assertEqual( self.assertEqual(
@ -467,99 +562,165 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async def test_create_silence_raise_alert_not_found(self) -> None: 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: async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
with self.assertRaises(AlertNotFoundError): 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: 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: async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
await alertmanager.get_alert("fingerprint1") await alertmanager_client.get_alert("fingerprint1")
with self.assertRaises(AlertmanagerServerError): 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: async def test_delete_silence(self) -> None:
fake_cache = Mock(spec=Cache)
async with FakeAlertmanagerServer() as fake_alertmanager_server: async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
await alertmanager.delete_silence("silence1") await alertmanager_client.delete_silence("silence1")
silences = await alertmanager.get_silences() silences = await alertmanager_client.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()
self.assertEqual( self.assertEqual(
[ [
{"id": "silence1", "status": {"state": "active"}}, {
{"id": "silence2", "status": {"state": "expired"}}, "id": "silence2",
"createdBy": "user2",
"status": {"state": "expired"},
"matchers": [],
}
], ],
silences, 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: async def test_delete_silence_raise_alertmanager_error(self) -> None:
fake_cache = Mock(spec=Cache)
async with FakeAlertmanagerServerWithErrorDeleteSilence() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorDeleteSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager_client = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager_client):
await alertmanager.get_alert("fingerprint1") await alertmanager_client.get_alert("fingerprint1")
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.delete_silence("silence1") await alertmanager_client.delete_silence("silence1")
async def test_find_alert_happy(self) -> None: async def test_find_alert_happy(self) -> None:
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache) fake_cache = Mock(spec=Cache)
alert = alertmanager._find_alert(
alertmanager_client = AlertmanagerClient("http://localhost", fake_cache)
alert = alertmanager_client._find_alert(
"fingerprint1", [{"fingerprint": "fingerprint1"}] "fingerprint1", [{"fingerprint": "fingerprint1"}]
) )
self.assertEqual({"fingerprint": "fingerprint1"}, alert) self.assertEqual({"fingerprint": "fingerprint1"}, alert)
async def test_find_alert_raise_alert_not_found(self) -> None: 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): with self.assertRaises(AlertNotFoundError):
alertmanager._find_alert("fingerprint1", []) alertmanager_client._find_alert("fingerprint1", [])
with self.assertRaises(AlertNotFoundError): 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: async def test_find_silence_happy(self) -> None:
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache) fake_cache = Mock(spec=Cache)
silence = alertmanager._find_silence("silence1", [{"id": "silence1"}])
alertmanager_client = AlertmanagerClient("http://localhost", fake_cache)
silence = alertmanager_client._find_silence("silence1", [{"id": "silence1"}])
self.assertEqual({"id": "silence1"}, silence) self.assertEqual({"id": "silence1"}, silence)
async def test_find_silence_raise_silence_not_found(self) -> None: 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): with self.assertRaises(SilenceNotFoundError):
alertmanager._find_silence("silence1", []) alertmanager_client._find_silence("silence1", [])
with self.assertRaises(SilenceNotFoundError): with self.assertRaises(SilenceNotFoundError):
alertmanager._find_silence("silence2", [{"id": "silence1"}]) alertmanager_client._find_silence("silence2", [{"id": "silence1"}])
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -17,11 +17,11 @@ from tests.utils import make_awaitable
class CallbacksTestCase(unittest.IsolatedAsyncioTestCase): class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None: def setUp(self) -> None:
# Create a Callbacks object and give it some Mock'd objects to use # Create a Callbacks object and give it some Mock'd objects to use
self.fake_client = Mock(spec=nio.AsyncClient) self.fake_matrix_client = Mock(spec=nio.AsyncClient)
self.fake_client.user = "@fake_user:example.com" self.fake_matrix_client.user = "@fake_user:example.com"
self.fake_cache = MagicMock(spec=Cache) 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 # Create a fake room to play with
self.fake_room = Mock(spec=nio.MatrixRoom) self.fake_room = Mock(spec=nio.MatrixRoom)
@ -35,7 +35,10 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_config.command_prefix = "!alert " self.fake_config.command_prefix = "!alert "
self.callbacks = Callbacks( 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: async def test_invite(self) -> None:
@ -45,13 +48,13 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_invite_event.sender = "@some_other_fake_user:example.com" fake_invite_event.sender = "@some_other_fake_user:example.com"
# Pretend that attempting to join a room is always successful # 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 # Pretend that we received an invite event
await self.callbacks.invite(self.fake_room, fake_invite_event) await self.callbacks.invite(self.fake_room, fake_invite_event)
# Check that we attempted to join the room # 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) @patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True)
async def test_message_without_prefix(self, fake_command_create: Mock) -> None: 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 # Check that the command was not executed
fake_command.assert_called_with( fake_command.assert_called_with(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
@ -115,9 +118,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to execute the command # Check that we attempted to execute the command
fake_command.assert_called_once_with( fake_command.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
fake_message_event.sender, 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 # Tests that the bot process messages in the room that contain a command
fake_message_event = Mock(spec=nio.RoomMessageText) 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 # Pretend that we received a text message event
await self.callbacks.message(self.fake_room, fake_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 # Check that the command was not executed
fake_command.assert_called_once_with( fake_command.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
@ -244,9 +247,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that the command was not executed # Check that the command was not executed
fake_command.assert_called_once_with( fake_command.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
@ -280,7 +283,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_event_response = Mock(spec=nio.RoomGetEventResponse) fake_event_response = Mock(spec=nio.RoomGetEventResponse)
fake_event_response.event = fake_alert_event 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 fake_event_response
) )
@ -289,9 +292,9 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to execute the command # Check that we attempted to execute the command
fake_command.assert_called_once_with( fake_command.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
fake_reaction_event.sender, fake_reaction_event.sender,
@ -299,7 +302,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
"some alert event id", "some alert event id",
) )
fake_command.return_value.process.assert_called_once() 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 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) 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 fake_event_response
) )
@ -334,7 +337,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to execute the command # Check that we attempted to execute the command
fake_command.assert_not_called() fake_command.assert_not_called()
self.fake_cache.set.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 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 = Mock(spec=nio.RoomGetEventResponse)
fake_event_response.event = fake_alert_event 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 fake_event_response
) )
@ -374,7 +377,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to execute the command # Check that we attempted to execute the command
fake_command.assert_not_called() fake_command.assert_not_called()
self.fake_cache.set.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 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 # Check that we attempted to execute the command
fake_command.assert_not_called() 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) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_ignore_reaction_sent_by_bot_user(self, fake_command: Mock) -> None: 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 = Mock(spec=nio.UnknownEvent)
fake_reaction_event.type = "m.reaction" fake_reaction_event.type = "m.reaction"
fake_reaction_event.event_id = "some event id" 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 = { fake_reaction_event.source = {
"content": { "content": {
"m.relates_to": { "m.relates_to": {
@ -433,7 +436,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to execute the command # Check that we attempted to execute the command
fake_command.assert_not_called() 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) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_ignore_reaction_in_unauthorized_room( async def test_ignore_reaction_in_unauthorized_room(
@ -467,7 +470,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to execute the command # Check that we attempted to execute the command
fake_command.assert_not_called() 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) @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_redaction(self, fake_command: Mock) -> None: 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 # Check that we attempted to execute the command
fake_command.assert_called_once_with( fake_command.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
fake_redaction_event.sender, fake_redaction_event.sender,
@ -504,7 +507,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
"""Tests the callback for RoomMessageText with the command prefix""" """Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command # Tests that the bot process messages in the room that contain a command
fake_redaction_event = Mock(spec=nio.RedactionEvent) 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 = {} fake_cache_dict: Dict = {}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__

View file

@ -32,10 +32,7 @@ def cache_get_item(key: str) -> str:
async def create_silence( async def create_silence(
fingerprint: str, fingerprint: str, user: str, seconds: Optional[int] = None
user: str,
seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
if fingerprint == "fingerprint1": if fingerprint == "fingerprint1":
return "silence1" return "silence1"
@ -45,10 +42,7 @@ async def create_silence(
async def create_silence_raise_alertmanager_error( async def create_silence_raise_alertmanager_error(
fingerprint: str, fingerprint: str, user: str, seconds: Optional[int] = None
user: str,
seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
if fingerprint == "fingerprint1": if fingerprint == "fingerprint1":
raise AlertmanagerError raise AlertmanagerError
@ -56,10 +50,7 @@ async def create_silence_raise_alertmanager_error(
async def create_silence_raise_alert_not_found_error( async def create_silence_raise_alert_not_found_error(
fingerprint: str, fingerprint: str, user: str, seconds: Optional[int] = None
user: str,
seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
if fingerprint == "fingerprint1": if fingerprint == "fingerprint1":
raise AlertNotFoundError raise AlertNotFoundError
@ -79,17 +70,17 @@ async def delete_silence_raise_silence_not_found_error(silence_id: str) -> None:
class CommandTestCase(unittest.IsolatedAsyncioTestCase): class CommandTestCase(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None: def setUp(self) -> None:
# Create a Command object and give it some Mock'd objects to use # Create a Command object and give it some Mock'd objects to use
self.fake_client = Mock(spec=nio.AsyncClient) self.fake_matrix_client = Mock(spec=nio.AsyncClient)
self.fake_client.user = "@fake_user:example.com" self.fake_matrix_client.user = "@fake_user:example.com"
# Pretend that attempting to send a message is always successful # 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 = MagicMock(spec=Cache)
self.fake_cache.__getitem__.side_effect = cache_get_item self.fake_cache.__getitem__.side_effect = cache_get_item
self.fake_cache.__contains__.return_value = True self.fake_cache.__contains__.return_value = True
self.fake_alertmanager = Mock(spec=AlertmanagerClient) self.fake_alertmanager_client = Mock(spec=AlertmanagerClient)
self.fake_alertmanager.create_silence.side_effect = create_silence self.fake_alertmanager_client.create_silence.side_effect = create_silence
# Create a fake room to play with # Create a fake room to play with
self.fake_room = Mock(spec=nio.MatrixRoom) self.fake_room = Mock(spec=nio.MatrixRoom)
@ -113,9 +104,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
command = CommandFactory.create( command = CommandFactory.create(
"ack", "ack",
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -134,9 +125,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
command = CommandFactory.create( command = CommandFactory.create(
"ack 1w 3d", "ack 1w 3d",
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -156,9 +147,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
for unack_cmd in ("unack", "nack"): for unack_cmd in ("unack", "nack"):
command = CommandFactory.create( command = CommandFactory.create(
unack_cmd, unack_cmd,
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -177,9 +168,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
command = CommandFactory.create( command = CommandFactory.create(
"help", "help",
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -197,9 +188,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
command = CommandFactory.create( command = CommandFactory.create(
"", "",
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -219,12 +210,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
} }
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
self.fake_cache.get.side_effect = fake_cache_dict.get
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -234,25 +224,17 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # Check that we attempted to create silences
self.fake_alertmanager.create_silence.assert_called_once_with( self.fake_alertmanager_client.create_silence.assert_called_once_with(
"fingerprint1", self.fake_sender, None, None "fingerprint1", self.fake_sender, None
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Created silence with ID silence1.", "Created silence with ID silence1.",
) )
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) 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_called_once_with(
self.fake_cache.set.assert_has_calls( "some event id", "fingerprint1", expire=None
[
call(
"some event id",
"fingerprint1",
expire=self.fake_config.cache_expire_time,
),
call("fingerprint1", "silence1", expire=None),
]
) )
@patch.object(matrix_alertbot.command, "send_text_to_room") @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.__getitem__.side_effect = fake_cache_dict.__getitem__
self.fake_cache.get.side_effect = fake_cache_dict.get
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -280,21 +261,17 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # Check that we attempted to create silences
self.fake_alertmanager.create_silence.assert_called_once_with( self.fake_alertmanager_client.create_silence.assert_called_once_with(
"fingerprint1", self.fake_sender, 864000, None "fingerprint1", self.fake_sender, 864000
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Created silence with ID silence1.", "Created silence with ID silence1.",
) )
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) 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_called_once_with(
self.fake_cache.set.assert_has_calls( "some event id", "fingerprint1", expire=864000
[
call("some event id", "fingerprint1", expire=864000),
call("fingerprint1", "silence1", expire=864000),
]
) )
@patch.object(matrix_alertbot.command, "send_text_to_room") @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.__getitem__.side_effect = fake_cache_dict.__getitem__
self.fake_cache.get.side_effect = fake_cache_dict.get
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -321,22 +297,21 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_alert_event_id, 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 create_silence_raise_alertmanager_error
) )
await command.process() await command.process()
# Check that we attempted to create silences # Check that we attempted to create silences
self.fake_alertmanager.create_silence.assert_called_once_with( self.fake_alertmanager_client.create_silence.assert_called_once_with(
"fingerprint1", self.fake_sender, None, None "fingerprint1", self.fake_sender, None
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Sorry, I couldn't create silence for alert with fingerprint fingerprint1 because something went wrong with Alertmanager: ", "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.__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() self.fake_cache.set.assert_not_called()
@patch.object(matrix_alertbot.command, "send_text_to_room") @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.__getitem__.side_effect = fake_cache_dict.__getitem__
self.fake_cache.get.side_effect = fake_cache_dict.get
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -363,22 +337,21 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_alert_event_id, 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 create_silence_raise_alert_not_found_error
) )
await command.process() await command.process()
# Check that we attempted to create silences # Check that we attempted to create silences
self.fake_alertmanager.create_silence.assert_called_once_with( self.fake_alertmanager_client.create_silence.assert_called_once_with(
"fingerprint1", self.fake_sender, None, None "fingerprint1", self.fake_sender, None
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Sorry, I couldn't create silence for alert with fingerprint fingerprint1: ", "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.__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() self.fake_cache.set.assert_not_called()
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
@ -388,9 +361,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
"""Tests the callback for InviteMemberEvents""" """Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it # Tests that the bot attempts to join a room after being invited to it
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -402,9 +375,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # 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( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"I tried really hard, but I can't convert the duration 'invalid duration' to a number of seconds.", "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 the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it # Tests that the bot attempts to join a room after being invited to it
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -433,9 +406,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # 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( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"I can't create a silence with a negative duration!", "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 self.fake_cache.get.side_effect = fake_cache_dict.get
command = AckAlertCommand( command = AckAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -468,61 +441,12 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # 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() fake_send_text_to_room.assert_not_called()
self.fake_cache.__getitem__.assert_called_once_with("some alert event id") self.fake_cache.__getitem__.assert_called_once_with("some alert event id")
self.fake_cache.get.assert_not_called() self.fake_cache.get.assert_not_called()
self.fake_cache.set.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") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_unack(self, fake_send_text_to_room: Mock) -> None: async def test_unack(self, fake_send_text_to_room: Mock) -> None:
"""Tests the callback for InviteMemberEvents""" """Tests the callback for InviteMemberEvents"""
@ -535,9 +459,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -547,9 +471,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # 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( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Removed silence with ID silence1.", "Removed silence with ID silence1.",
) )
@ -571,9 +495,9 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -581,15 +505,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_alert_event_id, 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 delete_silence_raise_alertmanager_error
) )
await command.process() await command.process()
# Check that we attempted to create silences # 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( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Sorry, I couldn't remove silence for alert with fingerprint fingerprint1 because something went wrong with Alertmanager: ", "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__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -621,15 +545,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_alert_event_id, 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 delete_silence_raise_silence_not_found_error
) )
await command.process() await command.process()
# Check that we attempted to create silences # 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( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Sorry, I couldn't remove silence for alert with fingerprint fingerprint1: ", "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__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -661,7 +585,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # 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() fake_send_text_to_room.assert_not_called()
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id) 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__ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -689,7 +613,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
await command.process() await command.process()
# Check that we attempted to create silences # 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() fake_send_text_to_room.assert_not_called()
self.fake_cache.__getitem__.assert_has_calls( self.fake_cache.__getitem__.assert_has_calls(
[call(self.fake_alert_event_id), call("fingerprint1")] [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 # Tests that the bot attempts to join a room after being invited to it
command = HelpCommand( command = HelpCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, 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 # Tests that the bot attempts to join a room after being invited to it
command = HelpCommand( command = HelpCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, 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 # Tests that the bot attempts to join a room after being invited to it
command = HelpCommand( command = HelpCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, 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 # Tests that the bot attempts to join a room after being invited to it
command = HelpCommand( command = HelpCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, 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 # Tests that the bot attempts to join a room after being invited to it
command = UnknownCommand( command = UnknownCommand(
self.fake_client, self.fake_matrix_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager_client,
self.fake_config, self.fake_config,
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
@ -805,7 +729,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to create silences # Check that we attempted to create silences
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_matrix_client,
self.fake_room.room_id, self.fake_room.room_id,
"Unknown command. Try the 'help' command for more information.", "Unknown command. Try the 'help' command for more information.",
) )

View file

@ -9,7 +9,13 @@ from diskcache import Cache
from nio import LocalProtocolError, RoomSendResponse from nio import LocalProtocolError, RoomSendResponse
import matrix_alertbot.webhook import matrix_alertbot.webhook
from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.config import Config from matrix_alertbot.config import Config
from matrix_alertbot.errors import (
AlertmanagerError,
SilenceExtendError,
SilenceNotFoundError,
)
from matrix_alertbot.webhook import Webhook from matrix_alertbot.webhook import Webhook
@ -19,9 +25,22 @@ def send_text_to_room_raise_error(
raise LocalProtocolError() 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): class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
async def get_application(self) -> web.Application: 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_cache = Mock(spec=Cache)
self.fake_room_id = "!abcdefg:example.com" 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 return webhook.app
@patch.object(matrix_alertbot.webhook, "send_text_to_room") @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 data = self.fake_alerts
async with self.client.request( async with self.client.request(
"POST", f"/alerts/{self.fake_room_id}", json=data "POST", f"/alerts/{self.fake_room_id}", json=data
) as response: ) as response:
self.assertEqual(200, response.status) 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( fake_send_text_to_room.assert_has_calls(
[ [
call( call(
self.fake_client, self.fake_matrix_client,
self.fake_room_id, self.fake_room_id,
"[🔥 CRITICAL] alert1: some description1", "[🔥 CRITICAL] alert1: some description1",
'<font color="#dc3545">\n <b>[🔥 CRITICAL]</b>\n</font> ' '<font color="#dc3545">\n <b>[🔥 CRITICAL]</b>\n</font> '
@ -84,7 +118,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
notice=False, notice=False,
), ),
call( call(
self.fake_client, self.fake_matrix_client,
self.fake_room_id, self.fake_room_id,
"[🥦 RESOLVED] alert2: some description2", "[🥦 RESOLVED] alert2: some description2",
'<font color="#33cc33">\n <b>[🥦 RESOLVED]</b>\n</font> ' '<font color="#33cc33">\n <b>[🥦 RESOLVED]</b>\n</font> '
@ -99,6 +133,118 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
"fingerprint1", "fingerprint1",
expire=self.fake_config.cache_expire_time, 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",
'<font color="#dc3545">\n <b>[🔥 CRITICAL]</b>\n</font> '
'<a href="http://example.com/alert1">alert1</a>\n (job1)<br/>\n'
"some description1",
notice=False,
),
call(
self.fake_matrix_client,
self.fake_room_id,
"[🥦 RESOLVED] alert2: some description2",
'<font color="#33cc33">\n <b>[🥦 RESOLVED]</b>\n</font> '
'<a href="http://example.com/alert2">alert2</a>\n (job2)<br/>\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",
'<font color="#33cc33">\n <b>[🥦 RESOLVED]</b>\n</font> '
'<a href="http://example.com/alert2">alert2</a>\n (job2)<br/>\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") @patch.object(matrix_alertbot.webhook, "send_text_to_room")
async def test_post_alerts_in_unauthorized_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() fake_send_text_to_room.assert_not_called()
self.fake_cache.set.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") @patch.object(matrix_alertbot.webhook, "send_text_to_room")
async def test_post_alerts_with_empty_data( 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) self.assertEqual("Data must contain 'alerts' key.", error_msg)
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.assert_not_called()
self.fake_cache.set.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") @patch.object(matrix_alertbot.webhook, "send_text_to_room")
async def test_post_empty_alerts(self, fake_send_text_to_room: Mock) -> None: 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) self.assertEqual("Alerts cannot be empty.", error_msg)
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.assert_not_called()
self.fake_cache.set.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") @patch.object(matrix_alertbot.webhook, "send_text_to_room")
async def test_post_invalid_alerts(self, fake_send_text_to_room: Mock) -> None: 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) self.assertEqual("Alerts must be a list, got 'str'.", error_msg)
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.assert_not_called()
self.fake_cache.set.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") @patch.object(matrix_alertbot.webhook, "send_text_to_room")
async def test_post_alerts_with_empty_items( async def test_post_alerts_with_empty_items(
@ -171,6 +321,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
self.assertEqual("Invalid alert: {}.", error_msg) self.assertEqual("Invalid alert: {}.", error_msg)
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.assert_not_called()
self.fake_cache.set.assert_not_called() self.fake_cache.set.assert_not_called()
self.fake_cache.delete.assert_not_called()
@patch.object( @patch.object(
matrix_alertbot.webhook, matrix_alertbot.webhook,
@ -180,6 +331,10 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
async def test_post_alerts_raise_send_error( async def test_post_alerts_raise_send_error(
self, fake_send_text_to_room: Mock self, fake_send_text_to_room: Mock
) -> None: ) -> None:
self.fake_alertmanager_client.update_silence.side_effect = (
update_silence_raise_silence_not_found
)
data = self.fake_alerts data = self.fake_alerts
async with self.client.request( async with self.client.request(
"POST", f"/alerts/{self.fake_room_id}", json=data "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() fake_send_text_to_room.assert_called_once()
self.fake_cache.set.assert_not_called() self.fake_cache.set.assert_not_called()
self.fake_cache.delete.assert_called_once_with("fingerprint1")
async def test_health(self) -> None: async def test_health(self) -> None:
async with self.client.request("GET", "/health") as response: async with self.client.request("GET", "/health") as response:
@ -205,7 +361,8 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase): class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None: 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_cache = Mock(spec=Cache)
self.fake_config = Mock(spec=Config) self.fake_config = Mock(spec=Config)
@ -217,7 +374,12 @@ class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase):
@patch.object(matrix_alertbot.webhook.web, "TCPSite", autospec=True) @patch.object(matrix_alertbot.webhook.web, "TCPSite", autospec=True)
async def test_webhook_start_address_port(self, fake_tcp_site: Mock) -> None: 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() await webhook.start()
fake_tcp_site.assert_called_once_with( fake_tcp_site.assert_called_once_with(
@ -231,7 +393,12 @@ class WebhookServerTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_config.address = None self.fake_config.address = None
self.fake_config.port = 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() await webhook.start()
fake_unix_site.assert_called_once_with(webhook.runner, self.fake_config.socket) fake_unix_site.assert_called_once_with(webhook.runner, self.fake_config.socket)