diff --git a/matrix_alertbot/alert.py b/matrix_alertbot/alert.py
index 2b8748d..1374ae1 100644
--- a/matrix_alertbot/alert.py
+++ b/matrix_alertbot/alert.py
@@ -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"],
diff --git a/matrix_alertbot/alertmanager.py b/matrix_alertbot/alertmanager.py
index eb4cf2f..e51acd8 100644
--- a/matrix_alertbot/alertmanager.py
+++ b/matrix_alertbot/alertmanager.py
@@ -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}")
diff --git a/matrix_alertbot/callback.py b/matrix_alertbot/callback.py
index 72dace2..fe689b0 100644
--- a/matrix_alertbot/callback.py
+++ b/matrix_alertbot/callback.py
@@ -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:
diff --git a/matrix_alertbot/command.py b/matrix_alertbot/command.py
index b382928..33acf96 100644
--- a/matrix_alertbot/command.py
+++ b/matrix_alertbot/command.py
@@ -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
)
diff --git a/matrix_alertbot/errors.py b/matrix_alertbot/errors.py
index c7f0ed9..fc65fdb 100644
--- a/matrix_alertbot/errors.py
+++ b/matrix_alertbot/errors.py
@@ -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
diff --git a/matrix_alertbot/matcher.py b/matrix_alertbot/matcher.py
deleted file mode 100644
index e386c02..0000000
--- a/matrix_alertbot/matcher.py
+++ /dev/null
@@ -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
diff --git a/matrix_alertbot/webhook.py b/matrix_alertbot/webhook.py
index 8962047..bc2c99a 100644
--- a/matrix_alertbot/webhook.py
+++ b/matrix_alertbot/webhook.py
@@ -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 += "
\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)
diff --git a/tests/test_alert.py b/tests/test_alert.py
index 0c10adc..dd1476c 100644
--- a/tests/test_alert.py
+++ b/tests/test_alert.py
@@ -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)
diff --git a/tests/test_alertmanager.py b/tests/test_alertmanager.py
index bca89e1..5afa104 100644
--- a/tests/test_alertmanager.py
+++ b/tests/test_alertmanager.py
@@ -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()
diff --git a/tests/test_callback.py b/tests/test_callback.py
index e129c51..4311ef3 100644
--- a/tests/test_callback.py
+++ b/tests/test_callback.py
@@ -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:
diff --git a/tests/test_command.py b/tests/test_command.py
index 7e82b09..f7bc8fa 100644
--- a/tests/test_command.py
+++ b/tests/test_command.py
@@ -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.",
)
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index 01d34f7..fc17d3a 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -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",
- "[🔥 CRITICAL] "
- "alert1 (job1)
"
- "some description1
\n"
- "[🥦 RESOLVED] "
- "alert2 (job2)
"
- "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",
+ "[🔥 CRITICAL] "
+ "alert1 (job1)
"
+ "some description1",
+ notice=False,
+ ),
+ call(
+ self.fake_client,
+ self.fake_config.room_id,
+ "[🥦 RESOLVED] alert2: some description2",
+ "[🥦 RESOLVED] "
+ "alert2 (job2)
"
+ "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()