refactor to handle one alert per matrix event

This commit is contained in:
HgO 2022-07-26 19:33:04 +02:00
parent f1691fc3a6
commit 5ed5a4aa08
12 changed files with 957 additions and 804 deletions

View file

@ -12,13 +12,13 @@ class Alert:
def __init__( def __init__(
self, self,
id: str, fingerprint: str,
url: str, url: str,
labels: Dict[str, str], labels: Dict[str, str],
annotations: Dict[str, str], annotations: Dict[str, str],
firing: bool = True, firing: bool = True,
): ):
self.id = id self.fingerprint = fingerprint
self.url = url self.url = url
self.firing = firing self.firing = firing
@ -33,7 +33,7 @@ class Alert:
@staticmethod @staticmethod
def from_dict(data: Dict) -> Alert: def from_dict(data: Dict) -> Alert:
return Alert( return Alert(
id=data["fingerprint"], fingerprint=data["fingerprint"],
url=data["generatorURL"], url=data["generatorURL"],
firing=data["status"] == "firing", firing=data["status"] == "firing",
labels=data["labels"], labels=data["labels"],

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, List from typing import Dict, List, Optional
import aiohttp import aiohttp
from aiohttp import ClientError from aiohttp import ClientError
@ -10,11 +10,11 @@ from diskcache import Cache
from matrix_alertbot.errors import ( from matrix_alertbot.errors import (
AlertmanagerServerError, AlertmanagerServerError,
AlertMismatchError,
AlertNotFoundError, AlertNotFoundError,
InvalidDurationError,
SilenceExpiredError,
SilenceNotFoundError, SilenceNotFoundError,
) )
from matrix_alertbot.matcher import AlertMatcher
class AlertmanagerClient: class AlertmanagerClient:
@ -40,28 +40,48 @@ class AlertmanagerClient:
alerts = await self.get_alerts() alerts = await self.get_alerts()
return self._find_alert(fingerprint, alerts) return self._find_alert(fingerprint, alerts)
async def get_silences(self) -> List[Dict]:
try:
async with self.session.get(f"{self.api_url}/silences") as response:
response.raise_for_status()
return await response.json()
except ClientError as e:
raise AlertmanagerServerError(
"Cannot fetch silences from Alertmanager"
) from e
async def get_silence(self, silence_id: str) -> Dict:
silences = await self.get_silences()
return self._find_silence(silence_id, silences)
async def create_silence( async def create_silence(
self, self,
fingerprint: str, fingerprint: str,
seconds: int,
user: str, user: str,
matchers: List[AlertMatcher], duration_seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
alert = await self.get_alert(fingerprint) alert = await self.get_alert(fingerprint)
self._match_alert(alert, matchers)
silence_matchers = [ silence_matchers = [
{"name": label, "value": value, "isRegex": False, "isEqual": True} {"name": label, "value": value, "isRegex": False, "isEqual": True}
for label, value in alert["labels"].items() for label, value in alert["labels"].items()
] ]
start_time = datetime.now() start_time = datetime.now()
if duration_seconds is None:
duration_delta = timedelta(seconds=seconds) end_time = datetime.max
end_time = start_time + duration_delta elif duration_seconds > 0:
try:
duration_delta = timedelta(seconds=duration_seconds)
end_time = start_time + duration_delta
except OverflowError:
end_time = datetime.max
else:
raise InvalidDurationError(f"Duration must be positive: {duration_seconds}")
silence = { silence = {
"id": silence_id,
"matchers": silence_matchers, "matchers": silence_matchers,
"startsAt": start_time.isoformat(), "startsAt": start_time.isoformat(),
"endsAt": end_time.isoformat(), "endsAt": end_time.isoformat(),
@ -82,33 +102,23 @@ class AlertmanagerClient:
return data["silenceID"] return data["silenceID"]
async def delete_silences( async def delete_silence(self, silence_id: str) -> None:
self, fingerprint: str, matchers: List[AlertMatcher] silence = await self.get_silence(silence_id)
) -> List[str]:
alert = await self.get_alert(fingerprint)
alert_state = alert["status"]["state"] silence_state = silence["state"]
if alert_state != "suppressed": if silence_state == "expired":
raise SilenceNotFoundError( raise SilenceExpiredError(
f"Cannot find silences for alert fingerprint {fingerprint} in state {alert_state}" f"Cannot delete already expired silence with ID {silence_id}"
) )
self._match_alert(alert, matchers)
silences = alert["status"]["silencedBy"]
for silence in silences:
await self._delete_silence(silence)
return silences
async def _delete_silence(self, silence: str) -> None:
try: try:
async with self.session.delete( async with self.session.delete(
f"{self.api_url}/silence/{silence}" f"{self.api_url}/silence/{silence_id}"
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
except ClientError as e: except ClientError as e:
raise AlertmanagerServerError( raise AlertmanagerServerError(
f"Cannot delete silence with ID {silence}" f"Cannot delete silence with ID {silence_id}"
) from e ) from e
@staticmethod @staticmethod
@ -119,16 +129,8 @@ class AlertmanagerClient:
raise AlertNotFoundError(f"Cannot find alert with fingerprint {fingerprint}") raise AlertNotFoundError(f"Cannot find alert with fingerprint {fingerprint}")
@staticmethod @staticmethod
def _match_alert(alert: Dict, matchers: List[AlertMatcher]) -> None: def _find_silence(silence_id: str, silences: List[Dict]) -> Dict:
labels = alert["labels"] for silence in silences:
for matcher in matchers: if silence["id"] == silence_id:
if matcher.label not in labels: return silence
labels_text = ", ".join(labels) raise SilenceNotFoundError(f"Cannot find silence with ID {silence_id}")
raise AlertMismatchError(
f"Cannot find label {matcher.label} in alert labels: {labels_text}"
)
if not matcher.match(labels):
raise AlertMismatchError(
f"Alert with label {matcher.label}={labels[matcher.label]} does not match {matcher}"
)

View file

@ -15,13 +15,13 @@ from nio import (
from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.chat_functions import strip_fallback from matrix_alertbot.chat_functions import strip_fallback
from matrix_alertbot.command import CommandFactory from matrix_alertbot.command import AckAlertCommand, CommandFactory, UnackAlertCommand
from matrix_alertbot.config import Config from matrix_alertbot.config import Config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
REACTION_DURATIONS = {"🤫": "12h", "😶": "1d", "🤐": "3d", "🙊": "5d", "🔇": "1w", "🔕": "3w"} REACTIONS = {"🤫", "😶", "🤐", "🙊", "🔇", "🔕"}
class Callbacks: class Callbacks:
@ -56,9 +56,6 @@ class Callbacks:
event: The event defining the message. event: The event defining the message.
""" """
# Extract the message text
msg = strip_fallback(event.body)
# Ignore messages from ourselves # Ignore messages from ourselves
if event.sender == self.client.user: if event.sender == self.client.user:
return return
@ -67,6 +64,9 @@ class Callbacks:
if room.room_id != self.config.room_id: if room.room_id != self.config.room_id:
return return
# Extract the message text
msg = strip_fallback(event.body)
logger.debug( logger.debug(
f"Bot message received for room {room.display_name} | " f"Bot message received for room {room.display_name} | "
f"{room.user_name(event.sender)}: {msg}" f"{room.user_name(event.sender)}: {msg}"
@ -75,19 +75,19 @@ class Callbacks:
has_command_prefix = msg.startswith(self.command_prefix) has_command_prefix = msg.startswith(self.command_prefix)
if not has_command_prefix: if not has_command_prefix:
logger.debug( logger.debug(
f"Message received without command prefix {self.command_prefix}: Aborting." f"Cannot process message: Command prefix {self.command_prefix} not provided."
) )
return return
source_content = event.source["content"] source_content = event.source["content"]
alert_event_id = ( reacted_to_event_id = (
source_content.get("m.relates_to", {}) source_content.get("m.relates_to", {})
.get("m.in_reply_to", {}) .get("m.in_reply_to", {})
.get("event_id") .get("event_id")
) )
if alert_event_id is None: if reacted_to_event_id is not None:
logger.warning("Unable to find the event ID of the alert") logger.debug(f"Command in reply to event ID {reacted_to_event_id}")
# Remove the command prefix # Remove the command prefix
cmd = msg[len(self.command_prefix) :] cmd = msg[len(self.command_prefix) :]
@ -101,10 +101,10 @@ class Callbacks:
room, room,
event.sender, event.sender,
event.event_id, event.event_id,
alert_event_id, reacted_to_event_id,
) )
except TypeError as e: except TypeError as e:
logging.error(f"Unable to create the command '{cmd}': {e}") logging.error(f"Cannot process command '{cmd}': {e}")
return return
await command.process() await command.process()
@ -176,10 +176,9 @@ class Callbacks:
reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key") reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key")
logger.debug(f"Got reaction {reaction} to {room.room_id} from {event.sender}.") logger.debug(f"Got reaction {reaction} to {room.room_id} from {event.sender}.")
if reaction not in REACTION_DURATIONS: if reaction not in REACTIONS:
logger.warning(f"Uknown duration reaction {reaction}") logger.warning(f"Uknown duration reaction {reaction}")
return return
duration = REACTION_DURATIONS[reaction]
# 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.client.room_get_event(room.room_id, alert_event_id)
@ -194,29 +193,17 @@ class Callbacks:
if reacted_to_event.sender != self.config.user_id: if reacted_to_event.sender != self.config.user_id:
return return
self.cache.set(
event.event_id,
reacted_to_event.event_id,
expire=self.config.cache_expire_time,
)
# Send a message acknowledging the reaction # Send a message acknowledging the reaction
cmd = f"ack {duration}" command = AckAlertCommand(
try: self.client,
command = CommandFactory.create( self.cache,
cmd, self.alertmanager,
self.client, self.config,
self.cache, room,
self.alertmanager, event.sender,
self.config, event.event_id,
room, alert_event_id,
event.sender, )
event.event_id,
alert_event_id,
)
except TypeError as e:
logging.error(f"Unable to create the command '{cmd}': {e}")
return
await command.process() await command.process()
@ -226,36 +213,29 @@ class Callbacks:
return return
# Ignore redactions from ourselves # Ignore redactions from ourselves
if event.sender == self.config.user_id: if event.sender == self.client.user:
return return
logger.debug( logger.debug(
f"Read alert event ID for redacted event {event.redacts} from cache" f"Read alert event ID for redacted event {event.redacts} from cache"
) )
if event.redacts not in self.cache:
logger.warning(
f"Unable to remove silences from event {event.redacts}: Redacted event is not in cache"
)
return
alert_event_id: str = self.cache[event.redacts]
try: try:
command = CommandFactory.create( reacted_to_event_id: str = self.cache[event.redacts]
"unack", except KeyError:
self.client, logger.warning(f"Unable to find silence from event {event.redacts}")
self.cache,
self.alertmanager,
self.config,
room,
event.sender,
event.redacts,
alert_event_id,
)
except TypeError as e:
logging.error(f"Unable to create the command 'unack': {e}")
return return
command = UnackAlertCommand(
self.client,
self.cache,
self.alertmanager,
self.config,
room,
event.sender,
event.redacts,
reacted_to_event_id,
)
await command.process() await command.process()
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None: async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None:

View file

@ -1,5 +1,5 @@
import logging import logging
from typing import List, Optional, Tuple from typing import Optional, Tuple
import pytimeparse2 import pytimeparse2
from diskcache import Cache from diskcache import Cache
@ -13,7 +13,6 @@ from matrix_alertbot.errors import (
AlertNotFoundError, AlertNotFoundError,
SilenceNotFoundError, SilenceNotFoundError,
) )
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,10 +24,10 @@ class BaseCommand:
cache: Cache, cache: Cache,
alertmanager: AlertmanagerClient, alertmanager: AlertmanagerClient,
config: Config, config: Config,
cmd: str,
room: MatrixRoom, room: MatrixRoom,
sender: str, sender: str,
event_id: str, event_id: str,
args: Tuple[str, ...] = None,
) -> None: ) -> None:
"""A command made by a user. """A command made by a user.
@ -53,12 +52,15 @@ class BaseCommand:
self.cache = cache self.cache = cache
self.alertmanager = alertmanager self.alertmanager = alertmanager
self.config = config self.config = config
self.cmd = cmd
self.args = cmd.split()[1:]
self.room = room self.room = room
self.sender = sender self.sender = sender
self.event_id = event_id self.event_id = event_id
if args is not None:
self.args = args
else:
self.args = ()
async def process(self) -> None: async def process(self) -> None:
raise NotImplementedError raise NotImplementedError
@ -70,166 +72,158 @@ class BaseAlertCommand(BaseCommand):
cache: Cache, cache: Cache,
alertmanager: AlertmanagerClient, alertmanager: AlertmanagerClient,
config: Config, config: Config,
cmd: str,
room: MatrixRoom, room: MatrixRoom,
sender: str, sender: str,
event_id: str, event_id: str,
alert_event_id: str, reacted_to_event_id: str,
args: Tuple[str, ...] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
client, cache, alertmanager, config, cmd, room, sender, event_id client, cache, alertmanager, config, room, sender, event_id, args
) )
self.alert_event_id = alert_event_id self.reacted_to_event_id = reacted_to_event_id
class AckAlertCommand(BaseAlertCommand): class AckAlertCommand(BaseAlertCommand):
async def process(self) -> None: async def process(self) -> None:
"""Acknowledge an alert and silence it for a certain duration in Alertmanager""" """Acknowledge an alert and silence it for a certain duration in Alertmanager"""
matchers: List[AlertMatcher] = [] durations = self.args
durations = []
for arg in self.args:
if "=~" in arg:
label, regex = arg.split("=~")
regex_matcher = AlertRegexMatcher(label, regex)
matchers.append(regex_matcher)
elif "=" in arg:
label, value = arg.split("=")
matcher = AlertMatcher(label, value)
matchers.append(matcher)
else:
durations.append(arg)
if len(durations) > 0: if len(durations) > 0:
duration = " ".join(durations) duration = " ".join(durations)
else: logger.debug(f"Receiving a command to create a silence for {duration}.")
duration = "1d"
logger.debug( duration_seconds = pytimeparse2.parse(duration)
f"Receiving a command to create a silence for a duration of {duration}" if duration_seconds is None:
) logger.error(f"Unable to create silence: Invalid duration '{duration}'")
await send_text_to_room(
duration_seconds = pytimeparse2.parse(duration) self.client,
if duration_seconds is None: self.room.room_id,
logger.error(f"Unable to create silence: Invalid duration '{duration}'") f"I tried really hard, but I can't convert the duration '{duration}' to a number of seconds.",
await send_text_to_room(
self.client,
self.room.room_id,
f"I tried really hard, but I can't convert the duration '{duration}' to a number of seconds.",
)
return
logger.debug(
f"Read alert fingerprints for alert event {self.alert_event_id} from cache"
)
if self.alert_event_id not in self.cache:
logger.error(
f"Cannot find fingerprints for alert event {self.alert_event_id} in cache"
)
return
alert_fingerprints: Tuple[str] = self.cache[self.alert_event_id]
logger.debug(f"Found {len(alert_fingerprints)} in cache")
count_alert_not_found = 0
created_silences = []
for alert_fingerprint in alert_fingerprints:
logger.debug(
f"Create silence for alert with fingerprint {alert_fingerprint} for a duration of {duration}"
)
try:
silence_id = await self.alertmanager.create_silence(
alert_fingerprint,
duration_seconds,
self.room.user_name(self.sender),
matchers,
) )
created_silences.append(silence_id) return
except AlertNotFoundError as e: else:
logger.warning(f"Unable to create silence: {e}") duration_seconds = None
count_alert_not_found += 1 logger.debug(
except AlertmanagerError as e: "Receiving a command to create a silence for an indefinite period"
logger.exception(f"Unable to create silence: {e}", exc_info=e) )
matchers_id = "".join(sorted(str(matcher) for matcher in matchers)) logger.debug(
ack_id = "".join(alert_fingerprints) + str(duration_seconds) + matchers_id f"Reading alert fingerprint for event {self.reacted_to_event_id} from cache"
self.cache.set(ack_id, tuple(created_silences), expire=duration_seconds) )
try:
alert_fingerprint: str = self.cache[self.reacted_to_event_id]
except KeyError:
logger.error(
f"Cannot find fingerprint for alert event {self.reacted_to_event_id} in cache"
)
return
if count_alert_not_found > 0: cached_silence_id = self.cache.get(alert_fingerprint)
if cached_silence_id is None:
logger.debug(
f"Creating silence for alert with fingerprint {alert_fingerprint}."
)
else:
logger.debug(
f"Updating silence with ID {cached_silence_id} for alert with fingerprint {alert_fingerprint}."
)
try:
silence_id = await self.alertmanager.create_silence(
alert_fingerprint,
self.room.user_name(self.sender),
duration_seconds,
cached_silence_id,
)
except AlertNotFoundError as e:
logger.warning(f"Unable to create silence: {e}")
await send_text_to_room( await send_text_to_room(
self.client, self.client,
self.room.room_id, self.room.room_id,
f"Sorry, I couldn't find {count_alert_not_found} alerts, therefore I couldn't create their silence.", f"Sorry, I couldn't find alert with fingerprint {alert_fingerprint}, therefore "
"I couldn't create the silence.",
) )
return
if len(created_silences) > 0: except AlertmanagerError as 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.client,
self.room.room_id, self.room.room_id,
f"Created {len(created_silences)} silences with a duration of {duration}.", "Something went wrong with Alertmanager, therefore "
f"I couldn't create silence for alert fingerprint {alert_fingerprint}.",
) )
return
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(
self.client,
self.room.room_id,
f"Created silence with ID {silence_id}.",
)
class UnackAlertCommand(BaseAlertCommand): class UnackAlertCommand(BaseAlertCommand):
async def process(self) -> None: async def process(self) -> None:
"""Delete an alert's acknowledgement of an alert and remove corresponding silence in Alertmanager""" """Delete an alert's acknowledgement of an alert and remove corresponding silence in Alertmanager"""
matchers: List[AlertMatcher] = []
for arg in self.args:
if "=~" in arg:
label, regex = arg.split("=~")
regex_matcher = AlertRegexMatcher(label, regex)
matchers.append(regex_matcher)
elif "=" in arg:
label, value = arg.split("=")
matcher = AlertMatcher(label, value)
matchers.append(matcher)
logger.debug("Receiving a command to delete a silence") logger.debug("Receiving a command to delete a silence")
logger.debug( logger.debug(
f"Read alert fingerprints for alert event {self.alert_event_id} from cache" f"Reading alert fingerprint for event {self.reacted_to_event_id} from cache."
)
try:
alert_fingerprint: str = self.cache[self.reacted_to_event_id]
except KeyError:
logger.error(
f"Cannot find fingerprint for alert event {self.reacted_to_event_id} in cache."
)
return
logger.debug(f"Found alert fingerprint {alert_fingerprint} in cache.")
logger.debug(
f"Reading silence ID for alert fingerprint {alert_fingerprint} from cache."
)
try:
silence_id: str = self.cache[alert_fingerprint]
except KeyError:
logger.error(
f"Cannot find silence for alert fingerprint {alert_fingerprint} in cache"
)
return
logger.debug(f"Found silence ID {silence_id} in cache.")
logger.debug(
f"Deleting silence with ID {silence_id} for alert with fingerprint {alert_fingerprint}"
) )
if self.alert_event_id not in self.cache: try:
logger.error( await self.alertmanager.delete_silence(silence_id)
f"Cannot find fingerprints for event {self.alert_event_id} in cache" except (AlertNotFoundError, SilenceNotFoundError) as e:
logger.error(f"Unable to delete silence: {e}")
await send_text_to_room(
self.client,
self.room.room_id,
f"Sorry, I couldn't find alert with fingerprint {alert_fingerprint}, therefore "
"I couldn't remove its silence.",
)
return
except AlertmanagerError as e:
logger.exception(f"Unable to delete silence: {e}", exc_info=e)
await send_text_to_room(
self.client,
self.room.room_id,
"Something went wrong with Alertmanager, therefore "
f"I couldn't delete silence for alert fingerprint {alert_fingerprint}.",
) )
return return
alert_fingerprints: Tuple[str] = self.cache[self.alert_event_id] await send_text_to_room(
logger.debug(f"Found {len(alert_fingerprints)} in cache") self.client,
self.room.room_id,
count_alert_not_found = 0 f"Removed silence with ID {silence_id}.",
count_removed_silences = 0 )
for alert_fingerprint in alert_fingerprints:
logger.debug(
f"Delete silence for alert with fingerprint {alert_fingerprint}"
)
try:
removed_silences = await self.alertmanager.delete_silences(
alert_fingerprint, matchers
)
count_removed_silences += len(removed_silences)
except (AlertNotFoundError, SilenceNotFoundError) as e:
logger.error(f"Unable to delete silence: {e}")
count_alert_not_found += 1
except AlertmanagerError as e:
logger.exception(f"Unable to delete silence: {e}", exc_info=e)
if count_alert_not_found > 0:
await send_text_to_room(
self.client,
self.room.room_id,
f"Sorry, I couldn't find {count_alert_not_found} alerts, therefore I couldn't remove their silences.",
)
if count_removed_silences > 0:
await send_text_to_room(
self.client,
self.room.room_id,
f"Removed {count_removed_silences} silences.",
)
class HelpCommand(BaseCommand): class HelpCommand(BaseCommand):
@ -262,7 +256,7 @@ class UnknownCommand(BaseCommand):
await send_text_to_room( await send_text_to_room(
self.client, self.client,
self.room.room_id, self.room.room_id,
f"Unknown command '{self.cmd}'. Try the 'help' command for more information.", "Unknown command. Try the 'help' command for more information.",
) )
@ -279,6 +273,8 @@ class CommandFactory:
event_id: str, event_id: str,
reacted_to_event_id: Optional[str] = None, reacted_to_event_id: Optional[str] = None,
) -> BaseCommand: ) -> BaseCommand:
args = tuple(cmd.split()[1:])
if cmd.startswith("ack"): if cmd.startswith("ack"):
if reacted_to_event_id is None: if reacted_to_event_id is None:
raise TypeError("Alert command must be in reply to an alert event.") raise TypeError("Alert command must be in reply to an alert event.")
@ -288,11 +284,11 @@ class CommandFactory:
cache, cache,
alertmanager, alertmanager,
config, config,
cmd,
room, room,
sender, sender,
event_id, event_id,
reacted_to_event_id, reacted_to_event_id,
args,
) )
elif cmd.startswith("unack") or cmd.startswith("nack"): elif cmd.startswith("unack") or cmd.startswith("nack"):
if reacted_to_event_id is None: if reacted_to_event_id is None:
@ -303,17 +299,17 @@ class CommandFactory:
cache, cache,
alertmanager, alertmanager,
config, config,
cmd,
room, room,
sender, sender,
event_id, event_id,
reacted_to_event_id, reacted_to_event_id,
args,
) )
elif cmd.startswith("help"): elif cmd.startswith("help"):
return HelpCommand( return HelpCommand(
client, cache, alertmanager, config, cmd, room, sender, event_id client, cache, alertmanager, config, room, sender, event_id, args
) )
else: else:
return UnknownCommand( return UnknownCommand(
client, cache, alertmanager, config, cmd, room, sender, event_id client, cache, alertmanager, config, room, sender, event_id, args
) )

