refactor to handle one alert per matrix event
This commit is contained in:
parent
f1691fc3a6
commit
5ed5a4aa08
12 changed files with 957 additions and 804 deletions
|
@ -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"],
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -24,9 +24,9 @@ async def get_health(request: web_request.Request) -> web.Response:
|
|||
|
||||
|
||||
@routes.post("/alerts")
|
||||
async def create_alert(request: web_request.Request) -> web.Response:
|
||||
async def create_alerts(request: web_request.Request) -> web.Response:
|
||||
data = await request.json()
|
||||
logger.info(f"Received alert: {data}")
|
||||
logger.info(f"Received alerts: {data}")
|
||||
client: AsyncClient = request.app["client"]
|
||||
config: Config = request.app["config"]
|
||||
cache: Cache = request.app["cache"]
|
||||
|
@ -40,34 +40,30 @@ async def create_alert(request: web_request.Request) -> web.Response:
|
|||
if len(data["alerts"]) == 0:
|
||||
return web.Response(status=400, body="Alerts cannot be empty.")
|
||||
|
||||
plaintext = ""
|
||||
html = ""
|
||||
for i, alert in enumerate(data["alerts"]):
|
||||
for alert in data["alerts"]:
|
||||
try:
|
||||
alert = Alert.from_dict(alert)
|
||||
except KeyError:
|
||||
return web.Response(status=400, body=f"Invalid alert: {alert}.")
|
||||
|
||||
if i != 0:
|
||||
plaintext += "\n"
|
||||
html += "<br/>\n"
|
||||
plaintext += alert.plaintext()
|
||||
html += alert.html()
|
||||
plaintext = alert.plaintext()
|
||||
html = alert.html()
|
||||
|
||||
try:
|
||||
event = await send_text_to_room(
|
||||
client, config.room_id, plaintext, html, notice=False
|
||||
)
|
||||
except (LocalProtocolError, ClientError) as e:
|
||||
logger.error(e)
|
||||
return web.Response(
|
||||
status=500, body="An error occured when sending alerts to Matrix room."
|
||||
)
|
||||
try:
|
||||
event = await send_text_to_room(
|
||||
client, config.room_id, plaintext, html, notice=False
|
||||
)
|
||||
except (LocalProtocolError, ClientError) as e:
|
||||
logger.error(
|
||||
f"Unable to send alert {alert.fingerprint} to Matrix room: {e}"
|
||||
)
|
||||
return web.Response(
|
||||
status=500,
|
||||
body=f"An error occured when sending alert with fingerprint '{alert.fingerprint}' to Matrix room.",
|
||||
)
|
||||
|
||||
cache.set(event.event_id, alert.fingerprint, expire=config.cache_expire_time)
|
||||
|
||||
fingerprints = tuple(sorted(alert["fingerprint"] for alert in data["alerts"]))
|
||||
cache.set(
|
||||
event.event_id, fingerprints, expire=config.cache_expire_time, tag="event"
|
||||
)
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import unittest
|
||||
from typing import Dict
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
import aiohttp.test_utils
|
||||
import nio
|
||||
|
@ -62,26 +62,35 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
|||
return webhook.app
|
||||
|
||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||
async def test_post_alert(self, fake_send_text_to_room: Mock) -> None:
|
||||
async def test_post_alerts(self, fake_send_text_to_room: Mock) -> None:
|
||||
data = self.fake_alerts
|
||||
async with self.client.request("POST", "/alerts", json=data) as response:
|
||||
self.assertEqual(200, response.status)
|
||||
fake_send_text_to_room.assert_called_once_with(
|
||||
self.fake_client,
|
||||
self.fake_config.room_id,
|
||||
"[🔥 CRITICAL] alert1: some description1\n"
|
||||
"[🥦 RESOLVED] alert2: some description2",
|
||||
"<font color='#dc3545'><b>[🔥 CRITICAL]</b></font> "
|
||||
"<a href='http://example.com/alert1'>alert1</a> (job1)<br/>"
|
||||
"some description1<br/>\n"
|
||||
"<font color='#33cc33'><b>[🥦 RESOLVED]</b></font> "
|
||||
"<a href='http://example.com/alert2'>alert2</a> (job2)<br/>"
|
||||
"some description2",
|
||||
notice=False,
|
||||
fake_send_text_to_room.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
self.fake_client,
|
||||
self.fake_config.room_id,
|
||||
"[🔥 CRITICAL] alert1: some description1",
|
||||
"<font color='#dc3545'><b>[🔥 CRITICAL]</b></font> "
|
||||
"<a href='http://example.com/alert1'>alert1</a> (job1)<br/>"
|
||||
"some description1",
|
||||
notice=False,
|
||||
),
|
||||
call(
|
||||
self.fake_client,
|
||||
self.fake_config.room_id,
|
||||
"[🥦 RESOLVED] alert2: some description2",
|
||||
"<font color='#33cc33'><b>[🥦 RESOLVED]</b></font> "
|
||||
"<a href='http://example.com/alert2'>alert2</a> (job2)<br/>"
|
||||
"some description2",
|
||||
notice=False,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||
async def test_post_alert_with_empty_data(
|
||||
async def test_post_alerts_with_empty_data(
|
||||
self, fake_send_text_to_room: Mock
|
||||
) -> None:
|
||||
async with self.client.request("POST", "/alerts", json={}) as response:
|
||||
|
@ -91,9 +100,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
|||
fake_send_text_to_room.assert_not_called()
|
||||
|
||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||
async def test_post_alert_with_empty_alerts(
|
||||
self, fake_send_text_to_room: Mock
|
||||
) -> None:
|
||||
async def test_post_empty_alerts(self, fake_send_text_to_room: Mock) -> None:
|
||||
data: Dict = {"alerts": []}
|
||||
async with self.client.request("POST", "/alerts", json=data) as response:
|
||||
self.assertEqual(400, response.status)
|
||||
|
@ -102,9 +109,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
|||
fake_send_text_to_room.assert_not_called()
|
||||
|
||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||
async def test_post_alert_with_invalid_alerts(
|
||||
self, fake_send_text_to_room: Mock
|
||||
) -> None:
|
||||
async def test_post_invalid_alerts(self, fake_send_text_to_room: Mock) -> None:
|
||||
data = {"alerts": "invalid"}
|
||||
async with self.client.request("POST", "/alerts", json=data) as response:
|
||||
self.assertEqual(400, response.status)
|
||||
|
@ -113,7 +118,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
|||
fake_send_text_to_room.assert_not_called()
|
||||
|
||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||
async def test_post_alert_with_empty_items(
|
||||
async def test_post_alerts_with_empty_items(
|
||||
self, fake_send_text_to_room: Mock
|
||||
) -> None:
|
||||
data: Dict = {"alerts": [{}]}
|
||||
|
@ -128,7 +133,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
|||
"send_text_to_room",
|
||||
side_effect=send_text_to_room_raise_error,
|
||||
)
|
||||
async def test_post_alert_with_send_error(
|
||||
async def test_post_alerts_raise_send_error(
|
||||
self, fake_send_text_to_room: Mock
|
||||
) -> None:
|
||||
data = self.fake_alerts
|
||||
|
@ -136,7 +141,8 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
|||
self.assertEqual(500, response.status)
|
||||
error_msg = await response.text()
|
||||
self.assertEqual(
|
||||
"An error occured when sending alerts to Matrix room.", error_msg
|
||||
"An error occured when sending alert with fingerprint 'fingerprint1' to Matrix room.",
|
||||
error_msg,
|
||||
)
|
||||
fake_send_text_to_room.assert_called_once()
|
||||
|
||||
|
|
Loading…
Reference in a new issue