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__(
self,
id: str,
fingerprint: str,
url: str,
labels: Dict[str, str],
annotations: Dict[str, str],
firing: bool = True,
):
self.id = id
self.fingerprint = fingerprint
self.url = url
self.firing = firing
@ -33,7 +33,7 @@ class Alert:
@staticmethod
def from_dict(data: Dict) -> Alert:
return Alert(
id=data["fingerprint"],
fingerprint=data["fingerprint"],
url=data["generatorURL"],
firing=data["status"] == "firing",
labels=data["labels"],

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import logging
from typing import List, Optional, Tuple
from typing import Optional, Tuple
import pytimeparse2
from diskcache import Cache
@ -13,7 +13,6 @@ from matrix_alertbot.errors import (
AlertNotFoundError,
SilenceNotFoundError,
)
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
logger = logging.getLogger(__name__)
@ -25,10 +24,10 @@ class BaseCommand:
cache: Cache,
alertmanager: AlertmanagerClient,
config: Config,
cmd: str,
room: MatrixRoom,
sender: str,
event_id: str,
args: Tuple[str, ...] = None,
) -> None:
"""A command made by a user.
@ -53,12 +52,15 @@ class BaseCommand:
self.cache = cache
self.alertmanager = alertmanager
self.config = config
self.cmd = cmd
self.args = cmd.split()[1:]
self.room = room
self.sender = sender
self.event_id = event_id
if args is not None:
self.args = args
else:
self.args = ()
async def process(self) -> None:
raise NotImplementedError
@ -70,166 +72,158 @@ class BaseAlertCommand(BaseCommand):
cache: Cache,
alertmanager: AlertmanagerClient,
config: Config,
cmd: str,
room: MatrixRoom,
sender: str,
event_id: str,
alert_event_id: str,
reacted_to_event_id: str,
args: Tuple[str, ...] = None,
) -> None:
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):
async def process(self) -> None:
"""Acknowledge an alert and silence it for a certain duration in Alertmanager"""
matchers: List[AlertMatcher] = []
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)
durations = self.args
if len(durations) > 0:
duration = " ".join(durations)
else:
duration = "1d"
logger.debug(f"Receiving a command to create a silence for {duration}.")
logger.debug(
f"Receiving a command to create a silence for a duration of {duration}"
)
duration_seconds = pytimeparse2.parse(duration)
if duration_seconds is None:
logger.error(f"Unable to create silence: Invalid duration '{duration}'")
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,
duration_seconds = pytimeparse2.parse(duration)
if duration_seconds is None:
logger.error(f"Unable to create silence: Invalid duration '{duration}'")
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.",
)
created_silences.append(silence_id)
except AlertNotFoundError as e:
logger.warning(f"Unable to create silence: {e}")
count_alert_not_found += 1
except AlertmanagerError as e:
logger.exception(f"Unable to create silence: {e}", exc_info=e)
return
else:
duration_seconds = None
logger.debug(
"Receiving a command to create a silence for an indefinite period"
)
matchers_id = "".join(sorted(str(matcher) for matcher in matchers))
ack_id = "".join(alert_fingerprints) + str(duration_seconds) + matchers_id
self.cache.set(ack_id, tuple(created_silences), expire=duration_seconds)
logger.debug(
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
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(
self.client,
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.",
)
if len(created_silences) > 0:
return
except AlertmanagerError as e:
logger.exception(f"Unable to create silence: {e}", exc_info=e)
await send_text_to_room(
self.client,
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):
async def process(self) -> None:
"""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(
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:
logger.error(
f"Cannot find fingerprints for event {self.alert_event_id} in cache"
try:
await self.alertmanager.delete_silence(silence_id)
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
alert_fingerprints: Tuple[str] = self.cache[self.alert_event_id]
logger.debug(f"Found {len(alert_fingerprints)} in cache")
count_alert_not_found = 0
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.",
)
await send_text_to_room(
self.client,
self.room.room_id,
f"Removed silence with ID {silence_id}.",
)
class HelpCommand(BaseCommand):
@ -262,7 +256,7 @@ class UnknownCommand(BaseCommand):
await send_text_to_room(
self.client,
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,
reacted_to_event_id: Optional[str] = None,
) -> BaseCommand:
args = tuple(cmd.split()[1:])
if cmd.startswith("ack"):
if reacted_to_event_id is None:
raise TypeError("Alert command must be in reply to an alert event.")
@ -288,11 +284,11 @@ class CommandFactory:
cache,
alertmanager,
config,
cmd,
room,
sender,
event_id,
reacted_to_event_id,
args,
)
elif cmd.startswith("unack") or cmd.startswith("nack"):
if reacted_to_event_id is None:
@ -303,17 +299,17 @@ class CommandFactory:
cache,
alertmanager,
config,
cmd,
room,
sender,
event_id,
reacted_to_event_id,
args,
)
elif cmd.startswith("help"):
return HelpCommand(
client, cache, alertmanager, config, cmd, room, sender, event_id
client, cache, alertmanager, config, room, sender, event_id, args
)
else:
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
class AlertMismatchError(AlertmanagerError):
"""An error encountered when alert's labels don't match."""
class SilenceNotFoundError(AlertmanagerError):
"""An error encountered when a silence cannot be found in Alertmanager."""
pass
class SilenceNotFoundError(AlertmanagerError):
"""An error encountered when a silence cannot be found in Alertmanager."""
class SilenceExpiredError(AlertmanagerError):
"""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

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

View file

@ -18,7 +18,7 @@ class AlertTestCase(unittest.TestCase):
self.alert_dict["status"] = "firing"
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.assertTrue(alert.firing)
self.assertEqual("critical", alert.status)

View file

@ -2,24 +2,25 @@ from __future__ import annotations
import json
import unittest
from datetime import datetime
from typing import Any, List
from unittest.mock import MagicMock, Mock, patch
from datetime import datetime, timedelta
from typing import Any
from unittest.mock import MagicMock, Mock
import aiohttp
import aiohttp.test_utils
import aiotools
from aiohttp import web, web_request
from diskcache import Cache
from freezegun import freeze_time
from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.errors import (
AlertmanagerServerError,
AlertMismatchError,
AlertNotFoundError,
InvalidDurationError,
SilenceExpiredError,
SilenceNotFoundError,
)
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
class FakeTimeDelta:
@ -36,10 +37,15 @@ class AbstractFakeAlertmanagerServer:
self.app.router.add_routes(
[
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.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)
@ -64,6 +70,9 @@ class AbstractFakeAlertmanagerServer:
async def get_alerts(self, request: web_request.Request) -> web.Response:
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:
raise NotImplementedError
@ -94,25 +103,56 @@ class FakeAlertmanagerServer(AbstractFakeAlertmanagerServer):
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(
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:
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")
class FakeAlertmanagerServerWithoutAlert(AbstractFakeAlertmanagerServer):
class FakeAlertmanagerServerWithoutAlert(FakeAlertmanagerServer):
async def get_alerts(self, request: web_request.Request) -> web.Response:
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:
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):
async def create_silence(self, request: web_request.Request) -> web.Response:
return web.Response(status=500)
@ -137,24 +177,25 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
)
async with aiotools.closing_async(alertmanager):
alerts = await alertmanager.get_alerts()
self.assertEqual(
[
{
"fingerprint": "fingerprint1",
"labels": {"alertname": "alert1"},
"status": {"state": "active"},
},
{
"fingerprint": "fingerprint2",
"labels": {"alertname": "alert2"},
"status": {
"state": "suppressed",
"silencedBy": ["silence1", "silence2"],
},
},
],
alerts,
)
self.assertEqual(
[
{
"fingerprint": "fingerprint1",
"labels": {"alertname": "alert1"},
"status": {"state": "active"},
},
{
"fingerprint": "fingerprint2",
"labels": {"alertname": "alert2"},
"status": {
"state": "suppressed",
"silencedBy": ["silence1", "silence2"],
},
},
],
alerts,
)
async def test_get_alerts_empty(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
@ -164,7 +205,8 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
)
async with aiotools.closing_async(alertmanager):
alerts = await alertmanager.get_alerts()
self.assertEqual([], alerts)
self.assertEqual([], alerts)
async def test_get_alerts_raise_alertmanager_error(self) -> None:
async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server:
@ -176,6 +218,44 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(AlertmanagerServerError):
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 with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port
@ -184,14 +264,15 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
)
async with aiotools.closing_async(alertmanager):
alert = await alertmanager.get_alert("fingerprint1")
self.assertEqual(
{
"fingerprint": "fingerprint1",
"labels": {"alertname": "alert1"},
"status": {"state": "active"},
},
alert,
)
self.assertEqual(
{
"fingerprint": "fingerprint1",
"labels": {"alertname": "alert1"},
"status": {"state": "active"},
},
alert,
)
async def test_get_alert_raise_alert_not_found(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
@ -213,120 +294,189 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
with self.assertRaises(AlertmanagerServerError):
await alertmanager.get_alert("fingerprint1")
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta)
async def test_create_silence_without_matchers(self, fake_timedelta: Mock) -> None:
async def test_get_silence_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):
silence = await alertmanager.create_silence(
"fingerprint1", 86400, "user", []
silence1 = await alertmanager.get_silence("silence1")
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)
fake_timedelta.assert_called_once_with(seconds=86400)
self.assertEqual("silence1", silence_id)
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)
async def test_create_silence_with_matchers(self, fake_timedelta: Mock) -> None:
matchers = [AlertMatcher(label="alertname", value="alert1")]
async with FakeAlertmanagerServer() as fake_alertmanager_server:
@freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_with_id(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 = await alertmanager.create_silence(
"fingerprint1",
86400,
"user",
matchers,
silence_id = await alertmanager.create_silence(
"fingerprint1", "user", 86400, "silence2"
)
silence = await alertmanager.get_silence("silence2")
self.assertEqual("silence1", silence)
fake_timedelta.assert_called_once_with(seconds=86400)
self.assertEqual("silence2", silence_id)
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)
async def test_create_silence_with_regex_matchers(
self, fake_timedelta: Mock
) -> None:
matchers: List[AlertMatcher] = [
AlertRegexMatcher(label="alertname", regex=r"alert\d+")
]
async with FakeAlertmanagerServer() as fake_alertmanager_server:
@freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_with_indefinite_duration(self) -> None:
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache
)
async with aiotools.closing_async(alertmanager):
silence = await alertmanager.create_silence(
"fingerprint1",
86400,
"user",
matchers,
silence_id = await alertmanager.create_silence("fingerprint1", "user")
silence = await alertmanager.get_silence("silence1")
self.assertEqual("silence1", silence_id)
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)
fake_timedelta.assert_called_once_with(seconds=86400)
self.assertEqual("silence1", silence_id)
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:
matchers = [
AlertMatcher(label="alertname", value="alert1"),
AlertMatcher(label="severity", value="critical"),
]
async with FakeAlertmanagerServer() as fake_alertmanager_server:
@freeze_time(datetime.utcfromtimestamp(0))
async def test_create_silence_raise_duration_error(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(AlertMismatchError):
await alertmanager.create_silence(
"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,
)
with self.assertRaises(InvalidDurationError):
await alertmanager.create_silence("fingerprint1", "user", -1)
async def test_create_silence_raise_alert_not_found(self) -> None:
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
@ -336,7 +486,7 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
)
async with aiotools.closing_async(alertmanager):
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 with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server:
@ -348,111 +498,40 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
await alertmanager.get_alert("fingerprint1")
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:
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", [])
await alertmanager.delete_silence("silence1")
silences = await alertmanager.get_silences()
self.assertEqual(["silence1", "silence2"], silences)
async def test_delete_silences_with_matchers(self) -> None:
matchers = [AlertMatcher(label="alertname", value="alert2")]
self.assertEqual([{"id": "silence2", "state": "expired"}], silences)
async def test_delete_silence_raise_silence_expired(self) -> None:
async with FakeAlertmanagerServer() as fake_alertmanager_server:
port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient(
f"http://localhost:{port}", self.fake_cache
)
async with aiotools.closing_async(alertmanager):
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:
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 def test_delete_silence_raise_alertmanager_error(self) -> None:
async with FakeAlertmanagerServerWithErrorDeleteSilence() as fake_alertmanager_server:
port = fake_alertmanager_server.port
alertmanager = AlertmanagerClient(
@ -462,7 +541,7 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
await alertmanager.get_alert("fingerprint1")
with self.assertRaises(AlertmanagerServerError):
await alertmanager.delete_silences("fingerprint2", [])
await alertmanager.delete_silence("silence1")
async def test_find_alert_happy(self) -> None:
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:
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
with self.assertRaises(AlertNotFoundError):
alertmanager._find_alert("fingerprint1", [])
with self.assertRaises(AlertNotFoundError):
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__":
unittest.main()

View file

@ -1,11 +1,12 @@
import unittest
from typing import Dict
from unittest.mock import MagicMock, Mock, patch
import nio
from diskcache import Cache
import matrix_alertbot.command
import matrix_alertbot.callback
import matrix_alertbot.command
from matrix_alertbot.alertmanager import AlertmanagerClient
from matrix_alertbot.callback import Callbacks
from matrix_alertbot.command import BaseCommand
@ -86,10 +87,10 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"help",
self.fake_room,
fake_message_event.sender,
fake_message_event.event_id,
(),
)
fake_command.return_value.process.assert_called_once()
@ -117,13 +118,47 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"help",
self.fake_room,
fake_message_event.sender,
fake_message_event.event_id,
(),
)
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)
async def test_message_ack_not_in_reply_with_prefix(
self, fake_command: Mock
@ -165,15 +200,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"ack",
self.fake_room,
fake_message_event.sender,
fake_message_event.event_id,
"some alert event id",
(),
)
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(
self, fake_command: Mock
) -> None:
@ -214,15 +249,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"unack",
self.fake_room,
fake_message_event.sender,
fake_message_event.event_id,
"some alert event id",
(),
)
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:
"""Tests the callback for RoomMessageText with the command prefix"""
# 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_alertmanager,
self.fake_config,
"ack 12h",
self.fake_room,
fake_reaction_event.sender,
fake_reaction_event.event_id,
"some alert event id",
)
fake_command.return_value.process.assert_called_once()
self.fake_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_room.room_id, fake_alert_event.event_id
)
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
async def test_reaction_to_unknown_event(self, fake_command: Mock) -> None:
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_inexistent_event(self, fake_command: Mock) -> None:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event = Mock(spec=nio.RoomMessageText)
fake_alert_event.event_id = "some alert event id"
fake_alert_event.sender = self.fake_config.user_id
fake_alert_event_id = "some alert event id"
fake_reaction_event = Mock(spec=nio.UnknownEvent)
fake_reaction_event.type = "m.reaction"
@ -290,7 +317,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_reaction_event.source = {
"content": {
"m.relates_to": {
"event_id": fake_alert_event.event_id,
"event_id": fake_alert_event_id,
"key": "🤫",
"rel_type": "m.annotation",
}
@ -309,11 +336,11 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_command.assert_not_called()
self.fake_cache.set.assert_not_called()
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)
async def test_reaction_to_event_with_incorrect_sender(
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
async def test_reaction_to_event_not_from_bot_user(
self, fake_command: Mock
) -> None:
"""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
)
@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:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event = Mock(spec=nio.RoomMessageText)
fake_alert_event.event_id = "some alert event id"
fake_alert_event_id = "some alert event id"
fake_reaction_event = Mock(spec=nio.UnknownEvent)
fake_reaction_event.type = "m.reaction"
@ -366,7 +392,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_reaction_event.source = {
"content": {
"m.relates_to": {
"event_id": fake_alert_event.event_id,
"event_id": fake_alert_event_id,
"key": "unknown",
"rel_type": "m.annotation",
}
@ -380,17 +406,83 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
fake_command.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:
"""Tests the callback for RoomMessageText with the command prefix"""
# Tests that the bot process messages in the room that contain a command
fake_alert_event_id = "some alert event id"
fake_redaction_event = Mock(spec=nio.RedactionEvent)
fake_redaction_event.redacts = "some other event id"
fake_redaction_event.event_id = "some event id"
fake_redaction_event.sender = "@some_other_fake_user:example.com"
self.fake_cache.__getitem__.return_value = "some alert event id"
self.fake_cache.__contains__.return_value = True
fake_cache_dict = {fake_redaction_event.redacts: fake_alert_event_id}
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)
@ -401,18 +493,17 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
self.fake_cache,
self.fake_alertmanager,
self.fake_config,
"unack",
self.fake_room,
fake_redaction_event.sender,
fake_redaction_event.redacts,
"some alert event id",
fake_alert_event_id,
)
fake_command.return_value.process.assert_called_once()
self.fake_cache.__getitem__.assert_called_once_with(
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:
"""Tests the callback for RoomMessageText with the command prefix"""
# 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.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
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_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)
async def test_unknown(self, fake_command_create: Mock) -> None:

View file

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

View file

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