View file

@ -37,14 +37,20 @@ class AlertNotFoundError(AlertmanagerError):
pass pass
class AlertMismatchError(AlertmanagerError): class SilenceNotFoundError(AlertmanagerError):
"""An error encountered when alert's labels don't match.""" """An error encountered when a silence cannot be found in Alertmanager."""
pass pass
class SilenceNotFoundError(AlertmanagerError): class SilenceExpiredError(AlertmanagerError):
"""An error encountered when a silence cannot be found in Alertmanager.""" """An error encountered when a silence is already expired in Alertmanager."""
pass
class InvalidDurationError(AlertmanagerError):
"""An error encountered when an alert has an invalid duration."""
pass pass

View file

@ -1,35 +0,0 @@
import re
from typing import Any, Dict
class AlertMatcher:
def __init__(self, label: str, value: str) -> None:
self.label = label
self.value = value
def match(self, labels: Dict[str, str]) -> bool:
return self.label in labels and self.value == labels[self.label]
def __str__(self) -> str:
return f"{self.label}={self.value}"
def __repr__(self) -> str:
return f"AlertMatcher({self})"
def __eq__(self, matcher: Any) -> bool:
return str(self) == str(matcher)
class AlertRegexMatcher(AlertMatcher):
def __init__(self, label: str, regex: str) -> None:
super().__init__(label, regex)
self.regex = re.compile(regex)
def __str__(self) -> str:
return f"{self.label}=~{self.value}"
def __repr__(self) -> str:
return f"AlertRegexMatcher({self})"
def match(self, labels: Dict[str, str]) -> bool:
return self.label in labels and self.regex.match(labels[self.label]) is not None

View file

@ -24,9 +24,9 @@ async def get_health(request: web_request.Request) -> web.Response:
@routes.post("/alerts") @routes.post("/alerts")
async def create_alert(request: web_request.Request) -> web.Response: async def create_alerts(request: web_request.Request) -> web.Response:
data = await request.json() data = await request.json()
logger.info(f"Received alert: {data}") logger.info(f"Received alerts: {data}")
client: AsyncClient = request.app["client"] client: AsyncClient = request.app["client"]
config: Config = request.app["config"] config: Config = request.app["config"]
cache: Cache = request.app["cache"] cache: Cache = request.app["cache"]
@ -40,34 +40,30 @@ async def create_alert(request: web_request.Request) -> web.Response:
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.")
plaintext = "" for alert in data["alerts"]:
html = ""
for i, alert in enumerate(data["alerts"]):
try: try:
alert = Alert.from_dict(alert) alert = Alert.from_dict(alert)
except KeyError: except KeyError:
return web.Response(status=400, body=f"Invalid alert: {alert}.") return web.Response(status=400, body=f"Invalid alert: {alert}.")
if i != 0: plaintext = alert.plaintext()
plaintext += "\n" html = alert.html()
html += "<br/>\n"
plaintext += alert.plaintext()
html += alert.html()
try: try:
event = await send_text_to_room( event = await send_text_to_room(
client, config.room_id, plaintext, html, notice=False client, config.room_id, plaintext, html, notice=False
) )
except (LocalProtocolError, ClientError) as e: except (LocalProtocolError, ClientError) as e:
logger.error(e) logger.error(
return web.Response( f"Unable to send alert {alert.fingerprint} to Matrix room: {e}"
status=500, body="An error occured when sending alerts to Matrix room." )
) return web.Response(
status=500,
body=f"An error occured when sending alert with fingerprint '{alert.fingerprint}' to Matrix room.",
)
cache.set(event.event_id, alert.fingerprint, expire=config.cache_expire_time)
fingerprints = tuple(sorted(alert["fingerprint"] for alert in data["alerts"]))
cache.set(
event.event_id, fingerprints, expire=config.cache_expire_time, tag="event"
)
return web.Response(status=200) return web.Response(status=200)

View file

@ -18,7 +18,7 @@ class AlertTestCase(unittest.TestCase):
self.alert_dict["status"] = "firing" self.alert_dict["status"] = "firing"
alert = Alert.from_dict(self.alert_dict) alert = Alert.from_dict(self.alert_dict)
self.assertEqual("fingerprint1", alert.id) self.assertEqual("fingerprint1", alert.fingerprint)
self.assertEqual("http://example.com", alert.url) self.assertEqual("http://example.com", alert.url)
self.assertTrue(alert.firing) self.assertTrue(alert.firing)
self.assertEqual("critical", alert.status) self.assertEqual("critical", alert.status)

View file

@ -2,24 +2,25 @@ from __future__ import annotations
import json import json
import unittest import unittest
from datetime import datetime from datetime import datetime, timedelta
from typing import Any, List from typing import Any
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock
import aiohttp import aiohttp
import aiohttp.test_utils import aiohttp.test_utils
import aiotools import aiotools
from aiohttp import web, web_request from aiohttp import web, web_request
from diskcache import Cache from diskcache import Cache
from freezegun import freeze_time
from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.errors import ( from matrix_alertbot.errors import (
AlertmanagerServerError, AlertmanagerServerError,
AlertMismatchError,
AlertNotFoundError, AlertNotFoundError,
InvalidDurationError,
SilenceExpiredError,
SilenceNotFoundError, SilenceNotFoundError,
) )
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
class FakeTimeDelta: class FakeTimeDelta:
@ -36,10 +37,15 @@ class AbstractFakeAlertmanagerServer:
self.app.router.add_routes( self.app.router.add_routes(
[ [
web.get("/api/v2/alerts", self.get_alerts), web.get("/api/v2/alerts", self.get_alerts),
web.get("/api/v2/silences", self.get_silences),
web.post("/api/v2/silences", self.create_silence), web.post("/api/v2/silences", self.create_silence),
web.delete("/api/v2/silence/{silence}", self.delete_silence), web.delete("/api/v2/silence/{silence}", self.delete_silence),
] ]
) )
self.app["silences"] = [
{"id": "silence1", "state": "active"},
{"id": "silence2", "state": "expired"},
]
self.runner = web.AppRunner(self.app) self.runner = web.AppRunner(self.app)
@ -64,6 +70,9 @@ class AbstractFakeAlertmanagerServer:
async def get_alerts(self, request: web_request.Request) -> web.Response: async def get_alerts(self, request: web_request.Request) -> web.Response:
raise NotImplementedError raise NotImplementedError
async def get_silences(self, request: web_request.Request) -> web.Response:
raise NotImplementedError
async def create_silence(self, request: web_request.Request) -> web.Response: async def create_silence(self, request: web_request.Request) -> web.Response:
raise NotImplementedError raise NotImplementedError
@ -94,25 +103,56 @@ class FakeAlertmanagerServer(AbstractFakeAlertmanagerServer):
content_type="application/json", content_type="application/json",
) )
async def create_silence(self, request: web_request.Request) -> web.Response: async def get_silences(self, request: web_request.Request) -> web.Response:
return web.Response( return web.Response(
body=json.dumps({"silenceID": "silence1"}), content_type="application/json" body=json.dumps(self.app["silences"]), content_type="application/json"
)
async def create_silence(self, request: web_request.Request) -> web.Response:
silences = self.app["silences"]
silence = await request.json()
if silence["id"] is None:
silence["id"] = "silence1"
silence["state"] = "active"
silences.append(silence)
return web.Response(
body=json.dumps({"silenceID": silence["id"]}),
content_type="application/json",
) )
async def delete_silence(self, request: web_request.Request) -> web.Response: async def delete_silence(self, request: web_request.Request) -> web.Response:
silence_id = request.match_info["silence"]
for i, silence in enumerate(self.app["silences"]):
if silence["id"] == silence_id:
del self.app["silences"][i]
break
return web.Response(status=200, content_type="application/json") return web.Response(status=200, content_type="application/json")
class FakeAlertmanagerServerWithoutAlert(AbstractFakeAlertmanagerServer): class FakeAlertmanagerServerWithoutAlert(FakeAlertmanagerServer):
async def get_alerts(self, request: web_request.Request) -> web.Response: async def get_alerts(self, request: web_request.Request) -> web.Response:
return web.Response(body=json.dumps([]), content_type="application/json") return web.Response(body=json.dumps([]), content_type="application/json")
class FakeAlertmanagerServerWithErrorAlerts(AbstractFakeAlertmanagerServer): class FakeAlertmanagerServerWithErrorAlerts(FakeAlertmanagerServer):
async def get_alerts(self, request: web_request.Request) -> web.Response: async def get_alerts(self, request: web_request.Request) -> web.Response:
return web.Response(status=500) return web.Response(status=500)
class FakeAlertmanagerServerWithoutSilence(FakeAlertmanagerServer):
def __init__(self) -> None:
super().__init__()
self.app["silences"] = []
class FakeAlertmanagerServerWithErrorSilences(FakeAlertmanagerServer):
async def get_silences(self, request: web_request.Request) -> web.Response:
return web.Response(status=500)
class FakeAlertmanagerServerWithErrorCreateSilence(FakeAlertmanagerServer): class FakeAlertmanagerServerWithErrorCreateSilence(FakeAlertmanagerServer):
async def create_silence(self, request: web_request.Request) -> web.Response: async def create_silence(self, request: web_request.Request) -> web.Response:
return web.Response(status=500) return web.Response(status=500)
@ -137,24 +177,25 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
alerts = await alertmanager.get_alerts() alerts = await alertmanager.get_alerts()
self.assertEqual(
[ self.assertEqual(
{ [
"fingerprint": "fingerprint1", {
"labels": {"alertname": "alert1"}, "fingerprint": "fingerprint1",
"status": {"state": "active"}, "labels": {"alertname": "alert1"},
}, "status": {"state": "active"},
{ },
"fingerprint": "fingerprint2", {
"labels": {"alertname": "alert2"}, "fingerprint": "fingerprint2",
"status": { "labels": {"alertname": "alert2"},
"state": "suppressed", "status": {
"silencedBy": ["silence1", "silence2"], "state": "suppressed",
}, "silencedBy": ["silence1", "silence2"],
}, },
], },
alerts, ],
) alerts,
)
async def test_get_alerts_empty(self) -> None: async def test_get_alerts_empty(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
@ -164,7 +205,8 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
alerts = await alertmanager.get_alerts() alerts = await alertmanager.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:
async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server:
@ -176,6 +218,44 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_alerts() await alertmanager.get_alerts()
async def test_get_silences_happy(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):
silences = await alertmanager.get_silences()
self.assertEqual(
[
{"id": "silence1", "state": "active"},
{"id": "silence2", "state": "expired"},
],
silences,
)
async def test_get_silences_empty(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):
silences = await alertmanager.get_silences()
self.assertEqual([], silences)
async def test_get_silences_raise_alertmanager_error(self) -> None:
async with FakeAlertmanagerServerWithErrorSilences() 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(AlertmanagerServerError):
await alertmanager.get_silences()
async def test_get_alert_happy(self) -> None: async def test_get_alert_happy(self) -> None:
async with FakeAlertmanagerServer() as fake_alertmanager_server: async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
@ -184,14 +264,15 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
alert = await alertmanager.get_alert("fingerprint1") alert = await alertmanager.get_alert("fingerprint1")
self.assertEqual(
{ self.assertEqual(
"fingerprint": "fingerprint1", {
"labels": {"alertname": "alert1"}, "fingerprint": "fingerprint1",
"status": {"state": "active"}, "labels": {"alertname": "alert1"},
}, "status": {"state": "active"},
alert, },
) alert,
)
async def test_get_alert_raise_alert_not_found(self) -> None: async def test_get_alert_raise_alert_not_found(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
@ -213,120 +294,189 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_alert("fingerprint1") await alertmanager.get_alert("fingerprint1")
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta) async def test_get_silence_happy(self) -> None:
async def test_create_silence_without_matchers(self, fake_timedelta: Mock) -> None:
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 = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", self.fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
silence = await alertmanager.create_silence( silence1 = await alertmanager.get_silence("silence1")
"fingerprint1", 86400, "user", [] silence2 = await alertmanager.get_silence("silence2")
self.assertEqual(
{"id": "silence1", "state": "active"},
silence1,
)
self.assertEqual(
{"id": "silence2", "state": "expired"},
silence2,
)
async def test_get_silence_raise_silence_not_found(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):
with self.assertRaises(SilenceNotFoundError):
await alertmanager.get_silence("silence1")
async def test_get_silence_raise_alertmanager_error(self) -> None:
async with FakeAlertmanagerServerWithErrorSilences() 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(AlertmanagerServerError):
await alertmanager.get_silence("silence1")
@freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence(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", 86400
) )
silence = await alertmanager.get_silence("silence1")
self.assertEqual("silence1", silence) self.assertEqual("silence1", silence_id)
fake_timedelta.assert_called_once_with(seconds=86400) self.assertEqual(
{
"id": "silence1",
"state": "active",
"matchers": [
{
"name": "alertname",
"value": "alert1",
"isRegex": False,
"isEqual": True,
}
],
"createdBy": "user",
"startsAt": "1970-01-01T00:00:00",
"endsAt": "1970-01-02T00:00:00",
"comment": "Acknowledge alert from Matrix",
},
silence,
)
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta) @freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_with_matchers(self, fake_timedelta: Mock) -> None: async def test_create_silence_with_id(self) -> None:
matchers = [AlertMatcher(label="alertname", value="alert1")] async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", self.fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
silence = await alertmanager.create_silence( silence_id = await alertmanager.create_silence(
"fingerprint1", "fingerprint1", "user", 86400, "silence2"
86400,
"user",
matchers,
) )
silence = await alertmanager.get_silence("silence2")
self.assertEqual("silence1", silence) self.assertEqual("silence2", silence_id)
fake_timedelta.assert_called_once_with(seconds=86400) self.assertEqual(
{
"id": "silence2",
"state": "active",
"matchers": [
{
"name": "alertname",
"value": "alert1",
"isRegex": False,
"isEqual": True,
}
],
"createdBy": "user",
"startsAt": "1970-01-01T00:00:00",
"endsAt": "1970-01-02T00:00:00",
"comment": "Acknowledge alert from Matrix",
},
silence,
)
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta) @freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_with_regex_matchers( async def test_create_silence_with_indefinite_duration(self) -> None:
self, fake_timedelta: Mock async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
) -> None:
matchers: List[AlertMatcher] = [
AlertRegexMatcher(label="alertname", regex=r"alert\d+")
]
async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", self.fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
silence = await alertmanager.create_silence( silence_id = await alertmanager.create_silence("fingerprint1", "user")
"fingerprint1", silence = await alertmanager.get_silence("silence1")
86400,
"user", self.assertEqual("silence1", silence_id)
matchers, self.assertEqual(
{
"id": "silence1",
"state": "active",
"matchers": [
{
"name": "alertname",
"value": "alert1",
"isRegex": False,
"isEqual": True,
}
],
"createdBy": "user",
"startsAt": "1970-01-01T00:00:00",
"endsAt": "9999-12-31T23:59:59.999999",
"comment": "Acknowledge alert from Matrix",
},
silence,
)
@freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_with_max_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", int(timedelta.max.total_seconds())
) )
silence = await alertmanager.get_silence("silence1")
self.assertEqual("silence1", silence) self.assertEqual("silence1", silence_id)
fake_timedelta.assert_called_once_with(seconds=86400) self.assertEqual(
{
"id": "silence1",
"state": "active",
"matchers": [
{
"name": "alertname",
"value": "alert1",
"isRegex": False,
"isEqual": True,
}
],
"createdBy": "user",
"startsAt": "1970-01-01T00:00:00",
"endsAt": "9999-12-31T23:59:59.999999",
"comment": "Acknowledge alert from Matrix",
},
silence,
)
async def test_create_silence_raise_missing_label(self) -> None: @freeze_time(datetime.utcfromtimestamp(0))
matchers = [ async def test_create_silence_raise_duration_error(self) -> None:
AlertMatcher(label="alertname", value="alert1"), async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
AlertMatcher(label="severity", value="critical"),
]
async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient( alertmanager = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", self.fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
with self.assertRaises(AlertMismatchError): with self.assertRaises(InvalidDurationError):
await alertmanager.create_silence( await alertmanager.create_silence("fingerprint1", "user", -1)
"fingerprint1",
86400,
"user",
matchers,
)
async def test_create_silence_raise_mismatch_label(self) -> None:
matchers = [AlertMatcher(label="alertname", value="alert2")]
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(AlertMismatchError):
await alertmanager.create_silence(
"fingerprint1",
86400,
"user",
matchers,
)
async def test_create_silence_raise_mismatch_regex_label(self) -> None:
matchers: List[AlertMatcher] = [
AlertRegexMatcher(label="alertname", regex=r"alert[^\d]+")
]
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(AlertMismatchError):
await alertmanager.create_silence(
"fingerprint1",
86400,
"user",
matchers,
)
async def test_create_silence_raise_alert_not_found(self) -> None: async def test_create_silence_raise_alert_not_found(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server: async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
@ -336,7 +486,7 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
with self.assertRaises(AlertNotFoundError): with self.assertRaises(AlertNotFoundError):
await alertmanager.create_silence("fingerprint1", 86400, "user", []) await alertmanager.create_silence("fingerprint1", "user")
async def test_create_silence_raise_alertmanager_error(self) -> None: async def test_create_silence_raise_alertmanager_error(self) -> None:
async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server: async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server:
@ -348,111 +498,40 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
await alertmanager.get_alert("fingerprint1") await alertmanager.get_alert("fingerprint1")
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.create_silence("fingerprint1", 86400, "user", []) await alertmanager.create_silence("fingerprint1", "user")
async def test_delete_silences_without_matchers(self) -> None: async def test_delete_silence(self) -> None:
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 = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", self.fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
silences = await alertmanager.delete_silences("fingerprint2", []) await alertmanager.delete_silence("silence1")
silences = await alertmanager.get_silences()
self.assertEqual(["silence1", "silence2"], silences) self.assertEqual([{"id": "silence2", "state": "expired"}], silences)
async def test_delete_silences_with_matchers(self) -> None:
matchers = [AlertMatcher(label="alertname", value="alert2")]
async def test_delete_silence_raise_silence_expired(self) -> None:
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 = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache f"http://localhost:{port}", self.fake_cache
) )
async with aiotools.closing_async(alertmanager): async with aiotools.closing_async(alertmanager):
silences = await alertmanager.delete_silences("fingerprint2", matchers) with self.assertRaises(SilenceExpiredError):
await alertmanager.delete_silence("silence2")
silences = await alertmanager.get_silences()
self.assertEqual(["silence1", "silence2"], silences) self.assertEqual(
[
{"id": "silence1", "state": "active"},
{"id": "silence2", "state": "expired"},
],
silences,
)
async def test_delete_silences_with_regex_matchers(self) -> None: async def test_delete_silence_raise_alertmanager_error(self) -> None:
matchers: List[AlertMatcher] = [
AlertRegexMatcher(label="alertname", regex=r"alert\d+")
]
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):
silences = await alertmanager.delete_silences("fingerprint2", matchers)
self.assertEqual(["silence1", "silence2"], silences)
async def test_delete_silences_raise_missing_label(self) -> None:
matchers = [
AlertMatcher(label="alertname", value="alert2"),
AlertMatcher(label="severity", value="critical"),
]
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(AlertMismatchError):
await alertmanager.delete_silences("fingerprint2", matchers)
async def test_delete_silences_raise_mismatch_label(self) -> None:
matchers = [
AlertMatcher(label="alertname", value="alert1"),
]
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(AlertMismatchError):
await alertmanager.delete_silences("fingerprint2", matchers)
async def test_delete_silences_raise_mismatch_regex_label(self) -> None:
matchers: List[AlertMatcher] = [
AlertRegexMatcher(label="alertname", regex=r"alert[^\d]+"),
]
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(AlertMismatchError):
await alertmanager.delete_silences("fingerprint2", matchers)
async def test_delete_silences_raise_silence_not_found(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(SilenceNotFoundError):
await alertmanager.delete_silences("fingerprint1", [])
async def test_delete_silences_raise_alert_not_found(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() 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(AlertNotFoundError):
await alertmanager.delete_silences("fingerprint2", [])
async def test_delete_silences_raise_alertmanager_error(self) -> None:
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 = AlertmanagerClient(
@ -462,7 +541,7 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
await alertmanager.get_alert("fingerprint1") await alertmanager.get_alert("fingerprint1")
with self.assertRaises(AlertmanagerServerError): with self.assertRaises(AlertmanagerServerError):
await alertmanager.delete_silences("fingerprint2", []) await alertmanager.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) alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
@ -474,9 +553,26 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
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) alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
with self.assertRaises(AlertNotFoundError):
alertmanager._find_alert("fingerprint1", [])
with self.assertRaises(AlertNotFoundError): with self.assertRaises(AlertNotFoundError):
alertmanager._find_alert("fingerprint2", [{"fingerprint": "fingerprint1"}]) alertmanager._find_alert("fingerprint2", [{"fingerprint": "fingerprint1"}])
async def test_find_silence_happy(self) -> None:
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
silence = alertmanager._find_silence("silence1", [{"id": "silence1"}])
self.assertEqual({"id": "silence1"}, silence)
async def test_find_silence_raise_silence_not_found(self) -> None:
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
with self.assertRaises(SilenceNotFoundError):
alertmanager._find_silence("silence1", [])
with self.assertRaises(SilenceNotFoundError):
alertmanager._find_silence("silence2", [{"id": "silence1"}])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -1,11 +1,12 @@
import unittest import unittest
from typing import Dict
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
import nio import nio
from diskcache import Cache from diskcache import Cache
import matrix_alertbot.command
import matrix_alertbot.callback import matrix_alertbot.callback
import matrix_alertbot.command
from matrix_alertbot.alertmanager import AlertmanagerClient from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.callback import Callbacks from matrix_alertbot.callback import Callbacks
from matrix_alertbot.command import BaseCommand from matrix_alertbot.command import BaseCommand
@ -86,10 +87,10 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"help",
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
fake_message_event.event_id, fake_message_event.event_id,
(),
) )
fake_command.return_value.process.assert_called_once() fake_command.return_value.process.assert_called_once()
@ -117,13 +118,47 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"help",
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
fake_message_event.event_id, fake_message_event.event_id,
(),
) )
fake_command.return_value.process.assert_called_once() fake_command.return_value.process.assert_called_once()
@patch.object(matrix_alertbot.command.CommandFactory, "create", autospec=True)
async def test_ignore_message_sent_by_bot(self, fake_create_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_message_event = Mock(spec=nio.RoomMessageText)
fake_message_event.sender = self.fake_client.user
# Pretend that we received a text message event
await self.callbacks.message(self.fake_room, fake_message_event)
# Check that we attempted to execute the command
fake_create_command.assert_not_called()
@patch.object(matrix_alertbot.command.CommandFactory, "create", autospec=True)
async def test_ignore_message_sent_on_unauthorized_room(
self, fake_create_command: Mock
) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
self.fake_room.room_id = "!unauthorizedroom@example.com"
fake_message_event = Mock(spec=nio.RoomMessageText)
fake_message_event.sender = "@some_other_fake_user:example.com"
self.assertNotEqual(self.fake_config.room_id, self.fake_room.room_id)
# Pretend that we received a text message event
await self.callbacks.message(self.fake_room, fake_message_event)
# Check that we attempted to execute the command
fake_create_command.assert_not_called()
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) @patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
async def test_message_ack_not_in_reply_with_prefix( async def test_message_ack_not_in_reply_with_prefix(
self, fake_command: Mock self, fake_command: Mock
@ -165,15 +200,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack",
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
fake_message_event.event_id, fake_message_event.event_id,
"some alert event id", "some alert event id",
(),
) )
fake_command.return_value.process.assert_called_once() fake_command.return_value.process.assert_called_once()
@patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_message_unack_not_in_reply_with_prefix( async def test_message_unack_not_in_reply_with_prefix(
self, fake_command: Mock self, fake_command: Mock
) -> None: ) -> None:
@ -214,15 +249,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack",
self.fake_room, self.fake_room,
fake_message_event.sender, fake_message_event.sender,
fake_message_event.event_id, fake_message_event.event_id,
"some alert event id", "some alert event id",
(),
) )
fake_command.return_value.process.assert_called_once() fake_command.return_value.process.assert_called_once()
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_existing_alert(self, fake_command: Mock) -> None: async def test_reaction_to_existing_alert(self, fake_command: Mock) -> None:
"""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
@ -259,29 +294,21 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack 12h",
self.fake_room, self.fake_room,
fake_reaction_event.sender, fake_reaction_event.sender,
fake_reaction_event.event_id, fake_reaction_event.event_id,
"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_cache.set.assert_called_once_with(
fake_reaction_event.event_id,
fake_alert_event.event_id,
expire=self.fake_config.cache_expire_time,
)
self.fake_client.room_get_event.assert_called_once_with( self.fake_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
) )
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_unknown_event(self, fake_command: Mock) -> None: async def test_reaction_to_inexistent_event(self, fake_command: Mock) -> None:
"""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_alert_event = Mock(spec=nio.RoomMessageText) fake_alert_event_id = "some alert event id"
fake_alert_event.event_id = "some alert event id"
fake_alert_event.sender = self.fake_config.user_id
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"
@ -290,7 +317,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_reaction_event.source = { fake_reaction_event.source = {
"content": { "content": {
"m.relates_to": { "m.relates_to": {
"event_id": fake_alert_event.event_id, "event_id": fake_alert_event_id,
"key": "🤫", "key": "🤫",
"rel_type": "m.annotation", "rel_type": "m.annotation",
} }
@ -309,11 +336,11 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
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_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_id
) )
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_event_with_incorrect_sender( async def test_reaction_to_event_not_from_bot_user(
self, fake_command: Mock self, fake_command: Mock
) -> None: ) -> None:
"""Tests the callback for RoomMessageText with the command prefix""" """Tests the callback for RoomMessageText with the command prefix"""
@ -352,12 +379,11 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_room.room_id, fake_alert_event.event_id self.fake_room.room_id, fake_alert_event.event_id
) )
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_unknown(self, fake_command: Mock) -> None: async def test_reaction_unknown(self, fake_command: Mock) -> None:
"""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_alert_event = Mock(spec=nio.RoomMessageText) fake_alert_event_id = "some alert event id"
fake_alert_event.event_id = "some alert event id"
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"
@ -366,7 +392,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_reaction_event.source = { fake_reaction_event.source = {
"content": { "content": {
"m.relates_to": { "m.relates_to": {
"event_id": fake_alert_event.event_id, "event_id": fake_alert_event_id,
"key": "unknown", "key": "unknown",
"rel_type": "m.annotation", "rel_type": "m.annotation",
} }
@ -380,17 +406,83 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_command.assert_not_called() fake_command.assert_not_called()
self.fake_client.room_get_event.assert_not_called() self.fake_client.room_get_event.assert_not_called()
@patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_ignore_reaction_sent_by_bot_user(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event_id = "some alert event id"
fake_reaction_event = Mock(spec=nio.UnknownEvent)
fake_reaction_event.type = "m.reaction"
fake_reaction_event.event_id = "some event id"
fake_reaction_event.sender = self.fake_client.user
fake_reaction_event.source = {
"content": {
"m.relates_to": {
"event_id": fake_alert_event_id,
"key": "unknown",
"rel_type": "m.annotation",
}
}
}
# Pretend that we received a text message event
await self.callbacks.unknown(self.fake_room, fake_reaction_event)
await self.callbacks._reaction(
self.fake_room, fake_reaction_event, fake_alert_event_id
)
# Check that we attempted to execute the command
fake_command.assert_not_called()
self.fake_client.room_get_event.assert_not_called()
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_ignore_reaction_in_unauthorized_room(
self, fake_command: Mock
) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
self.fake_room.room_id = "!unauthorizedroom@example.com"
fake_alert_event_id = "some alert event id"
fake_reaction_event = Mock(spec=nio.UnknownEvent)
fake_reaction_event.type = "m.reaction"
fake_reaction_event.event_id = "some event id"
fake_reaction_event.sender = "@some_other_fake_user:example.com"
fake_reaction_event.source = {
"content": {
"m.relates_to": {
"event_id": fake_alert_event_id,
"key": "unknown",
"rel_type": "m.annotation",
}
}
}
# Pretend that we received a text message event
await self.callbacks.unknown(self.fake_room, fake_reaction_event)
await self.callbacks._reaction(
self.fake_room, fake_reaction_event, fake_alert_event_id
)
# Check that we attempted to execute the command
fake_command.assert_not_called()
self.fake_client.room_get_event.assert_not_called()
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_redaction_in_cache(self, fake_command: Mock) -> None: async def test_redaction_in_cache(self, fake_command: Mock) -> None:
"""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_alert_event_id = "some alert event id"
fake_redaction_event = Mock(spec=nio.RedactionEvent) fake_redaction_event = Mock(spec=nio.RedactionEvent)
fake_redaction_event.redacts = "some other event id" fake_redaction_event.redacts = "some other event id"
fake_redaction_event.event_id = "some event id" fake_redaction_event.event_id = "some event id"
fake_redaction_event.sender = "@some_other_fake_user:example.com" fake_redaction_event.sender = "@some_other_fake_user:example.com"
self.fake_cache.__getitem__.return_value = "some alert event id" fake_cache_dict = {fake_redaction_event.redacts: fake_alert_event_id}
self.fake_cache.__contains__.return_value = True self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
# Pretend that we received a text message event # Pretend that we received a text message event
await self.callbacks.redaction(self.fake_room, fake_redaction_event) await self.callbacks.redaction(self.fake_room, fake_redaction_event)
@ -401,18 +493,17 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack",
self.fake_room, self.fake_room,
fake_redaction_event.sender, fake_redaction_event.sender,
fake_redaction_event.redacts, fake_redaction_event.redacts,
"some alert event id", fake_alert_event_id,
) )
fake_command.return_value.process.assert_called_once() fake_command.return_value.process.assert_called_once()
self.fake_cache.__getitem__.assert_called_once_with( self.fake_cache.__getitem__.assert_called_once_with(
fake_redaction_event.redacts fake_redaction_event.redacts
) )
@patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True) @patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_redaction_not_in_cache(self, fake_command: Mock) -> None: async def test_redaction_not_in_cache(self, fake_command: Mock) -> None:
"""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
@ -421,13 +512,55 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_redaction_event.event_id = "some event id" fake_redaction_event.event_id = "some event id"
fake_redaction_event.sender = "@some_other_fake_user:example.com" fake_redaction_event.sender = "@some_other_fake_user:example.com"
self.fake_cache.__contains__.return_value = False fake_cache_dict: Dict = {}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
# Pretend that we received a text message event # Pretend that we received a text message event
await self.callbacks.redaction(self.fake_room, fake_redaction_event) await self.callbacks.redaction(self.fake_room, fake_redaction_event)
# 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.__getitem__.assert_called_once_with(
fake_redaction_event.redacts
)
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_ignore_redaction_sent_by_bot_user(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_redaction_event = Mock(spec=nio.RedactionEvent)
fake_redaction_event.sender = self.fake_client.user
fake_cache_dict: Dict = {}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
# Pretend that we received a text message event
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
# Check that we attempted to execute the command
fake_command.assert_not_called()
self.fake_cache.__getitem__.assert_not_called()
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
async def test_ignore_redaction_in_unauthorized_room(
self, fake_command: Mock
) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
self.fake_room.room_id = "!unauthorizedroom@example.com"
fake_redaction_event = Mock(spec=nio.RedactionEvent)
fake_redaction_event.sender = "@some_other_fake_user:example.com"
fake_cache_dict: Dict = {}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
# Pretend that we received a text message event
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
# Check that we attempted to execute the command
fake_command.assert_not_called()
self.fake_cache.__getitem__.assert_not_called()
@patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True) @patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True)
async def test_unknown(self, fake_command_create: Mock) -> None: async def test_unknown(self, fake_command_create: Mock) -> None:

View file

@ -1,5 +1,5 @@
import unittest import unittest
from typing import List from typing import Dict, Optional
from unittest.mock import MagicMock, Mock, call, patch from unittest.mock import MagicMock, Mock, call, patch
import nio import nio
@ -20,13 +20,22 @@ from matrix_alertbot.errors import (
AlertNotFoundError, AlertNotFoundError,
SilenceNotFoundError, SilenceNotFoundError,
) )
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
from tests.utils import make_awaitable from tests.utils import make_awaitable
def cache_get_item(key: str) -> str:
return {
"some alert event id": "fingerprint1",
"fingerprint1": "silence1",
}[key]
async def create_silence( async def create_silence(
fingerprint: str, seconds: int, user: str, matchers: List[AlertMatcher] fingerprint: str,
user: str,
seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
if fingerprint == "fingerprint1": if fingerprint == "fingerprint1":
return "silence1" return "silence1"
@ -36,7 +45,10 @@ async def create_silence(
async def create_silence_raise_alertmanager_error( async def create_silence_raise_alertmanager_error(
fingerprint: str, seconds: int, user: str, matchers: List[AlertMatcher] fingerprint: str,
user: str,
seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
if fingerprint == "fingerprint1": if fingerprint == "fingerprint1":
raise AlertmanagerError raise AlertmanagerError
@ -44,27 +56,24 @@ 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, seconds: int, user: str, matchers: List[AlertMatcher] fingerprint: str,
user: str,
seconds: Optional[int] = None,
silence_id: Optional[str] = None,
) -> str: ) -> str:
if fingerprint == "fingerprint1": if fingerprint == "fingerprint1":
raise AlertNotFoundError raise AlertNotFoundError
return "silence1" return "silence1"
async def delete_silence_raise_alertmanager_error( async def delete_silence_raise_alertmanager_error(silence_id: str) -> None:
fingerprint: str, matchers: List[AlertMatcher] if silence_id == "silence1":
) -> List[str]:
if fingerprint == "fingerprint1":
raise AlertmanagerError raise AlertmanagerError
return ["silence1"]
async def delete_silence_raise_silence_not_found_error( async def delete_silence_raise_silence_not_found_error(silence_id: str) -> None:
fingerprint: str, matchers: List[AlertMatcher] if silence_id == "silence1":
) -> List[str]:
if fingerprint == "fingerprint1":
raise SilenceNotFoundError raise SilenceNotFoundError
return ["silence1"]
class CommandTestCase(unittest.IsolatedAsyncioTestCase): class CommandTestCase(unittest.IsolatedAsyncioTestCase):
@ -75,15 +84,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
# 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_client.room_send.return_value = make_awaitable(None)
self.fake_fingerprints = ["fingerprint1", "fingerprint2"]
self.fake_silences = ["silence1", "silence2"]
self.fake_cache = MagicMock(spec=Cache) self.fake_cache = MagicMock(spec=Cache)
self.fake_cache.__getitem__.return_value = self.fake_fingerprints 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 = Mock(spec=AlertmanagerClient)
self.fake_alertmanager.delete_silences.return_value = self.fake_silences
self.fake_alertmanager.create_silence.side_effect = create_silence self.fake_alertmanager.create_silence.side_effect = create_silence
# Create a fake room to play with # Create a fake room to play with
@ -122,6 +127,27 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
# Check that we attempted to process the command # Check that we attempted to process the command
fake_ack.assert_called_once() fake_ack.assert_called_once()
@patch.object(matrix_alertbot.command.AckAlertCommand, "process")
async def test_process_ack_with_duration_command(self, fake_ack: Mock) -> None:
"""Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it
command = CommandFactory.create(
"ack 1w 3d",
self.fake_client,
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
self.fake_room,
self.fake_sender,
self.fake_event_id,
self.fake_alert_event_id,
)
await command.process()
# Check that we attempted to process the command
fake_ack.assert_called_once()
@patch.object(matrix_alertbot.command.UnackAlertCommand, "process") @patch.object(matrix_alertbot.command.UnackAlertCommand, "process")
async def test_process_unack_command(self, fake_unack: Mock) -> None: async def test_process_unack_command(self, fake_unack: Mock) -> None:
"""Tests the callback for InviteMemberEvents""" """Tests the callback for InviteMemberEvents"""
@ -185,18 +211,21 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
fake_unknown.assert_called_once() fake_unknown.assert_called_once()
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_without_duration_nor_matchers( async def test_ack_without_duration(self, fake_send_text_to_room: Mock) -> None:
self, fake_send_text_to_room: Mock
) -> None:
"""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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
}
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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -205,162 +234,64 @@ 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_has_calls( self.fake_alertmanager.create_silence.assert_called_once_with(
[ "fingerprint1", self.fake_sender, None, None
call(fingerprint, 86400, self.fake_sender, [])
for fingerprint in self.fake_fingerprints
]
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_client,
self.fake_room.room_id, self.fake_room.room_id,
"Created 2 silences with a duration of 1d.", "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.set.assert_called_once_with( self.fake_cache.get.assert_called_once_with("fingerprint1")
"".join(self.fake_fingerprints) + "86400", self.fake_cache.set.assert_has_calls(
tuple(self.fake_silences), [
expire=86400, call("some event id", "fingerprint1", expire=None),
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_ack_without_duration_and_with_matchers( async def test_ack_with_duration(self, fake_send_text_to_room: Mock) -> None:
self, fake_send_text_to_room: Mock
) -> None:
"""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
matchers = [ fake_cache_dict = {
AlertMatcher(label="alertname", value="alert1"), self.fake_alert_event_id: "fingerprint1",
AlertRegexMatcher(label="severity", regex="critical"), }
]
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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack alertname=alert1 severity=~critical",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
self.fake_alert_event_id, self.fake_alert_event_id,
("1w", "3d"),
) )
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_has_calls( self.fake_alertmanager.create_silence.assert_called_once_with(
[ "fingerprint1", self.fake_sender, 864000, None
call(
fingerprint,
86400,
self.fake_sender,
matchers,
)
for fingerprint in self.fake_fingerprints
]
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_client,
self.fake_room.room_id, self.fake_room.room_id,
"Created 2 silences with a duration of 1d.", "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.set.assert_called_once_with( self.fake_cache.get.assert_called_once_with("fingerprint1")
"".join(self.fake_fingerprints) self.fake_cache.set.assert_has_calls(
+ "86400"
+ "".join(str(matcher) for matcher in matchers),
tuple(self.fake_silences),
expire=86400,
)
@patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_with_duration_and_without_matchers(
self, fake_send_text_to_room: Mock
) -> None:
"""Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it
command = AckAlertCommand(
self.fake_client,
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"ack 1w 3d",
self.fake_room,
self.fake_sender,
self.fake_event_id,
self.fake_alert_event_id,
)
await command.process()
# Check that we attempted to create silences
self.fake_alertmanager.create_silence.assert_has_calls(
[ [
call(fingerprint, 864000, self.fake_sender, []) call("some event id", "fingerprint1", expire=864000),
for fingerprint in self.fake_fingerprints call("fingerprint1", "silence1", expire=864000),
] ]
) )
fake_send_text_to_room.assert_called_once_with(
self.fake_client,
self.fake_room.room_id,
"Created 2 silences with a duration of 1w 3d.",
)
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
self.fake_cache.set.assert_called_once_with(
"".join(self.fake_fingerprints) + "864000",
tuple(self.fake_silences),
expire=864000,
)
@patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_with_duration_and_matchers(
self, fake_send_text_to_room: Mock
) -> None:
"""Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it
matchers = [
AlertMatcher(label="alertname", value="alert1"),
AlertMatcher(label="severity", value="critical"),
]
command = AckAlertCommand(
self.fake_client,
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"ack 1w 3d alertname=alert1 severity=critical",
self.fake_room,
self.fake_sender,
self.fake_event_id,
self.fake_alert_event_id,
)
await command.process()
# Check that we attempted to create silences
self.fake_alertmanager.create_silence.assert_has_calls(
[
call(
fingerprint,
864000,
self.fake_sender,
matchers,
)
for fingerprint in self.fake_fingerprints
]
)
fake_send_text_to_room.assert_called_once_with(
self.fake_client,
self.fake_room.room_id,
"Created 2 silences with a duration of 1w 3d.",
)
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
self.fake_cache.set.assert_called_once_with(
"".join(self.fake_fingerprints)
+ "864000"
+ "".join(str(matcher) for matcher in matchers),
tuple(self.fake_silences),
expire=864000,
)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_raise_alertmanager_error( async def test_ack_raise_alertmanager_error(
@ -368,13 +299,18 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
) -> None: ) -> None:
"""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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
}
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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -387,23 +323,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_has_calls( self.fake_alertmanager.create_silence.assert_called_once_with(
[ "fingerprint1", self.fake_sender, None, None
call(fingerprint, 86400, self.fake_sender, [])
for fingerprint in self.fake_fingerprints
]
) )
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_client,
self.fake_room.room_id, self.fake_room.room_id,
"Created 1 silences with a duration of 1d.", "Something went wrong with Alertmanager, therefore I couldn't create silence for alert 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.set.assert_called_once_with( self.fake_cache.get.assert_called_once_with("fingerprint1")
"".join(self.fake_fingerprints) + "86400", self.fake_cache.set.assert_not_called()
("silence1",),
expire=86400,
)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_raise_alert_not_found_error( async def test_ack_raise_alert_not_found_error(
@ -411,13 +341,18 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
) -> None: ) -> None:
"""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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
}
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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -430,32 +365,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_has_calls( self.fake_alertmanager.create_silence.assert_called_once_with(
[ "fingerprint1", self.fake_sender, None, None
call(fingerprint, 86400, self.fake_sender, [])
for fingerprint in self.fake_fingerprints
]
) )
fake_send_text_to_room.assert_has_calls( fake_send_text_to_room.assert_called_once_with(
[ self.fake_client,
call( self.fake_room.room_id,
self.fake_client, "Sorry, I couldn't find alert with fingerprint fingerprint1, therefore I couldn't create the silence.",
self.fake_room.room_id,
"Sorry, I couldn't find 1 alerts, therefore I couldn't create their silence.",
),
call(
self.fake_client,
self.fake_room.room_id,
"Created 1 silences with a duration of 1d.",
),
]
) )
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.set.assert_called_once_with( self.fake_cache.get.assert_called_once_with("fingerprint1")
"".join(self.fake_fingerprints) + "86400", self.fake_cache.set.assert_not_called()
("silence1",),
expire=86400,
)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_with_invalid_duration( async def test_ack_with_invalid_duration(
@ -463,17 +383,16 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
) -> None: ) -> None:
"""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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack invalid duration",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
self.fake_alert_event_id, self.fake_alert_event_id,
("invalid duration",),
) )
await command.process() await command.process()
@ -486,23 +405,25 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
"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.",
) )
self.fake_cache.__getitem__.assert_not_called() self.fake_cache.__getitem__.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") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_ack_with_event_not_found_in_cache( async def test_ack_with_alert_event_not_found_in_cache(
self, fake_send_text_to_room: Mock self, fake_send_text_to_room: Mock
) -> None: ) -> None:
"""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
fake_cache_dict: Dict = {}
self.fake_cache.__contains__.return_value = False 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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"ack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -514,52 +435,71 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
# 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.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_not_called() self.fake_cache.__getitem__.assert_called_once_with("some alert event id")
self.fake_cache.get.assert_not_called()
self.fake_cache.set.assert_not_called() 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")
async def test_unack_without_matchers(self, fake_send_text_to_room: Mock) -> None: async def test_ack_with_silence_in_cache(
self, fake_send_text_to_room: Mock
) -> None:
"""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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
"fingerprint1": "silence2",
}
command = UnackAlertCommand( 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_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
self.fake_alert_event_id, self.fake_alert_event_id,
) )
await command.process() await command.process()
# Check that we attempted to create silences # Check that we attempted to create silences
self.fake_alertmanager.delete_silences.assert_has_calls( self.fake_alertmanager.create_silence.assert_called_once_with(
[call(fingerprint, []) for fingerprint in self.fake_fingerprints] "fingerprint1", self.fake_sender, None, "silence2"
) )
fake_send_text_to_room.assert_called_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_room.room_id, "Removed 4 silences." 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=None),
call("fingerprint1", "silence1", expire=None),
]
) )
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_unack_with_matchers(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"""
# 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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
"fingerprint1": "silence1",
}
matchers = [ self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
AlertMatcher(label="alertname", value="alert1"),
AlertRegexMatcher(label="severity", regex="critical"),
]
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack alertname=alert1 severity=~critical",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -568,13 +508,15 @@ 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_silences.assert_has_calls( self.fake_alertmanager.delete_silence.assert_called_once_with("silence1")
[call(fingerprint, matchers) for fingerprint in self.fake_fingerprints] fake_send_text_to_room.assert_called_once_with(
self.fake_client,
self.fake_room.room_id,
"Removed silence with ID silence1.",
) )
fake_send_text_to_room.assert_called_with( self.fake_cache.__getitem__.assert_has_calls(
self.fake_client, self.fake_room.room_id, "Removed 4 silences." [call(self.fake_alert_event_id), call("fingerprint1")]
) )
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_unack_silence_raise_alertmanager_error( async def test_unack_silence_raise_alertmanager_error(
@ -582,32 +524,39 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
) -> None: ) -> None:
"""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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
"fingerprint1": "silence1",
}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
self.fake_alert_event_id, self.fake_alert_event_id,
) )
self.fake_alertmanager.delete_silences.side_effect = ( self.fake_alertmanager.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_silences.assert_has_calls( self.fake_alertmanager.delete_silence.assert_called_once_with("silence1")
[call(fingerprint, []) for fingerprint in self.fake_fingerprints] fake_send_text_to_room.assert_called_once_with(
self.fake_client,
self.fake_room.room_id,
"Something went wrong with Alertmanager, therefore I couldn't delete silence for alert fingerprint fingerprint1.",
) )
fake_send_text_to_room.assert_called_with( self.fake_cache.__getitem__.assert_has_calls(
self.fake_client, self.fake_room.room_id, "Removed 1 silences." [call(self.fake_alert_event_id), call("fingerprint1")]
) )
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_unack_raise_silence_not_found_error( async def test_unack_raise_silence_not_found_error(
@ -615,43 +564,39 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
) -> None: ) -> None:
"""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
fake_cache_dict = {
self.fake_alert_event_id: "fingerprint1",
"fingerprint1": "silence1",
}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
self.fake_alert_event_id, self.fake_alert_event_id,
) )
self.fake_alertmanager.delete_silences.side_effect = ( self.fake_alertmanager.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_silences.assert_has_calls( self.fake_alertmanager.delete_silence.assert_called_once_with("silence1")
[call(fingerprint, []) for fingerprint in self.fake_fingerprints] fake_send_text_to_room.assert_called_once_with(
self.fake_client,
self.fake_room.room_id,
"Sorry, I couldn't find alert with fingerprint fingerprint1, therefore I couldn't remove its silence.",
) )
fake_send_text_to_room.assert_has_calls( self.fake_cache.__getitem__.assert_has_calls(
[ [call(self.fake_alert_event_id), call("fingerprint1")]
call(
self.fake_client,
self.fake_room.room_id,
"Sorry, I couldn't find 1 alerts, therefore I couldn't remove their silences.",
),
call(
self.fake_client,
self.fake_room.room_id,
"Removed 1 silences.",
),
]
) )
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_unack_with_event_not_found_in_cache( async def test_unack_with_event_not_found_in_cache(
@ -659,15 +604,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
) -> None: ) -> None:
"""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
fake_cache_dict: Dict = {}
self.fake_cache.__contains__.return_value = False self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand( command = UnackAlertCommand(
self.fake_client, self.fake_client,
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"unack",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -677,9 +622,39 @@ 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.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_not_called() self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
@patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_unack_with_silence_not_found_in_cache(
self, fake_send_text_to_room: Mock
) -> None:
"""Tests the callback for InviteMemberEvents"""
# Tests that the bot attempts to join a room after being invited to it
fake_cache_dict = {self.fake_alert_event_id: "fingerprint1"}
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
command = UnackAlertCommand(
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.delete_silence.assert_not_called()
fake_send_text_to_room.assert_not_called()
self.fake_cache.__getitem__.assert_has_calls(
[call(self.fake_alert_event_id), call("fingerprint1")]
)
@patch.object(matrix_alertbot.command, "send_text_to_room") @patch.object(matrix_alertbot.command, "send_text_to_room")
async def test_help_without_topic(self, fake_send_text_to_room: Mock) -> None: async def test_help_without_topic(self, fake_send_text_to_room: Mock) -> None:
@ -691,7 +666,6 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"help",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -714,10 +688,10 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"help rules",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
("rules",),
) )
await command.process() await command.process()
@ -737,10 +711,10 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"help commands",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
("commands",),
) )
await command.process() await command.process()
@ -760,10 +734,10 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"help unknown",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
("unknown",),
) )
await command.process() await command.process()
@ -783,7 +757,6 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache, self.fake_cache,
self.fake_alertmanager, self.fake_alertmanager,
self.fake_config, self.fake_config,
"",
self.fake_room, self.fake_room,
self.fake_sender, self.fake_sender,
self.fake_event_id, self.fake_event_id,
@ -795,7 +768,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_called_once_with(
self.fake_client, self.fake_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

@ -1,6 +1,6 @@
import unittest import unittest
from typing import Dict from typing import Dict
from unittest.mock import Mock, patch from unittest.mock import Mock, call, patch
import aiohttp.test_utils import aiohttp.test_utils
import nio import nio
@ -62,26 +62,35 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
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_alert(self, fake_send_text_to_room: Mock) -> None: async def test_post_alerts(self, fake_send_text_to_room: Mock) -> None:
data = self.fake_alerts data = self.fake_alerts
async with self.client.request("POST", "/alerts", json=data) as response: async with self.client.request("POST", "/alerts", json=data) as response:
self.assertEqual(200, response.status) self.assertEqual(200, response.status)
fake_send_text_to_room.assert_called_once_with( fake_send_text_to_room.assert_has_calls(
self.fake_client, [
self.fake_config.room_id, call(
"[🔥 CRITICAL] alert1: some description1\n" self.fake_client,
"[🥦 RESOLVED] alert2: some description2", self.fake_config.room_id,
"<font color='#dc3545'><b>[🔥 CRITICAL]</b></font> " "[🔥 CRITICAL] alert1: some description1",
"<a href='http://example.com/alert1'>alert1</a> (job1)<br/>" "<font color='#dc3545'><b>[🔥 CRITICAL]</b></font> "
"some description1<br/>\n" "<a href='http://example.com/alert1'>alert1</a> (job1)<br/>"
"<font color='#33cc33'><b>[🥦 RESOLVED]</b></font> " "some description1",
"<a href='http://example.com/alert2'>alert2</a> (job2)<br/>" notice=False,
"some description2", ),
notice=False, call(
self.fake_client,
self.fake_config.room_id,
"[🥦 RESOLVED] alert2: some description2",
"<font color='#33cc33'><b>[🥦 RESOLVED]</b></font> "
"<a href='http://example.com/alert2'>alert2</a> (job2)<br/>"
"some description2",
notice=False,
),
]
) )
@patch.object(matrix_alertbot.webhook, "send_text_to_room") @patch.object(matrix_alertbot.webhook, "send_text_to_room")
async def test_post_alert_with_empty_data( async def test_post_alerts_with_empty_data(
self, fake_send_text_to_room: Mock self, fake_send_text_to_room: Mock
) -> None: ) -> None:
async with self.client.request("POST", "/alerts", json={}) as response: async with self.client.request("POST", "/alerts", json={}) as response:
@ -91,9 +100,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.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_alert_with_empty_alerts( async def test_post_empty_alerts(self, fake_send_text_to_room: Mock) -> None:
self, fake_send_text_to_room: Mock
) -> None:
data: Dict = {"alerts": []} data: Dict = {"alerts": []}
async with self.client.request("POST", "/alerts", json=data) as response: async with self.client.request("POST", "/alerts", json=data) as response:
self.assertEqual(400, response.status) self.assertEqual(400, response.status)
@ -102,9 +109,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.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_alert_with_invalid_alerts( async def test_post_invalid_alerts(self, fake_send_text_to_room: Mock) -> None:
self, fake_send_text_to_room: Mock
) -> None:
data = {"alerts": "invalid"} data = {"alerts": "invalid"}
async with self.client.request("POST", "/alerts", json=data) as response: async with self.client.request("POST", "/alerts", json=data) as response:
self.assertEqual(400, response.status) self.assertEqual(400, response.status)
@ -113,7 +118,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
fake_send_text_to_room.assert_not_called() fake_send_text_to_room.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_alert_with_empty_items( async def test_post_alerts_with_empty_items(
self, fake_send_text_to_room: Mock self, fake_send_text_to_room: Mock
) -> None: ) -> None:
data: Dict = {"alerts": [{}]} data: Dict = {"alerts": [{}]}
@ -128,7 +133,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
"send_text_to_room", "send_text_to_room",
side_effect=send_text_to_room_raise_error, side_effect=send_text_to_room_raise_error,
) )
async def test_post_alert_with_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:
data = self.fake_alerts data = self.fake_alerts
@ -136,7 +141,8 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
self.assertEqual(500, response.status) self.assertEqual(500, response.status)
error_msg = await response.text() error_msg = await response.text()
self.assertEqual( self.assertEqual(
"An error occured when sending alerts to Matrix room.", error_msg "An error occured when sending alert with fingerprint 'fingerprint1' to Matrix room.",
error_msg,
) )
fake_send_text_to_room.assert_called_once() fake_send_text_to_room.assert_called_once()