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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
id: str,
|
fingerprint: str,
|
||||||
url: str,
|
url: str,
|
||||||
labels: Dict[str, str],
|
labels: Dict[str, str],
|
||||||
annotations: Dict[str, str],
|
annotations: Dict[str, str],
|
||||||
firing: bool = True,
|
firing: bool = True,
|
||||||
):
|
):
|
||||||
self.id = id
|
self.fingerprint = fingerprint
|
||||||
self.url = url
|
self.url = url
|
||||||
self.firing = firing
|
self.firing = firing
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class Alert:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: Dict) -> Alert:
|
def from_dict(data: Dict) -> Alert:
|
||||||
return Alert(
|
return Alert(
|
||||||
id=data["fingerprint"],
|
fingerprint=data["fingerprint"],
|
||||||
url=data["generatorURL"],
|
url=data["generatorURL"],
|
||||||
firing=data["status"] == "firing",
|
firing=data["status"] == "firing",
|
||||||
labels=data["labels"],
|
labels=data["labels"],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
@ -10,11 +10,11 @@ from diskcache import Cache
|
||||||
|
|
||||||
from matrix_alertbot.errors import (
|
from matrix_alertbot.errors import (
|
||||||
AlertmanagerServerError,
|
AlertmanagerServerError,
|
||||||
AlertMismatchError,
|
|
||||||
AlertNotFoundError,
|
AlertNotFoundError,
|
||||||
|
InvalidDurationError,
|
||||||
|
SilenceExpiredError,
|
||||||
SilenceNotFoundError,
|
SilenceNotFoundError,
|
||||||
)
|
)
|
||||||
from matrix_alertbot.matcher import AlertMatcher
|
|
||||||
|
|
||||||
|
|
||||||
class AlertmanagerClient:
|
class AlertmanagerClient:
|
||||||
|
@ -40,28 +40,48 @@ class AlertmanagerClient:
|
||||||
alerts = await self.get_alerts()
|
alerts = await self.get_alerts()
|
||||||
return self._find_alert(fingerprint, alerts)
|
return self._find_alert(fingerprint, alerts)
|
||||||
|
|
||||||
|
async def get_silences(self) -> List[Dict]:
|
||||||
|
try:
|
||||||
|
async with self.session.get(f"{self.api_url}/silences") as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
except ClientError as e:
|
||||||
|
raise AlertmanagerServerError(
|
||||||
|
"Cannot fetch silences from Alertmanager"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
async def get_silence(self, silence_id: str) -> Dict:
|
||||||
|
silences = await self.get_silences()
|
||||||
|
return self._find_silence(silence_id, silences)
|
||||||
|
|
||||||
async def create_silence(
|
async def create_silence(
|
||||||
self,
|
self,
|
||||||
fingerprint: str,
|
fingerprint: str,
|
||||||
seconds: int,
|
|
||||||
user: str,
|
user: str,
|
||||||
matchers: List[AlertMatcher],
|
duration_seconds: Optional[int] = None,
|
||||||
|
silence_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
alert = await self.get_alert(fingerprint)
|
alert = await self.get_alert(fingerprint)
|
||||||
|
|
||||||
self._match_alert(alert, matchers)
|
|
||||||
|
|
||||||
silence_matchers = [
|
silence_matchers = [
|
||||||
{"name": label, "value": value, "isRegex": False, "isEqual": True}
|
{"name": label, "value": value, "isRegex": False, "isEqual": True}
|
||||||
for label, value in alert["labels"].items()
|
for label, value in alert["labels"].items()
|
||||||
]
|
]
|
||||||
|
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
|
if duration_seconds is None:
|
||||||
duration_delta = timedelta(seconds=seconds)
|
end_time = datetime.max
|
||||||
end_time = start_time + duration_delta
|
elif duration_seconds > 0:
|
||||||
|
try:
|
||||||
|
duration_delta = timedelta(seconds=duration_seconds)
|
||||||
|
end_time = start_time + duration_delta
|
||||||
|
except OverflowError:
|
||||||
|
end_time = datetime.max
|
||||||
|
else:
|
||||||
|
raise InvalidDurationError(f"Duration must be positive: {duration_seconds}")
|
||||||
|
|
||||||
silence = {
|
silence = {
|
||||||
|
"id": silence_id,
|
||||||
"matchers": silence_matchers,
|
"matchers": silence_matchers,
|
||||||
"startsAt": start_time.isoformat(),
|
"startsAt": start_time.isoformat(),
|
||||||
"endsAt": end_time.isoformat(),
|
"endsAt": end_time.isoformat(),
|
||||||
|
@ -82,33 +102,23 @@ class AlertmanagerClient:
|
||||||
|
|
||||||
return data["silenceID"]
|
return data["silenceID"]
|
||||||
|
|
||||||
async def delete_silences(
|
async def delete_silence(self, silence_id: str) -> None:
|
||||||
self, fingerprint: str, matchers: List[AlertMatcher]
|
silence = await self.get_silence(silence_id)
|
||||||
) -> List[str]:
|
|
||||||
alert = await self.get_alert(fingerprint)
|
|
||||||
|
|
||||||
alert_state = alert["status"]["state"]
|
silence_state = silence["state"]
|
||||||
if alert_state != "suppressed":
|
if silence_state == "expired":
|
||||||
raise SilenceNotFoundError(
|
raise SilenceExpiredError(
|
||||||
f"Cannot find silences for alert fingerprint {fingerprint} in state {alert_state}"
|
f"Cannot delete already expired silence with ID {silence_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._match_alert(alert, matchers)
|
|
||||||
|
|
||||||
silences = alert["status"]["silencedBy"]
|
|
||||||
for silence in silences:
|
|
||||||
await self._delete_silence(silence)
|
|
||||||
return silences
|
|
||||||
|
|
||||||
async def _delete_silence(self, silence: str) -> None:
|
|
||||||
try:
|
try:
|
||||||
async with self.session.delete(
|
async with self.session.delete(
|
||||||
f"{self.api_url}/silence/{silence}"
|
f"{self.api_url}/silence/{silence_id}"
|
||||||
) as response:
|
) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except ClientError as e:
|
except ClientError as e:
|
||||||
raise AlertmanagerServerError(
|
raise AlertmanagerServerError(
|
||||||
f"Cannot delete silence with ID {silence}"
|
f"Cannot delete silence with ID {silence_id}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -119,16 +129,8 @@ class AlertmanagerClient:
|
||||||
raise AlertNotFoundError(f"Cannot find alert with fingerprint {fingerprint}")
|
raise AlertNotFoundError(f"Cannot find alert with fingerprint {fingerprint}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _match_alert(alert: Dict, matchers: List[AlertMatcher]) -> None:
|
def _find_silence(silence_id: str, silences: List[Dict]) -> Dict:
|
||||||
labels = alert["labels"]
|
for silence in silences:
|
||||||
for matcher in matchers:
|
if silence["id"] == silence_id:
|
||||||
if matcher.label not in labels:
|
return silence
|
||||||
labels_text = ", ".join(labels)
|
raise SilenceNotFoundError(f"Cannot find silence with ID {silence_id}")
|
||||||
raise AlertMismatchError(
|
|
||||||
f"Cannot find label {matcher.label} in alert labels: {labels_text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not matcher.match(labels):
|
|
||||||
raise AlertMismatchError(
|
|
||||||
f"Alert with label {matcher.label}={labels[matcher.label]} does not match {matcher}"
|
|
||||||
)
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ from nio import (
|
||||||
|
|
||||||
from matrix_alertbot.alertmanager import AlertmanagerClient
|
from matrix_alertbot.alertmanager import AlertmanagerClient
|
||||||
from matrix_alertbot.chat_functions import strip_fallback
|
from matrix_alertbot.chat_functions import strip_fallback
|
||||||
from matrix_alertbot.command import CommandFactory
|
from matrix_alertbot.command import AckAlertCommand, CommandFactory, UnackAlertCommand
|
||||||
from matrix_alertbot.config import Config
|
from matrix_alertbot.config import Config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
REACTION_DURATIONS = {"🤫": "12h", "😶": "1d", "🤐": "3d", "🙊": "5d", "🔇": "1w", "🔕": "3w"}
|
REACTIONS = {"🤫", "😶", "🤐", "🙊", "🔇", "🔕"}
|
||||||
|
|
||||||
|
|
||||||
class Callbacks:
|
class Callbacks:
|
||||||
|
@ -56,9 +56,6 @@ class Callbacks:
|
||||||
|
|
||||||
event: The event defining the message.
|
event: The event defining the message.
|
||||||
"""
|
"""
|
||||||
# Extract the message text
|
|
||||||
msg = strip_fallback(event.body)
|
|
||||||
|
|
||||||
# Ignore messages from ourselves
|
# Ignore messages from ourselves
|
||||||
if event.sender == self.client.user:
|
if event.sender == self.client.user:
|
||||||
return
|
return
|
||||||
|
@ -67,6 +64,9 @@ class Callbacks:
|
||||||
if room.room_id != self.config.room_id:
|
if room.room_id != self.config.room_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Extract the message text
|
||||||
|
msg = strip_fallback(event.body)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Bot message received for room {room.display_name} | "
|
f"Bot message received for room {room.display_name} | "
|
||||||
f"{room.user_name(event.sender)}: {msg}"
|
f"{room.user_name(event.sender)}: {msg}"
|
||||||
|
@ -75,19 +75,19 @@ class Callbacks:
|
||||||
has_command_prefix = msg.startswith(self.command_prefix)
|
has_command_prefix = msg.startswith(self.command_prefix)
|
||||||
if not has_command_prefix:
|
if not has_command_prefix:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Message received without command prefix {self.command_prefix}: Aborting."
|
f"Cannot process message: Command prefix {self.command_prefix} not provided."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
source_content = event.source["content"]
|
source_content = event.source["content"]
|
||||||
alert_event_id = (
|
reacted_to_event_id = (
|
||||||
source_content.get("m.relates_to", {})
|
source_content.get("m.relates_to", {})
|
||||||
.get("m.in_reply_to", {})
|
.get("m.in_reply_to", {})
|
||||||
.get("event_id")
|
.get("event_id")
|
||||||
)
|
)
|
||||||
|
|
||||||
if alert_event_id is None:
|
if reacted_to_event_id is not None:
|
||||||
logger.warning("Unable to find the event ID of the alert")
|
logger.debug(f"Command in reply to event ID {reacted_to_event_id}")
|
||||||
|
|
||||||
# Remove the command prefix
|
# Remove the command prefix
|
||||||
cmd = msg[len(self.command_prefix) :]
|
cmd = msg[len(self.command_prefix) :]
|
||||||
|
@ -101,10 +101,10 @@ class Callbacks:
|
||||||
room,
|
room,
|
||||||
event.sender,
|
event.sender,
|
||||||
event.event_id,
|
event.event_id,
|
||||||
alert_event_id,
|
reacted_to_event_id,
|
||||||
)
|
)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
logging.error(f"Unable to create the command '{cmd}': {e}")
|
logging.error(f"Cannot process command '{cmd}': {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
@ -176,10 +176,9 @@ class Callbacks:
|
||||||
reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key")
|
reaction = event.source.get("content", {}).get("m.relates_to", {}).get("key")
|
||||||
logger.debug(f"Got reaction {reaction} to {room.room_id} from {event.sender}.")
|
logger.debug(f"Got reaction {reaction} to {room.room_id} from {event.sender}.")
|
||||||
|
|
||||||
if reaction not in REACTION_DURATIONS:
|
if reaction not in REACTIONS:
|
||||||
logger.warning(f"Uknown duration reaction {reaction}")
|
logger.warning(f"Uknown duration reaction {reaction}")
|
||||||
return
|
return
|
||||||
duration = REACTION_DURATIONS[reaction]
|
|
||||||
|
|
||||||
# Get the original event that was reacted to
|
# Get the original event that was reacted to
|
||||||
event_response = await self.client.room_get_event(room.room_id, alert_event_id)
|
event_response = await self.client.room_get_event(room.room_id, alert_event_id)
|
||||||
|
@ -194,29 +193,17 @@ class Callbacks:
|
||||||
if reacted_to_event.sender != self.config.user_id:
|
if reacted_to_event.sender != self.config.user_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.cache.set(
|
|
||||||
event.event_id,
|
|
||||||
reacted_to_event.event_id,
|
|
||||||
expire=self.config.cache_expire_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send a message acknowledging the reaction
|
# Send a message acknowledging the reaction
|
||||||
cmd = f"ack {duration}"
|
command = AckAlertCommand(
|
||||||
try:
|
self.client,
|
||||||
command = CommandFactory.create(
|
self.cache,
|
||||||
cmd,
|
self.alertmanager,
|
||||||
self.client,
|
self.config,
|
||||||
self.cache,
|
room,
|
||||||
self.alertmanager,
|
event.sender,
|
||||||
self.config,
|
event.event_id,
|
||||||
room,
|
alert_event_id,
|
||||||
event.sender,
|
)
|
||||||
event.event_id,
|
|
||||||
alert_event_id,
|
|
||||||
)
|
|
||||||
except TypeError as e:
|
|
||||||
logging.error(f"Unable to create the command '{cmd}': {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
|
@ -226,36 +213,29 @@ class Callbacks:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Ignore redactions from ourselves
|
# Ignore redactions from ourselves
|
||||||
if event.sender == self.config.user_id:
|
if event.sender == self.client.user:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Read alert event ID for redacted event {event.redacts} from cache"
|
f"Read alert event ID for redacted event {event.redacts} from cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
if event.redacts not in self.cache:
|
|
||||||
logger.warning(
|
|
||||||
f"Unable to remove silences from event {event.redacts}: Redacted event is not in cache"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
alert_event_id: str = self.cache[event.redacts]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
command = CommandFactory.create(
|
reacted_to_event_id: str = self.cache[event.redacts]
|
||||||
"unack",
|
except KeyError:
|
||||||
self.client,
|
logger.warning(f"Unable to find silence from event {event.redacts}")
|
||||||
self.cache,
|
|
||||||
self.alertmanager,
|
|
||||||
self.config,
|
|
||||||
room,
|
|
||||||
event.sender,
|
|
||||||
event.redacts,
|
|
||||||
alert_event_id,
|
|
||||||
)
|
|
||||||
except TypeError as e:
|
|
||||||
logging.error(f"Unable to create the command 'unack': {e}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
command = UnackAlertCommand(
|
||||||
|
self.client,
|
||||||
|
self.cache,
|
||||||
|
self.alertmanager,
|
||||||
|
self.config,
|
||||||
|
room,
|
||||||
|
event.sender,
|
||||||
|
event.redacts,
|
||||||
|
reacted_to_event_id,
|
||||||
|
)
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None:
|
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import pytimeparse2
|
import pytimeparse2
|
||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
|
@ -13,7 +13,6 @@ from matrix_alertbot.errors import (
|
||||||
AlertNotFoundError,
|
AlertNotFoundError,
|
||||||
SilenceNotFoundError,
|
SilenceNotFoundError,
|
||||||
)
|
)
|
||||||
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -25,10 +24,10 @@ class BaseCommand:
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
alertmanager: AlertmanagerClient,
|
alertmanager: AlertmanagerClient,
|
||||||
config: Config,
|
config: Config,
|
||||||
cmd: str,
|
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
sender: str,
|
sender: str,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
|
args: Tuple[str, ...] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""A command made by a user.
|
"""A command made by a user.
|
||||||
|
|
||||||
|
@ -53,12 +52,15 @@ class BaseCommand:
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.alertmanager = alertmanager
|
self.alertmanager = alertmanager
|
||||||
self.config = config
|
self.config = config
|
||||||
self.cmd = cmd
|
|
||||||
self.args = cmd.split()[1:]
|
|
||||||
self.room = room
|
self.room = room
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.event_id = event_id
|
self.event_id = event_id
|
||||||
|
|
||||||
|
if args is not None:
|
||||||
|
self.args = args
|
||||||
|
else:
|
||||||
|
self.args = ()
|
||||||
|
|
||||||
async def process(self) -> None:
|
async def process(self) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -70,166 +72,158 @@ class BaseAlertCommand(BaseCommand):
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
alertmanager: AlertmanagerClient,
|
alertmanager: AlertmanagerClient,
|
||||||
config: Config,
|
config: Config,
|
||||||
cmd: str,
|
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
sender: str,
|
sender: str,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
alert_event_id: str,
|
reacted_to_event_id: str,
|
||||||
|
args: Tuple[str, ...] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
client, cache, alertmanager, config, cmd, room, sender, event_id
|
client, cache, alertmanager, config, room, sender, event_id, args
|
||||||
)
|
)
|
||||||
|
|
||||||
self.alert_event_id = alert_event_id
|
self.reacted_to_event_id = reacted_to_event_id
|
||||||
|
|
||||||
|
|
||||||
class AckAlertCommand(BaseAlertCommand):
|
class AckAlertCommand(BaseAlertCommand):
|
||||||
async def process(self) -> None:
|
async def process(self) -> None:
|
||||||
"""Acknowledge an alert and silence it for a certain duration in Alertmanager"""
|
"""Acknowledge an alert and silence it for a certain duration in Alertmanager"""
|
||||||
matchers: List[AlertMatcher] = []
|
durations = self.args
|
||||||
durations = []
|
|
||||||
for arg in self.args:
|
|
||||||
if "=~" in arg:
|
|
||||||
label, regex = arg.split("=~")
|
|
||||||
regex_matcher = AlertRegexMatcher(label, regex)
|
|
||||||
matchers.append(regex_matcher)
|
|
||||||
elif "=" in arg:
|
|
||||||
label, value = arg.split("=")
|
|
||||||
matcher = AlertMatcher(label, value)
|
|
||||||
matchers.append(matcher)
|
|
||||||
else:
|
|
||||||
durations.append(arg)
|
|
||||||
|
|
||||||
if len(durations) > 0:
|
if len(durations) > 0:
|
||||||
duration = " ".join(durations)
|
duration = " ".join(durations)
|
||||||
else:
|
logger.debug(f"Receiving a command to create a silence for {duration}.")
|
||||||
duration = "1d"
|
|
||||||
|
|
||||||
logger.debug(
|
duration_seconds = pytimeparse2.parse(duration)
|
||||||
f"Receiving a command to create a silence for a duration of {duration}"
|
if duration_seconds is None:
|
||||||
)
|
logger.error(f"Unable to create silence: Invalid duration '{duration}'")
|
||||||
|
await send_text_to_room(
|
||||||
duration_seconds = pytimeparse2.parse(duration)
|
self.client,
|
||||||
if duration_seconds is None:
|
self.room.room_id,
|
||||||
logger.error(f"Unable to create silence: Invalid duration '{duration}'")
|
f"I tried really hard, but I can't convert the duration '{duration}' to a number of seconds.",
|
||||||
await send_text_to_room(
|
|
||||||
self.client,
|
|
||||||
self.room.room_id,
|
|
||||||
f"I tried really hard, but I can't convert the duration '{duration}' to a number of seconds.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Read alert fingerprints for alert event {self.alert_event_id} from cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.alert_event_id not in self.cache:
|
|
||||||
logger.error(
|
|
||||||
f"Cannot find fingerprints for alert event {self.alert_event_id} in cache"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
alert_fingerprints: Tuple[str] = self.cache[self.alert_event_id]
|
|
||||||
logger.debug(f"Found {len(alert_fingerprints)} in cache")
|
|
||||||
|
|
||||||
count_alert_not_found = 0
|
|
||||||
created_silences = []
|
|
||||||
for alert_fingerprint in alert_fingerprints:
|
|
||||||
logger.debug(
|
|
||||||
f"Create silence for alert with fingerprint {alert_fingerprint} for a duration of {duration}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
silence_id = await self.alertmanager.create_silence(
|
|
||||||
alert_fingerprint,
|
|
||||||
duration_seconds,
|
|
||||||
self.room.user_name(self.sender),
|
|
||||||
matchers,
|
|
||||||
)
|
)
|
||||||
created_silences.append(silence_id)
|
return
|
||||||
except AlertNotFoundError as e:
|
else:
|
||||||
logger.warning(f"Unable to create silence: {e}")
|
duration_seconds = None
|
||||||
count_alert_not_found += 1
|
logger.debug(
|
||||||
except AlertmanagerError as e:
|
"Receiving a command to create a silence for an indefinite period"
|
||||||
logger.exception(f"Unable to create silence: {e}", exc_info=e)
|
)
|
||||||
|
|
||||||
matchers_id = "".join(sorted(str(matcher) for matcher in matchers))
|
logger.debug(
|
||||||
ack_id = "".join(alert_fingerprints) + str(duration_seconds) + matchers_id
|
f"Reading alert fingerprint for event {self.reacted_to_event_id} from cache"
|
||||||
self.cache.set(ack_id, tuple(created_silences), expire=duration_seconds)
|
)
|
||||||
|
try:
|
||||||
|
alert_fingerprint: str = self.cache[self.reacted_to_event_id]
|
||||||
|
except KeyError:
|
||||||
|
logger.error(
|
||||||
|
f"Cannot find fingerprint for alert event {self.reacted_to_event_id} in cache"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if count_alert_not_found > 0:
|
cached_silence_id = self.cache.get(alert_fingerprint)
|
||||||
|
if cached_silence_id is None:
|
||||||
|
logger.debug(
|
||||||
|
f"Creating silence for alert with fingerprint {alert_fingerprint}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Updating silence with ID {cached_silence_id} for alert with fingerprint {alert_fingerprint}."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
silence_id = await self.alertmanager.create_silence(
|
||||||
|
alert_fingerprint,
|
||||||
|
self.room.user_name(self.sender),
|
||||||
|
duration_seconds,
|
||||||
|
cached_silence_id,
|
||||||
|
)
|
||||||
|
except AlertNotFoundError as e:
|
||||||
|
logger.warning(f"Unable to create silence: {e}")
|
||||||
await send_text_to_room(
|
await send_text_to_room(
|
||||||
self.client,
|
self.client,
|
||||||
self.room.room_id,
|
self.room.room_id,
|
||||||
f"Sorry, I couldn't find {count_alert_not_found} alerts, therefore I couldn't create their silence.",
|
f"Sorry, I couldn't find alert with fingerprint {alert_fingerprint}, therefore "
|
||||||
|
"I couldn't create the silence.",
|
||||||
)
|
)
|
||||||
|
return
|
||||||
if len(created_silences) > 0:
|
except AlertmanagerError as e:
|
||||||
|
logger.exception(f"Unable to create silence: {e}", exc_info=e)
|
||||||
await send_text_to_room(
|
await send_text_to_room(
|
||||||
self.client,
|
self.client,
|
||||||
self.room.room_id,
|
self.room.room_id,
|
||||||
f"Created {len(created_silences)} silences with a duration of {duration}.",
|
"Something went wrong with Alertmanager, therefore "
|
||||||
|
f"I couldn't create silence for alert fingerprint {alert_fingerprint}.",
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cache.set(self.event_id, alert_fingerprint, expire=duration_seconds)
|
||||||
|
self.cache.set(alert_fingerprint, silence_id, expire=duration_seconds)
|
||||||
|
|
||||||
|
await send_text_to_room(
|
||||||
|
self.client,
|
||||||
|
self.room.room_id,
|
||||||
|
f"Created silence with ID {silence_id}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UnackAlertCommand(BaseAlertCommand):
|
class UnackAlertCommand(BaseAlertCommand):
|
||||||
async def process(self) -> None:
|
async def process(self) -> None:
|
||||||
"""Delete an alert's acknowledgement of an alert and remove corresponding silence in Alertmanager"""
|
"""Delete an alert's acknowledgement of an alert and remove corresponding silence in Alertmanager"""
|
||||||
matchers: List[AlertMatcher] = []
|
|
||||||
for arg in self.args:
|
|
||||||
if "=~" in arg:
|
|
||||||
label, regex = arg.split("=~")
|
|
||||||
regex_matcher = AlertRegexMatcher(label, regex)
|
|
||||||
matchers.append(regex_matcher)
|
|
||||||
elif "=" in arg:
|
|
||||||
label, value = arg.split("=")
|
|
||||||
matcher = AlertMatcher(label, value)
|
|
||||||
matchers.append(matcher)
|
|
||||||
|
|
||||||
logger.debug("Receiving a command to delete a silence")
|
logger.debug("Receiving a command to delete a silence")
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Read alert fingerprints for alert event {self.alert_event_id} from cache"
|
f"Reading alert fingerprint for event {self.reacted_to_event_id} from cache."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
alert_fingerprint: str = self.cache[self.reacted_to_event_id]
|
||||||
|
except KeyError:
|
||||||
|
logger.error(
|
||||||
|
f"Cannot find fingerprint for alert event {self.reacted_to_event_id} in cache."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
logger.debug(f"Found alert fingerprint {alert_fingerprint} in cache.")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Reading silence ID for alert fingerprint {alert_fingerprint} from cache."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
silence_id: str = self.cache[alert_fingerprint]
|
||||||
|
except KeyError:
|
||||||
|
logger.error(
|
||||||
|
f"Cannot find silence for alert fingerprint {alert_fingerprint} in cache"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
logger.debug(f"Found silence ID {silence_id} in cache.")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Deleting silence with ID {silence_id} for alert with fingerprint {alert_fingerprint}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.alert_event_id not in self.cache:
|
try:
|
||||||
logger.error(
|
await self.alertmanager.delete_silence(silence_id)
|
||||||
f"Cannot find fingerprints for event {self.alert_event_id} in cache"
|
except (AlertNotFoundError, SilenceNotFoundError) as e:
|
||||||
|
logger.error(f"Unable to delete silence: {e}")
|
||||||
|
await send_text_to_room(
|
||||||
|
self.client,
|
||||||
|
self.room.room_id,
|
||||||
|
f"Sorry, I couldn't find alert with fingerprint {alert_fingerprint}, therefore "
|
||||||
|
"I couldn't remove its silence.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except AlertmanagerError as e:
|
||||||
|
logger.exception(f"Unable to delete silence: {e}", exc_info=e)
|
||||||
|
await send_text_to_room(
|
||||||
|
self.client,
|
||||||
|
self.room.room_id,
|
||||||
|
"Something went wrong with Alertmanager, therefore "
|
||||||
|
f"I couldn't delete silence for alert fingerprint {alert_fingerprint}.",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
alert_fingerprints: Tuple[str] = self.cache[self.alert_event_id]
|
await send_text_to_room(
|
||||||
logger.debug(f"Found {len(alert_fingerprints)} in cache")
|
self.client,
|
||||||
|
self.room.room_id,
|
||||||
count_alert_not_found = 0
|
f"Removed silence with ID {silence_id}.",
|
||||||
count_removed_silences = 0
|
)
|
||||||
for alert_fingerprint in alert_fingerprints:
|
|
||||||
logger.debug(
|
|
||||||
f"Delete silence for alert with fingerprint {alert_fingerprint}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
removed_silences = await self.alertmanager.delete_silences(
|
|
||||||
alert_fingerprint, matchers
|
|
||||||
)
|
|
||||||
count_removed_silences += len(removed_silences)
|
|
||||||
except (AlertNotFoundError, SilenceNotFoundError) as e:
|
|
||||||
logger.error(f"Unable to delete silence: {e}")
|
|
||||||
count_alert_not_found += 1
|
|
||||||
except AlertmanagerError as e:
|
|
||||||
logger.exception(f"Unable to delete silence: {e}", exc_info=e)
|
|
||||||
|
|
||||||
if count_alert_not_found > 0:
|
|
||||||
await send_text_to_room(
|
|
||||||
self.client,
|
|
||||||
self.room.room_id,
|
|
||||||
f"Sorry, I couldn't find {count_alert_not_found} alerts, therefore I couldn't remove their silences.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if count_removed_silences > 0:
|
|
||||||
await send_text_to_room(
|
|
||||||
self.client,
|
|
||||||
self.room.room_id,
|
|
||||||
f"Removed {count_removed_silences} silences.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HelpCommand(BaseCommand):
|
class HelpCommand(BaseCommand):
|
||||||
|
@ -262,7 +256,7 @@ class UnknownCommand(BaseCommand):
|
||||||
await send_text_to_room(
|
await send_text_to_room(
|
||||||
self.client,
|
self.client,
|
||||||
self.room.room_id,
|
self.room.room_id,
|
||||||
f"Unknown command '{self.cmd}'. Try the 'help' command for more information.",
|
"Unknown command. Try the 'help' command for more information.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -279,6 +273,8 @@ class CommandFactory:
|
||||||
event_id: str,
|
event_id: str,
|
||||||
reacted_to_event_id: Optional[str] = None,
|
reacted_to_event_id: Optional[str] = None,
|
||||||
) -> BaseCommand:
|
) -> BaseCommand:
|
||||||
|
args = tuple(cmd.split()[1:])
|
||||||
|
|
||||||
if cmd.startswith("ack"):
|
if cmd.startswith("ack"):
|
||||||
if reacted_to_event_id is None:
|
if reacted_to_event_id is None:
|
||||||
raise TypeError("Alert command must be in reply to an alert event.")
|
raise TypeError("Alert command must be in reply to an alert event.")
|
||||||
|
@ -288,11 +284,11 @@ class CommandFactory:
|
||||||
cache,
|
cache,
|
||||||
alertmanager,
|
alertmanager,
|
||||||
config,
|
config,
|
||||||
cmd,
|
|
||||||
room,
|
room,
|
||||||
sender,
|
sender,
|
||||||
event_id,
|
event_id,
|
||||||
reacted_to_event_id,
|
reacted_to_event_id,
|
||||||
|
args,
|
||||||
)
|
)
|
||||||
elif cmd.startswith("unack") or cmd.startswith("nack"):
|
elif cmd.startswith("unack") or cmd.startswith("nack"):
|
||||||
if reacted_to_event_id is None:
|
if reacted_to_event_id is None:
|
||||||
|
@ -303,17 +299,17 @@ class CommandFactory:
|
||||||
cache,
|
cache,
|
||||||
alertmanager,
|
alertmanager,
|
||||||
config,
|
config,
|
||||||
cmd,
|
|
||||||
room,
|
room,
|
||||||
sender,
|
sender,
|
||||||
event_id,
|
event_id,
|
||||||
reacted_to_event_id,
|
reacted_to_event_id,
|
||||||
|
args,
|
||||||
)
|
)
|
||||||
elif cmd.startswith("help"):
|
elif cmd.startswith("help"):
|
||||||
return HelpCommand(
|
return HelpCommand(
|
||||||
client, cache, alertmanager, config, cmd, room, sender, event_id
|
client, cache, alertmanager, config, room, sender, event_id, args
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return UnknownCommand(
|
return UnknownCommand(
|
||||||
client, cache, alertmanager, config, cmd, room, sender, event_id
|
client, cache, alertmanager, config, room, sender, event_id, args
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,14 +37,20 @@ class AlertNotFoundError(AlertmanagerError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AlertMismatchError(AlertmanagerError):
|
class SilenceNotFoundError(AlertmanagerError):
|
||||||
"""An error encountered when alert's labels don't match."""
|
"""An error encountered when a silence cannot be found in Alertmanager."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SilenceNotFoundError(AlertmanagerError):
|
class SilenceExpiredError(AlertmanagerError):
|
||||||
"""An error encountered when a silence cannot be found in Alertmanager."""
|
"""An error encountered when a silence is already expired in Alertmanager."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidDurationError(AlertmanagerError):
|
||||||
|
"""An error encountered when an alert has an invalid duration."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -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")
|
@routes.post("/alerts")
|
||||||
async def create_alert(request: web_request.Request) -> web.Response:
|
async def create_alerts(request: web_request.Request) -> web.Response:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
logger.info(f"Received alert: {data}")
|
logger.info(f"Received alerts: {data}")
|
||||||
client: AsyncClient = request.app["client"]
|
client: AsyncClient = request.app["client"]
|
||||||
config: Config = request.app["config"]
|
config: Config = request.app["config"]
|
||||||
cache: Cache = request.app["cache"]
|
cache: Cache = request.app["cache"]
|
||||||
|
@ -40,34 +40,30 @@ async def create_alert(request: web_request.Request) -> web.Response:
|
||||||
if len(data["alerts"]) == 0:
|
if len(data["alerts"]) == 0:
|
||||||
return web.Response(status=400, body="Alerts cannot be empty.")
|
return web.Response(status=400, body="Alerts cannot be empty.")
|
||||||
|
|
||||||
plaintext = ""
|
for alert in data["alerts"]:
|
||||||
html = ""
|
|
||||||
for i, alert in enumerate(data["alerts"]):
|
|
||||||
try:
|
try:
|
||||||
alert = Alert.from_dict(alert)
|
alert = Alert.from_dict(alert)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return web.Response(status=400, body=f"Invalid alert: {alert}.")
|
return web.Response(status=400, body=f"Invalid alert: {alert}.")
|
||||||
|
|
||||||
if i != 0:
|
plaintext = alert.plaintext()
|
||||||
plaintext += "\n"
|
html = alert.html()
|
||||||
html += "<br/>\n"
|
|
||||||
plaintext += alert.plaintext()
|
|
||||||
html += alert.html()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event = await send_text_to_room(
|
event = await send_text_to_room(
|
||||||
client, config.room_id, plaintext, html, notice=False
|
client, config.room_id, plaintext, html, notice=False
|
||||||
)
|
)
|
||||||
except (LocalProtocolError, ClientError) as e:
|
except (LocalProtocolError, ClientError) as e:
|
||||||
logger.error(e)
|
logger.error(
|
||||||
return web.Response(
|
f"Unable to send alert {alert.fingerprint} to Matrix room: {e}"
|
||||||
status=500, body="An error occured when sending alerts to Matrix room."
|
)
|
||||||
)
|
return web.Response(
|
||||||
|
status=500,
|
||||||
|
body=f"An error occured when sending alert with fingerprint '{alert.fingerprint}' to Matrix room.",
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(event.event_id, alert.fingerprint, expire=config.cache_expire_time)
|
||||||
|
|
||||||
fingerprints = tuple(sorted(alert["fingerprint"] for alert in data["alerts"]))
|
|
||||||
cache.set(
|
|
||||||
event.event_id, fingerprints, expire=config.cache_expire_time, tag="event"
|
|
||||||
)
|
|
||||||
return web.Response(status=200)
|
return web.Response(status=200)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class AlertTestCase(unittest.TestCase):
|
||||||
self.alert_dict["status"] = "firing"
|
self.alert_dict["status"] = "firing"
|
||||||
alert = Alert.from_dict(self.alert_dict)
|
alert = Alert.from_dict(self.alert_dict)
|
||||||
|
|
||||||
self.assertEqual("fingerprint1", alert.id)
|
self.assertEqual("fingerprint1", alert.fingerprint)
|
||||||
self.assertEqual("http://example.com", alert.url)
|
self.assertEqual("http://example.com", alert.url)
|
||||||
self.assertTrue(alert.firing)
|
self.assertTrue(alert.firing)
|
||||||
self.assertEqual("critical", alert.status)
|
self.assertEqual("critical", alert.status)
|
||||||
|
|
|
@ -2,24 +2,25 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, List
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import aiohttp.test_utils
|
import aiohttp.test_utils
|
||||||
import aiotools
|
import aiotools
|
||||||
from aiohttp import web, web_request
|
from aiohttp import web, web_request
|
||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from matrix_alertbot.alertmanager import AlertmanagerClient
|
from matrix_alertbot.alertmanager import AlertmanagerClient
|
||||||
from matrix_alertbot.errors import (
|
from matrix_alertbot.errors import (
|
||||||
AlertmanagerServerError,
|
AlertmanagerServerError,
|
||||||
AlertMismatchError,
|
|
||||||
AlertNotFoundError,
|
AlertNotFoundError,
|
||||||
|
InvalidDurationError,
|
||||||
|
SilenceExpiredError,
|
||||||
SilenceNotFoundError,
|
SilenceNotFoundError,
|
||||||
)
|
)
|
||||||
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
|
|
||||||
|
|
||||||
|
|
||||||
class FakeTimeDelta:
|
class FakeTimeDelta:
|
||||||
|
@ -36,10 +37,15 @@ class AbstractFakeAlertmanagerServer:
|
||||||
self.app.router.add_routes(
|
self.app.router.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/api/v2/alerts", self.get_alerts),
|
web.get("/api/v2/alerts", self.get_alerts),
|
||||||
|
web.get("/api/v2/silences", self.get_silences),
|
||||||
web.post("/api/v2/silences", self.create_silence),
|
web.post("/api/v2/silences", self.create_silence),
|
||||||
web.delete("/api/v2/silence/{silence}", self.delete_silence),
|
web.delete("/api/v2/silence/{silence}", self.delete_silence),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
self.app["silences"] = [
|
||||||
|
{"id": "silence1", "state": "active"},
|
||||||
|
{"id": "silence2", "state": "expired"},
|
||||||
|
]
|
||||||
|
|
||||||
self.runner = web.AppRunner(self.app)
|
self.runner = web.AppRunner(self.app)
|
||||||
|
|
||||||
|
@ -64,6 +70,9 @@ class AbstractFakeAlertmanagerServer:
|
||||||
async def get_alerts(self, request: web_request.Request) -> web.Response:
|
async def get_alerts(self, request: web_request.Request) -> web.Response:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def get_silences(self, request: web_request.Request) -> web.Response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
async def create_silence(self, request: web_request.Request) -> web.Response:
|
async def create_silence(self, request: web_request.Request) -> web.Response:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -94,25 +103,56 @@ class FakeAlertmanagerServer(AbstractFakeAlertmanagerServer):
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_silence(self, request: web_request.Request) -> web.Response:
|
async def get_silences(self, request: web_request.Request) -> web.Response:
|
||||||
return web.Response(
|
return web.Response(
|
||||||
body=json.dumps({"silenceID": "silence1"}), content_type="application/json"
|
body=json.dumps(self.app["silences"]), content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def create_silence(self, request: web_request.Request) -> web.Response:
|
||||||
|
silences = self.app["silences"]
|
||||||
|
|
||||||
|
silence = await request.json()
|
||||||
|
if silence["id"] is None:
|
||||||
|
silence["id"] = "silence1"
|
||||||
|
silence["state"] = "active"
|
||||||
|
silences.append(silence)
|
||||||
|
|
||||||
|
return web.Response(
|
||||||
|
body=json.dumps({"silenceID": silence["id"]}),
|
||||||
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def delete_silence(self, request: web_request.Request) -> web.Response:
|
async def delete_silence(self, request: web_request.Request) -> web.Response:
|
||||||
|
silence_id = request.match_info["silence"]
|
||||||
|
for i, silence in enumerate(self.app["silences"]):
|
||||||
|
if silence["id"] == silence_id:
|
||||||
|
del self.app["silences"][i]
|
||||||
|
break
|
||||||
|
|
||||||
return web.Response(status=200, content_type="application/json")
|
return web.Response(status=200, content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
class FakeAlertmanagerServerWithoutAlert(AbstractFakeAlertmanagerServer):
|
class FakeAlertmanagerServerWithoutAlert(FakeAlertmanagerServer):
|
||||||
async def get_alerts(self, request: web_request.Request) -> web.Response:
|
async def get_alerts(self, request: web_request.Request) -> web.Response:
|
||||||
return web.Response(body=json.dumps([]), content_type="application/json")
|
return web.Response(body=json.dumps([]), content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
class FakeAlertmanagerServerWithErrorAlerts(AbstractFakeAlertmanagerServer):
|
class FakeAlertmanagerServerWithErrorAlerts(FakeAlertmanagerServer):
|
||||||
async def get_alerts(self, request: web_request.Request) -> web.Response:
|
async def get_alerts(self, request: web_request.Request) -> web.Response:
|
||||||
return web.Response(status=500)
|
return web.Response(status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAlertmanagerServerWithoutSilence(FakeAlertmanagerServer):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app["silences"] = []
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAlertmanagerServerWithErrorSilences(FakeAlertmanagerServer):
|
||||||
|
async def get_silences(self, request: web_request.Request) -> web.Response:
|
||||||
|
return web.Response(status=500)
|
||||||
|
|
||||||
|
|
||||||
class FakeAlertmanagerServerWithErrorCreateSilence(FakeAlertmanagerServer):
|
class FakeAlertmanagerServerWithErrorCreateSilence(FakeAlertmanagerServer):
|
||||||
async def create_silence(self, request: web_request.Request) -> web.Response:
|
async def create_silence(self, request: web_request.Request) -> web.Response:
|
||||||
return web.Response(status=500)
|
return web.Response(status=500)
|
||||||
|
@ -137,24 +177,25 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
alerts = await alertmanager.get_alerts()
|
alerts = await alertmanager.get_alerts()
|
||||||
self.assertEqual(
|
|
||||||
[
|
self.assertEqual(
|
||||||
{
|
[
|
||||||
"fingerprint": "fingerprint1",
|
{
|
||||||
"labels": {"alertname": "alert1"},
|
"fingerprint": "fingerprint1",
|
||||||
"status": {"state": "active"},
|
"labels": {"alertname": "alert1"},
|
||||||
},
|
"status": {"state": "active"},
|
||||||
{
|
},
|
||||||
"fingerprint": "fingerprint2",
|
{
|
||||||
"labels": {"alertname": "alert2"},
|
"fingerprint": "fingerprint2",
|
||||||
"status": {
|
"labels": {"alertname": "alert2"},
|
||||||
"state": "suppressed",
|
"status": {
|
||||||
"silencedBy": ["silence1", "silence2"],
|
"state": "suppressed",
|
||||||
},
|
"silencedBy": ["silence1", "silence2"],
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
alerts,
|
],
|
||||||
)
|
alerts,
|
||||||
|
)
|
||||||
|
|
||||||
async def test_get_alerts_empty(self) -> None:
|
async def test_get_alerts_empty(self) -> None:
|
||||||
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
||||||
|
@ -164,7 +205,8 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
alerts = await alertmanager.get_alerts()
|
alerts = await alertmanager.get_alerts()
|
||||||
self.assertEqual([], alerts)
|
|
||||||
|
self.assertEqual([], alerts)
|
||||||
|
|
||||||
async def test_get_alerts_raise_alertmanager_error(self) -> None:
|
async def test_get_alerts_raise_alertmanager_error(self) -> None:
|
||||||
async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server:
|
async with FakeAlertmanagerServerWithErrorAlerts() as fake_alertmanager_server:
|
||||||
|
@ -176,6 +218,44 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
with self.assertRaises(AlertmanagerServerError):
|
with self.assertRaises(AlertmanagerServerError):
|
||||||
await alertmanager.get_alerts()
|
await alertmanager.get_alerts()
|
||||||
|
|
||||||
|
async def test_get_silences_happy(self) -> None:
|
||||||
|
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
silences = await alertmanager.get_silences()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
{"id": "silence1", "state": "active"},
|
||||||
|
{"id": "silence2", "state": "expired"},
|
||||||
|
],
|
||||||
|
silences,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_get_silences_empty(self) -> None:
|
||||||
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
silences = await alertmanager.get_silences()
|
||||||
|
|
||||||
|
self.assertEqual([], silences)
|
||||||
|
|
||||||
|
async def test_get_silences_raise_alertmanager_error(self) -> None:
|
||||||
|
async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
with self.assertRaises(AlertmanagerServerError):
|
||||||
|
await alertmanager.get_silences()
|
||||||
|
|
||||||
async def test_get_alert_happy(self) -> None:
|
async def test_get_alert_happy(self) -> None:
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
|
@ -184,14 +264,15 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
alert = await alertmanager.get_alert("fingerprint1")
|
alert = await alertmanager.get_alert("fingerprint1")
|
||||||
self.assertEqual(
|
|
||||||
{
|
self.assertEqual(
|
||||||
"fingerprint": "fingerprint1",
|
{
|
||||||
"labels": {"alertname": "alert1"},
|
"fingerprint": "fingerprint1",
|
||||||
"status": {"state": "active"},
|
"labels": {"alertname": "alert1"},
|
||||||
},
|
"status": {"state": "active"},
|
||||||
alert,
|
},
|
||||||
)
|
alert,
|
||||||
|
)
|
||||||
|
|
||||||
async def test_get_alert_raise_alert_not_found(self) -> None:
|
async def test_get_alert_raise_alert_not_found(self) -> None:
|
||||||
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
||||||
|
@ -213,120 +294,189 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
with self.assertRaises(AlertmanagerServerError):
|
with self.assertRaises(AlertmanagerServerError):
|
||||||
await alertmanager.get_alert("fingerprint1")
|
await alertmanager.get_alert("fingerprint1")
|
||||||
|
|
||||||
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta)
|
async def test_get_silence_happy(self) -> None:
|
||||||
async def test_create_silence_without_matchers(self, fake_timedelta: Mock) -> None:
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
f"http://localhost:{port}", self.fake_cache
|
f"http://localhost:{port}", self.fake_cache
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
silence = await alertmanager.create_silence(
|
silence1 = await alertmanager.get_silence("silence1")
|
||||||
"fingerprint1", 86400, "user", []
|
silence2 = await alertmanager.get_silence("silence2")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{"id": "silence1", "state": "active"},
|
||||||
|
silence1,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{"id": "silence2", "state": "expired"},
|
||||||
|
silence2,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_get_silence_raise_silence_not_found(self) -> None:
|
||||||
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
with self.assertRaises(SilenceNotFoundError):
|
||||||
|
await alertmanager.get_silence("silence1")
|
||||||
|
|
||||||
|
async def test_get_silence_raise_alertmanager_error(self) -> None:
|
||||||
|
async with FakeAlertmanagerServerWithErrorSilences() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
with self.assertRaises(AlertmanagerServerError):
|
||||||
|
await alertmanager.get_silence("silence1")
|
||||||
|
|
||||||
|
@freeze_time(datetime.utcfromtimestamp(0))
|
||||||
|
async def test_create_silence(self) -> None:
|
||||||
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
silence_id = await alertmanager.create_silence(
|
||||||
|
"fingerprint1", "user", 86400
|
||||||
)
|
)
|
||||||
|
silence = await alertmanager.get_silence("silence1")
|
||||||
|
|
||||||
self.assertEqual("silence1", silence)
|
self.assertEqual("silence1", silence_id)
|
||||||
fake_timedelta.assert_called_once_with(seconds=86400)
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"id": "silence1",
|
||||||
|
"state": "active",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"name": "alertname",
|
||||||
|
"value": "alert1",
|
||||||
|
"isRegex": False,
|
||||||
|
"isEqual": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdBy": "user",
|
||||||
|
"startsAt": "1970-01-01T00:00:00",
|
||||||
|
"endsAt": "1970-01-02T00:00:00",
|
||||||
|
"comment": "Acknowledge alert from Matrix",
|
||||||
|
},
|
||||||
|
silence,
|
||||||
|
)
|
||||||
|
|
||||||
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta)
|
@freeze_time(datetime.utcfromtimestamp(0))
|
||||||
async def test_create_silence_with_matchers(self, fake_timedelta: Mock) -> None:
|
async def test_create_silence_with_id(self) -> None:
|
||||||
matchers = [AlertMatcher(label="alertname", value="alert1")]
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
f"http://localhost:{port}", self.fake_cache
|
f"http://localhost:{port}", self.fake_cache
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
silence = await alertmanager.create_silence(
|
silence_id = await alertmanager.create_silence(
|
||||||
"fingerprint1",
|
"fingerprint1", "user", 86400, "silence2"
|
||||||
86400,
|
|
||||||
"user",
|
|
||||||
matchers,
|
|
||||||
)
|
)
|
||||||
|
silence = await alertmanager.get_silence("silence2")
|
||||||
|
|
||||||
self.assertEqual("silence1", silence)
|
self.assertEqual("silence2", silence_id)
|
||||||
fake_timedelta.assert_called_once_with(seconds=86400)
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"id": "silence2",
|
||||||
|
"state": "active",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"name": "alertname",
|
||||||
|
"value": "alert1",
|
||||||
|
"isRegex": False,
|
||||||
|
"isEqual": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdBy": "user",
|
||||||
|
"startsAt": "1970-01-01T00:00:00",
|
||||||
|
"endsAt": "1970-01-02T00:00:00",
|
||||||
|
"comment": "Acknowledge alert from Matrix",
|
||||||
|
},
|
||||||
|
silence,
|
||||||
|
)
|
||||||
|
|
||||||
@patch("matrix_alertbot.alertmanager.timedelta", side_effect=FakeTimeDelta)
|
@freeze_time(datetime.utcfromtimestamp(0))
|
||||||
async def test_create_silence_with_regex_matchers(
|
async def test_create_silence_with_indefinite_duration(self) -> None:
|
||||||
self, fake_timedelta: Mock
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
) -> None:
|
|
||||||
matchers: List[AlertMatcher] = [
|
|
||||||
AlertRegexMatcher(label="alertname", regex=r"alert\d+")
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
f"http://localhost:{port}", self.fake_cache
|
f"http://localhost:{port}", self.fake_cache
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
silence = await alertmanager.create_silence(
|
silence_id = await alertmanager.create_silence("fingerprint1", "user")
|
||||||
"fingerprint1",
|
silence = await alertmanager.get_silence("silence1")
|
||||||
86400,
|
|
||||||
"user",
|
self.assertEqual("silence1", silence_id)
|
||||||
matchers,
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"id": "silence1",
|
||||||
|
"state": "active",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"name": "alertname",
|
||||||
|
"value": "alert1",
|
||||||
|
"isRegex": False,
|
||||||
|
"isEqual": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdBy": "user",
|
||||||
|
"startsAt": "1970-01-01T00:00:00",
|
||||||
|
"endsAt": "9999-12-31T23:59:59.999999",
|
||||||
|
"comment": "Acknowledge alert from Matrix",
|
||||||
|
},
|
||||||
|
silence,
|
||||||
|
)
|
||||||
|
|
||||||
|
@freeze_time(datetime.utcfromtimestamp(0))
|
||||||
|
async def test_create_silence_with_max_duration(self) -> None:
|
||||||
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
|
port = fake_alertmanager_server.port
|
||||||
|
alertmanager = AlertmanagerClient(
|
||||||
|
f"http://localhost:{port}", self.fake_cache
|
||||||
|
)
|
||||||
|
async with aiotools.closing_async(alertmanager):
|
||||||
|
silence_id = await alertmanager.create_silence(
|
||||||
|
"fingerprint1", "user", int(timedelta.max.total_seconds())
|
||||||
)
|
)
|
||||||
|
silence = await alertmanager.get_silence("silence1")
|
||||||
|
|
||||||
self.assertEqual("silence1", silence)
|
self.assertEqual("silence1", silence_id)
|
||||||
fake_timedelta.assert_called_once_with(seconds=86400)
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"id": "silence1",
|
||||||
|
"state": "active",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"name": "alertname",
|
||||||
|
"value": "alert1",
|
||||||
|
"isRegex": False,
|
||||||
|
"isEqual": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdBy": "user",
|
||||||
|
"startsAt": "1970-01-01T00:00:00",
|
||||||
|
"endsAt": "9999-12-31T23:59:59.999999",
|
||||||
|
"comment": "Acknowledge alert from Matrix",
|
||||||
|
},
|
||||||
|
silence,
|
||||||
|
)
|
||||||
|
|
||||||
async def test_create_silence_raise_missing_label(self) -> None:
|
@freeze_time(datetime.utcfromtimestamp(0))
|
||||||
matchers = [
|
async def test_create_silence_raise_duration_error(self) -> None:
|
||||||
AlertMatcher(label="alertname", value="alert1"),
|
async with FakeAlertmanagerServerWithoutSilence() as fake_alertmanager_server:
|
||||||
AlertMatcher(label="severity", value="critical"),
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
f"http://localhost:{port}", self.fake_cache
|
f"http://localhost:{port}", self.fake_cache
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
with self.assertRaises(AlertMismatchError):
|
with self.assertRaises(InvalidDurationError):
|
||||||
await alertmanager.create_silence(
|
await alertmanager.create_silence("fingerprint1", "user", -1)
|
||||||
"fingerprint1",
|
|
||||||
86400,
|
|
||||||
"user",
|
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_create_silence_raise_mismatch_label(self) -> None:
|
|
||||||
matchers = [AlertMatcher(label="alertname", value="alert2")]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(AlertMismatchError):
|
|
||||||
await alertmanager.create_silence(
|
|
||||||
"fingerprint1",
|
|
||||||
86400,
|
|
||||||
"user",
|
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_create_silence_raise_mismatch_regex_label(self) -> None:
|
|
||||||
matchers: List[AlertMatcher] = [
|
|
||||||
AlertRegexMatcher(label="alertname", regex=r"alert[^\d]+")
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(AlertMismatchError):
|
|
||||||
await alertmanager.create_silence(
|
|
||||||
"fingerprint1",
|
|
||||||
86400,
|
|
||||||
"user",
|
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_create_silence_raise_alert_not_found(self) -> None:
|
async def test_create_silence_raise_alert_not_found(self) -> None:
|
||||||
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
||||||
|
@ -336,7 +486,7 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
with self.assertRaises(AlertNotFoundError):
|
with self.assertRaises(AlertNotFoundError):
|
||||||
await alertmanager.create_silence("fingerprint1", 86400, "user", [])
|
await alertmanager.create_silence("fingerprint1", "user")
|
||||||
|
|
||||||
async def test_create_silence_raise_alertmanager_error(self) -> None:
|
async def test_create_silence_raise_alertmanager_error(self) -> None:
|
||||||
async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server:
|
async with FakeAlertmanagerServerWithErrorCreateSilence() as fake_alertmanager_server:
|
||||||
|
@ -348,111 +498,40 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await alertmanager.get_alert("fingerprint1")
|
await alertmanager.get_alert("fingerprint1")
|
||||||
|
|
||||||
with self.assertRaises(AlertmanagerServerError):
|
with self.assertRaises(AlertmanagerServerError):
|
||||||
await alertmanager.create_silence("fingerprint1", 86400, "user", [])
|
await alertmanager.create_silence("fingerprint1", "user")
|
||||||
|
|
||||||
async def test_delete_silences_without_matchers(self) -> None:
|
async def test_delete_silence(self) -> None:
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
f"http://localhost:{port}", self.fake_cache
|
f"http://localhost:{port}", self.fake_cache
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
silences = await alertmanager.delete_silences("fingerprint2", [])
|
await alertmanager.delete_silence("silence1")
|
||||||
|
silences = await alertmanager.get_silences()
|
||||||
|
|
||||||
self.assertEqual(["silence1", "silence2"], silences)
|
self.assertEqual([{"id": "silence2", "state": "expired"}], silences)
|
||||||
|
|
||||||
async def test_delete_silences_with_matchers(self) -> None:
|
|
||||||
matchers = [AlertMatcher(label="alertname", value="alert2")]
|
|
||||||
|
|
||||||
|
async def test_delete_silence_raise_silence_expired(self) -> None:
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
f"http://localhost:{port}", self.fake_cache
|
f"http://localhost:{port}", self.fake_cache
|
||||||
)
|
)
|
||||||
async with aiotools.closing_async(alertmanager):
|
async with aiotools.closing_async(alertmanager):
|
||||||
silences = await alertmanager.delete_silences("fingerprint2", matchers)
|
with self.assertRaises(SilenceExpiredError):
|
||||||
|
await alertmanager.delete_silence("silence2")
|
||||||
|
silences = await alertmanager.get_silences()
|
||||||
|
|
||||||
self.assertEqual(["silence1", "silence2"], silences)
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
{"id": "silence1", "state": "active"},
|
||||||
|
{"id": "silence2", "state": "expired"},
|
||||||
|
],
|
||||||
|
silences,
|
||||||
|
)
|
||||||
|
|
||||||
async def test_delete_silences_with_regex_matchers(self) -> None:
|
async def test_delete_silence_raise_alertmanager_error(self) -> None:
|
||||||
matchers: List[AlertMatcher] = [
|
|
||||||
AlertRegexMatcher(label="alertname", regex=r"alert\d+")
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
silences = await alertmanager.delete_silences("fingerprint2", matchers)
|
|
||||||
|
|
||||||
self.assertEqual(["silence1", "silence2"], silences)
|
|
||||||
|
|
||||||
async def test_delete_silences_raise_missing_label(self) -> None:
|
|
||||||
matchers = [
|
|
||||||
AlertMatcher(label="alertname", value="alert2"),
|
|
||||||
AlertMatcher(label="severity", value="critical"),
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(AlertMismatchError):
|
|
||||||
await alertmanager.delete_silences("fingerprint2", matchers)
|
|
||||||
|
|
||||||
async def test_delete_silences_raise_mismatch_label(self) -> None:
|
|
||||||
matchers = [
|
|
||||||
AlertMatcher(label="alertname", value="alert1"),
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(AlertMismatchError):
|
|
||||||
await alertmanager.delete_silences("fingerprint2", matchers)
|
|
||||||
|
|
||||||
async def test_delete_silences_raise_mismatch_regex_label(self) -> None:
|
|
||||||
matchers: List[AlertMatcher] = [
|
|
||||||
AlertRegexMatcher(label="alertname", regex=r"alert[^\d]+"),
|
|
||||||
]
|
|
||||||
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(AlertMismatchError):
|
|
||||||
await alertmanager.delete_silences("fingerprint2", matchers)
|
|
||||||
|
|
||||||
async def test_delete_silences_raise_silence_not_found(self) -> None:
|
|
||||||
async with FakeAlertmanagerServer() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(SilenceNotFoundError):
|
|
||||||
await alertmanager.delete_silences("fingerprint1", [])
|
|
||||||
|
|
||||||
async def test_delete_silences_raise_alert_not_found(self) -> None:
|
|
||||||
async with FakeAlertmanagerServerWithoutAlert() as fake_alertmanager_server:
|
|
||||||
port = fake_alertmanager_server.port
|
|
||||||
alertmanager = AlertmanagerClient(
|
|
||||||
f"http://localhost:{port}", self.fake_cache
|
|
||||||
)
|
|
||||||
async with aiotools.closing_async(alertmanager):
|
|
||||||
with self.assertRaises(AlertNotFoundError):
|
|
||||||
await alertmanager.delete_silences("fingerprint2", [])
|
|
||||||
|
|
||||||
async def test_delete_silences_raise_alertmanager_error(self) -> None:
|
|
||||||
async with FakeAlertmanagerServerWithErrorDeleteSilence() as fake_alertmanager_server:
|
async with FakeAlertmanagerServerWithErrorDeleteSilence() as fake_alertmanager_server:
|
||||||
port = fake_alertmanager_server.port
|
port = fake_alertmanager_server.port
|
||||||
alertmanager = AlertmanagerClient(
|
alertmanager = AlertmanagerClient(
|
||||||
|
@ -462,7 +541,7 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await alertmanager.get_alert("fingerprint1")
|
await alertmanager.get_alert("fingerprint1")
|
||||||
|
|
||||||
with self.assertRaises(AlertmanagerServerError):
|
with self.assertRaises(AlertmanagerServerError):
|
||||||
await alertmanager.delete_silences("fingerprint2", [])
|
await alertmanager.delete_silence("silence1")
|
||||||
|
|
||||||
async def test_find_alert_happy(self) -> None:
|
async def test_find_alert_happy(self) -> None:
|
||||||
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
|
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
|
||||||
|
@ -474,9 +553,26 @@ class AlertmanagerClientTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
async def test_find_alert_raise_alert_not_found(self) -> None:
|
async def test_find_alert_raise_alert_not_found(self) -> None:
|
||||||
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
|
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
|
||||||
|
|
||||||
|
with self.assertRaises(AlertNotFoundError):
|
||||||
|
alertmanager._find_alert("fingerprint1", [])
|
||||||
|
|
||||||
with self.assertRaises(AlertNotFoundError):
|
with self.assertRaises(AlertNotFoundError):
|
||||||
alertmanager._find_alert("fingerprint2", [{"fingerprint": "fingerprint1"}])
|
alertmanager._find_alert("fingerprint2", [{"fingerprint": "fingerprint1"}])
|
||||||
|
|
||||||
|
async def test_find_silence_happy(self) -> None:
|
||||||
|
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
|
||||||
|
silence = alertmanager._find_silence("silence1", [{"id": "silence1"}])
|
||||||
|
self.assertEqual({"id": "silence1"}, silence)
|
||||||
|
|
||||||
|
async def test_find_silence_raise_silence_not_found(self) -> None:
|
||||||
|
alertmanager = AlertmanagerClient("http://localhost", self.fake_cache)
|
||||||
|
|
||||||
|
with self.assertRaises(SilenceNotFoundError):
|
||||||
|
alertmanager._find_silence("silence1", [])
|
||||||
|
|
||||||
|
with self.assertRaises(SilenceNotFoundError):
|
||||||
|
alertmanager._find_silence("silence2", [{"id": "silence1"}])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
from typing import Dict
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
|
|
||||||
import matrix_alertbot.command
|
|
||||||
import matrix_alertbot.callback
|
import matrix_alertbot.callback
|
||||||
|
import matrix_alertbot.command
|
||||||
from matrix_alertbot.alertmanager import AlertmanagerClient
|
from matrix_alertbot.alertmanager import AlertmanagerClient
|
||||||
from matrix_alertbot.callback import Callbacks
|
from matrix_alertbot.callback import Callbacks
|
||||||
from matrix_alertbot.command import BaseCommand
|
from matrix_alertbot.command import BaseCommand
|
||||||
|
@ -86,10 +87,10 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"help",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
fake_message_event.sender,
|
fake_message_event.sender,
|
||||||
fake_message_event.event_id,
|
fake_message_event.event_id,
|
||||||
|
(),
|
||||||
)
|
)
|
||||||
fake_command.return_value.process.assert_called_once()
|
fake_command.return_value.process.assert_called_once()
|
||||||
|
|
||||||
|
@ -117,13 +118,47 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"help",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
fake_message_event.sender,
|
fake_message_event.sender,
|
||||||
fake_message_event.event_id,
|
fake_message_event.event_id,
|
||||||
|
(),
|
||||||
)
|
)
|
||||||
fake_command.return_value.process.assert_called_once()
|
fake_command.return_value.process.assert_called_once()
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.command.CommandFactory, "create", autospec=True)
|
||||||
|
async def test_ignore_message_sent_by_bot(self, fake_create_command: Mock) -> None:
|
||||||
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
|
||||||
|
fake_message_event = Mock(spec=nio.RoomMessageText)
|
||||||
|
fake_message_event.sender = self.fake_client.user
|
||||||
|
|
||||||
|
# Pretend that we received a text message event
|
||||||
|
await self.callbacks.message(self.fake_room, fake_message_event)
|
||||||
|
|
||||||
|
# Check that we attempted to execute the command
|
||||||
|
fake_create_command.assert_not_called()
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.command.CommandFactory, "create", autospec=True)
|
||||||
|
async def test_ignore_message_sent_on_unauthorized_room(
|
||||||
|
self, fake_create_command: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
|
||||||
|
self.fake_room.room_id = "!unauthorizedroom@example.com"
|
||||||
|
|
||||||
|
fake_message_event = Mock(spec=nio.RoomMessageText)
|
||||||
|
fake_message_event.sender = "@some_other_fake_user:example.com"
|
||||||
|
|
||||||
|
self.assertNotEqual(self.fake_config.room_id, self.fake_room.room_id)
|
||||||
|
|
||||||
|
# Pretend that we received a text message event
|
||||||
|
await self.callbacks.message(self.fake_room, fake_message_event)
|
||||||
|
|
||||||
|
# Check that we attempted to execute the command
|
||||||
|
fake_create_command.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
|
||||||
async def test_message_ack_not_in_reply_with_prefix(
|
async def test_message_ack_not_in_reply_with_prefix(
|
||||||
self, fake_command: Mock
|
self, fake_command: Mock
|
||||||
|
@ -165,15 +200,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
fake_message_event.sender,
|
fake_message_event.sender,
|
||||||
fake_message_event.event_id,
|
fake_message_event.event_id,
|
||||||
"some alert event id",
|
"some alert event id",
|
||||||
|
(),
|
||||||
)
|
)
|
||||||
fake_command.return_value.process.assert_called_once()
|
fake_command.return_value.process.assert_called_once()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
|
||||||
async def test_message_unack_not_in_reply_with_prefix(
|
async def test_message_unack_not_in_reply_with_prefix(
|
||||||
self, fake_command: Mock
|
self, fake_command: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -214,15 +249,15 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
fake_message_event.sender,
|
fake_message_event.sender,
|
||||||
fake_message_event.event_id,
|
fake_message_event.event_id,
|
||||||
"some alert event id",
|
"some alert event id",
|
||||||
|
(),
|
||||||
)
|
)
|
||||||
fake_command.return_value.process.assert_called_once()
|
fake_command.return_value.process.assert_called_once()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
|
||||||
async def test_reaction_to_existing_alert(self, fake_command: Mock) -> None:
|
async def test_reaction_to_existing_alert(self, fake_command: Mock) -> None:
|
||||||
"""Tests the callback for RoomMessageText with the command prefix"""
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
# Tests that the bot process messages in the room that contain a command
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
@ -259,29 +294,21 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack 12h",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
fake_reaction_event.sender,
|
fake_reaction_event.sender,
|
||||||
fake_reaction_event.event_id,
|
fake_reaction_event.event_id,
|
||||||
"some alert event id",
|
"some alert event id",
|
||||||
)
|
)
|
||||||
fake_command.return_value.process.assert_called_once()
|
fake_command.return_value.process.assert_called_once()
|
||||||
self.fake_cache.set.assert_called_once_with(
|
|
||||||
fake_reaction_event.event_id,
|
|
||||||
fake_alert_event.event_id,
|
|
||||||
expire=self.fake_config.cache_expire_time,
|
|
||||||
)
|
|
||||||
self.fake_client.room_get_event.assert_called_once_with(
|
self.fake_client.room_get_event.assert_called_once_with(
|
||||||
self.fake_room.room_id, fake_alert_event.event_id
|
self.fake_room.room_id, fake_alert_event.event_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
|
||||||
async def test_reaction_to_unknown_event(self, fake_command: Mock) -> None:
|
async def test_reaction_to_inexistent_event(self, fake_command: Mock) -> None:
|
||||||
"""Tests the callback for RoomMessageText with the command prefix"""
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
# Tests that the bot process messages in the room that contain a command
|
# Tests that the bot process messages in the room that contain a command
|
||||||
fake_alert_event = Mock(spec=nio.RoomMessageText)
|
fake_alert_event_id = "some alert event id"
|
||||||
fake_alert_event.event_id = "some alert event id"
|
|
||||||
fake_alert_event.sender = self.fake_config.user_id
|
|
||||||
|
|
||||||
fake_reaction_event = Mock(spec=nio.UnknownEvent)
|
fake_reaction_event = Mock(spec=nio.UnknownEvent)
|
||||||
fake_reaction_event.type = "m.reaction"
|
fake_reaction_event.type = "m.reaction"
|
||||||
|
@ -290,7 +317,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_reaction_event.source = {
|
fake_reaction_event.source = {
|
||||||
"content": {
|
"content": {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": fake_alert_event.event_id,
|
"event_id": fake_alert_event_id,
|
||||||
"key": "🤫",
|
"key": "🤫",
|
||||||
"rel_type": "m.annotation",
|
"rel_type": "m.annotation",
|
||||||
}
|
}
|
||||||
|
@ -309,11 +336,11 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_command.assert_not_called()
|
fake_command.assert_not_called()
|
||||||
self.fake_cache.set.assert_not_called()
|
self.fake_cache.set.assert_not_called()
|
||||||
self.fake_client.room_get_event.assert_called_once_with(
|
self.fake_client.room_get_event.assert_called_once_with(
|
||||||
self.fake_room.room_id, fake_alert_event.event_id
|
self.fake_room.room_id, fake_alert_event_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
|
||||||
async def test_reaction_to_event_with_incorrect_sender(
|
async def test_reaction_to_event_not_from_bot_user(
|
||||||
self, fake_command: Mock
|
self, fake_command: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for RoomMessageText with the command prefix"""
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
@ -352,12 +379,11 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_room.room_id, fake_alert_event.event_id
|
self.fake_room.room_id, fake_alert_event.event_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "AckAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
|
||||||
async def test_reaction_unknown(self, fake_command: Mock) -> None:
|
async def test_reaction_unknown(self, fake_command: Mock) -> None:
|
||||||
"""Tests the callback for RoomMessageText with the command prefix"""
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
# Tests that the bot process messages in the room that contain a command
|
# Tests that the bot process messages in the room that contain a command
|
||||||
fake_alert_event = Mock(spec=nio.RoomMessageText)
|
fake_alert_event_id = "some alert event id"
|
||||||
fake_alert_event.event_id = "some alert event id"
|
|
||||||
|
|
||||||
fake_reaction_event = Mock(spec=nio.UnknownEvent)
|
fake_reaction_event = Mock(spec=nio.UnknownEvent)
|
||||||
fake_reaction_event.type = "m.reaction"
|
fake_reaction_event.type = "m.reaction"
|
||||||
|
@ -366,7 +392,7 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_reaction_event.source = {
|
fake_reaction_event.source = {
|
||||||
"content": {
|
"content": {
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"event_id": fake_alert_event.event_id,
|
"event_id": fake_alert_event_id,
|
||||||
"key": "unknown",
|
"key": "unknown",
|
||||||
"rel_type": "m.annotation",
|
"rel_type": "m.annotation",
|
||||||
}
|
}
|
||||||
|
@ -380,17 +406,83 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_command.assert_not_called()
|
fake_command.assert_not_called()
|
||||||
self.fake_client.room_get_event.assert_not_called()
|
self.fake_client.room_get_event.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
|
||||||
|
async def test_ignore_reaction_sent_by_bot_user(self, fake_command: Mock) -> None:
|
||||||
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
fake_alert_event_id = "some alert event id"
|
||||||
|
|
||||||
|
fake_reaction_event = Mock(spec=nio.UnknownEvent)
|
||||||
|
fake_reaction_event.type = "m.reaction"
|
||||||
|
fake_reaction_event.event_id = "some event id"
|
||||||
|
fake_reaction_event.sender = self.fake_client.user
|
||||||
|
fake_reaction_event.source = {
|
||||||
|
"content": {
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": fake_alert_event_id,
|
||||||
|
"key": "unknown",
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pretend that we received a text message event
|
||||||
|
await self.callbacks.unknown(self.fake_room, fake_reaction_event)
|
||||||
|
await self.callbacks._reaction(
|
||||||
|
self.fake_room, fake_reaction_event, fake_alert_event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we attempted to execute the command
|
||||||
|
fake_command.assert_not_called()
|
||||||
|
self.fake_client.room_get_event.assert_not_called()
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.callback, "AckAlertCommand", autospec=True)
|
||||||
|
async def test_ignore_reaction_in_unauthorized_room(
|
||||||
|
self, fake_command: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
self.fake_room.room_id = "!unauthorizedroom@example.com"
|
||||||
|
|
||||||
|
fake_alert_event_id = "some alert event id"
|
||||||
|
|
||||||
|
fake_reaction_event = Mock(spec=nio.UnknownEvent)
|
||||||
|
fake_reaction_event.type = "m.reaction"
|
||||||
|
fake_reaction_event.event_id = "some event id"
|
||||||
|
fake_reaction_event.sender = "@some_other_fake_user:example.com"
|
||||||
|
fake_reaction_event.source = {
|
||||||
|
"content": {
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": fake_alert_event_id,
|
||||||
|
"key": "unknown",
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pretend that we received a text message event
|
||||||
|
await self.callbacks.unknown(self.fake_room, fake_reaction_event)
|
||||||
|
await self.callbacks._reaction(
|
||||||
|
self.fake_room, fake_reaction_event, fake_alert_event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we attempted to execute the command
|
||||||
|
fake_command.assert_not_called()
|
||||||
|
self.fake_client.room_get_event.assert_not_called()
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
|
||||||
async def test_redaction_in_cache(self, fake_command: Mock) -> None:
|
async def test_redaction_in_cache(self, fake_command: Mock) -> None:
|
||||||
"""Tests the callback for RoomMessageText with the command prefix"""
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
# Tests that the bot process messages in the room that contain a command
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
fake_alert_event_id = "some alert event id"
|
||||||
|
|
||||||
fake_redaction_event = Mock(spec=nio.RedactionEvent)
|
fake_redaction_event = Mock(spec=nio.RedactionEvent)
|
||||||
fake_redaction_event.redacts = "some other event id"
|
fake_redaction_event.redacts = "some other event id"
|
||||||
fake_redaction_event.event_id = "some event id"
|
fake_redaction_event.event_id = "some event id"
|
||||||
fake_redaction_event.sender = "@some_other_fake_user:example.com"
|
fake_redaction_event.sender = "@some_other_fake_user:example.com"
|
||||||
|
|
||||||
self.fake_cache.__getitem__.return_value = "some alert event id"
|
fake_cache_dict = {fake_redaction_event.redacts: fake_alert_event_id}
|
||||||
self.fake_cache.__contains__.return_value = True
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
# Pretend that we received a text message event
|
# Pretend that we received a text message event
|
||||||
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
|
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
|
||||||
|
@ -401,18 +493,17 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
fake_redaction_event.sender,
|
fake_redaction_event.sender,
|
||||||
fake_redaction_event.redacts,
|
fake_redaction_event.redacts,
|
||||||
"some alert event id",
|
fake_alert_event_id,
|
||||||
)
|
)
|
||||||
fake_command.return_value.process.assert_called_once()
|
fake_command.return_value.process.assert_called_once()
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(
|
self.fake_cache.__getitem__.assert_called_once_with(
|
||||||
fake_redaction_event.redacts
|
fake_redaction_event.redacts
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "UnackAlertCommand", autospec=True)
|
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
|
||||||
async def test_redaction_not_in_cache(self, fake_command: Mock) -> None:
|
async def test_redaction_not_in_cache(self, fake_command: Mock) -> None:
|
||||||
"""Tests the callback for RoomMessageText with the command prefix"""
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
# Tests that the bot process messages in the room that contain a command
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
@ -421,13 +512,55 @@ class CallbacksTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_redaction_event.event_id = "some event id"
|
fake_redaction_event.event_id = "some event id"
|
||||||
fake_redaction_event.sender = "@some_other_fake_user:example.com"
|
fake_redaction_event.sender = "@some_other_fake_user:example.com"
|
||||||
|
|
||||||
self.fake_cache.__contains__.return_value = False
|
fake_cache_dict: Dict = {}
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
# Pretend that we received a text message event
|
# Pretend that we received a text message event
|
||||||
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
|
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
|
||||||
|
|
||||||
# Check that we attempted to execute the command
|
# Check that we attempted to execute the command
|
||||||
fake_command.assert_not_called()
|
fake_command.assert_not_called()
|
||||||
|
self.fake_cache.__getitem__.assert_called_once_with(
|
||||||
|
fake_redaction_event.redacts
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
|
||||||
|
async def test_ignore_redaction_sent_by_bot_user(self, fake_command: Mock) -> None:
|
||||||
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
fake_redaction_event = Mock(spec=nio.RedactionEvent)
|
||||||
|
fake_redaction_event.sender = self.fake_client.user
|
||||||
|
|
||||||
|
fake_cache_dict: Dict = {}
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
|
# Pretend that we received a text message event
|
||||||
|
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
|
||||||
|
|
||||||
|
# Check that we attempted to execute the command
|
||||||
|
fake_command.assert_not_called()
|
||||||
|
self.fake_cache.__getitem__.assert_not_called()
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.callback, "UnackAlertCommand", autospec=True)
|
||||||
|
async def test_ignore_redaction_in_unauthorized_room(
|
||||||
|
self, fake_command: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Tests the callback for RoomMessageText with the command prefix"""
|
||||||
|
# Tests that the bot process messages in the room that contain a command
|
||||||
|
self.fake_room.room_id = "!unauthorizedroom@example.com"
|
||||||
|
|
||||||
|
fake_redaction_event = Mock(spec=nio.RedactionEvent)
|
||||||
|
fake_redaction_event.sender = "@some_other_fake_user:example.com"
|
||||||
|
|
||||||
|
fake_cache_dict: Dict = {}
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
|
# Pretend that we received a text message event
|
||||||
|
await self.callbacks.redaction(self.fake_room, fake_redaction_event)
|
||||||
|
|
||||||
|
# Check that we attempted to execute the command
|
||||||
|
fake_command.assert_not_called()
|
||||||
|
self.fake_cache.__getitem__.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True)
|
@patch.object(matrix_alertbot.callback.CommandFactory, "create", autospec=True)
|
||||||
async def test_unknown(self, fake_command_create: Mock) -> None:
|
async def test_unknown(self, fake_command_create: Mock) -> None:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import unittest
|
import unittest
|
||||||
from typing import List
|
from typing import Dict, Optional
|
||||||
from unittest.mock import MagicMock, Mock, call, patch
|
from unittest.mock import MagicMock, Mock, call, patch
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
@ -20,13 +20,22 @@ from matrix_alertbot.errors import (
|
||||||
AlertNotFoundError,
|
AlertNotFoundError,
|
||||||
SilenceNotFoundError,
|
SilenceNotFoundError,
|
||||||
)
|
)
|
||||||
from matrix_alertbot.matcher import AlertMatcher, AlertRegexMatcher
|
|
||||||
|
|
||||||
from tests.utils import make_awaitable
|
from tests.utils import make_awaitable
|
||||||
|
|
||||||
|
|
||||||
|
def cache_get_item(key: str) -> str:
|
||||||
|
return {
|
||||||
|
"some alert event id": "fingerprint1",
|
||||||
|
"fingerprint1": "silence1",
|
||||||
|
}[key]
|
||||||
|
|
||||||
|
|
||||||
async def create_silence(
|
async def create_silence(
|
||||||
fingerprint: str, seconds: int, user: str, matchers: List[AlertMatcher]
|
fingerprint: str,
|
||||||
|
user: str,
|
||||||
|
seconds: Optional[int] = None,
|
||||||
|
silence_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if fingerprint == "fingerprint1":
|
if fingerprint == "fingerprint1":
|
||||||
return "silence1"
|
return "silence1"
|
||||||
|
@ -36,7 +45,10 @@ async def create_silence(
|
||||||
|
|
||||||
|
|
||||||
async def create_silence_raise_alertmanager_error(
|
async def create_silence_raise_alertmanager_error(
|
||||||
fingerprint: str, seconds: int, user: str, matchers: List[AlertMatcher]
|
fingerprint: str,
|
||||||
|
user: str,
|
||||||
|
seconds: Optional[int] = None,
|
||||||
|
silence_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if fingerprint == "fingerprint1":
|
if fingerprint == "fingerprint1":
|
||||||
raise AlertmanagerError
|
raise AlertmanagerError
|
||||||
|
@ -44,27 +56,24 @@ async def create_silence_raise_alertmanager_error(
|
||||||
|
|
||||||
|
|
||||||
async def create_silence_raise_alert_not_found_error(
|
async def create_silence_raise_alert_not_found_error(
|
||||||
fingerprint: str, seconds: int, user: str, matchers: List[AlertMatcher]
|
fingerprint: str,
|
||||||
|
user: str,
|
||||||
|
seconds: Optional[int] = None,
|
||||||
|
silence_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if fingerprint == "fingerprint1":
|
if fingerprint == "fingerprint1":
|
||||||
raise AlertNotFoundError
|
raise AlertNotFoundError
|
||||||
return "silence1"
|
return "silence1"
|
||||||
|
|
||||||
|
|
||||||
async def delete_silence_raise_alertmanager_error(
|
async def delete_silence_raise_alertmanager_error(silence_id: str) -> None:
|
||||||
fingerprint: str, matchers: List[AlertMatcher]
|
if silence_id == "silence1":
|
||||||
) -> List[str]:
|
|
||||||
if fingerprint == "fingerprint1":
|
|
||||||
raise AlertmanagerError
|
raise AlertmanagerError
|
||||||
return ["silence1"]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_silence_raise_silence_not_found_error(
|
async def delete_silence_raise_silence_not_found_error(silence_id: str) -> None:
|
||||||
fingerprint: str, matchers: List[AlertMatcher]
|
if silence_id == "silence1":
|
||||||
) -> List[str]:
|
|
||||||
if fingerprint == "fingerprint1":
|
|
||||||
raise SilenceNotFoundError
|
raise SilenceNotFoundError
|
||||||
return ["silence1"]
|
|
||||||
|
|
||||||
|
|
||||||
class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
|
@ -75,15 +84,11 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
# Pretend that attempting to send a message is always successful
|
# Pretend that attempting to send a message is always successful
|
||||||
self.fake_client.room_send.return_value = make_awaitable(None)
|
self.fake_client.room_send.return_value = make_awaitable(None)
|
||||||
|
|
||||||
self.fake_fingerprints = ["fingerprint1", "fingerprint2"]
|
|
||||||
self.fake_silences = ["silence1", "silence2"]
|
|
||||||
|
|
||||||
self.fake_cache = MagicMock(spec=Cache)
|
self.fake_cache = MagicMock(spec=Cache)
|
||||||
self.fake_cache.__getitem__.return_value = self.fake_fingerprints
|
self.fake_cache.__getitem__.side_effect = cache_get_item
|
||||||
self.fake_cache.__contains__.return_value = True
|
self.fake_cache.__contains__.return_value = True
|
||||||
|
|
||||||
self.fake_alertmanager = Mock(spec=AlertmanagerClient)
|
self.fake_alertmanager = Mock(spec=AlertmanagerClient)
|
||||||
self.fake_alertmanager.delete_silences.return_value = self.fake_silences
|
|
||||||
self.fake_alertmanager.create_silence.side_effect = create_silence
|
self.fake_alertmanager.create_silence.side_effect = create_silence
|
||||||
|
|
||||||
# Create a fake room to play with
|
# Create a fake room to play with
|
||||||
|
@ -122,6 +127,27 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
# Check that we attempted to process the command
|
# Check that we attempted to process the command
|
||||||
fake_ack.assert_called_once()
|
fake_ack.assert_called_once()
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.command.AckAlertCommand, "process")
|
||||||
|
async def test_process_ack_with_duration_command(self, fake_ack: Mock) -> None:
|
||||||
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
|
||||||
|
command = CommandFactory.create(
|
||||||
|
"ack 1w 3d",
|
||||||
|
self.fake_client,
|
||||||
|
self.fake_cache,
|
||||||
|
self.fake_alertmanager,
|
||||||
|
self.fake_config,
|
||||||
|
self.fake_room,
|
||||||
|
self.fake_sender,
|
||||||
|
self.fake_event_id,
|
||||||
|
self.fake_alert_event_id,
|
||||||
|
)
|
||||||
|
await command.process()
|
||||||
|
|
||||||
|
# Check that we attempted to process the command
|
||||||
|
fake_ack.assert_called_once()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command.UnackAlertCommand, "process")
|
@patch.object(matrix_alertbot.command.UnackAlertCommand, "process")
|
||||||
async def test_process_unack_command(self, fake_unack: Mock) -> None:
|
async def test_process_unack_command(self, fake_unack: Mock) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
|
@ -185,18 +211,21 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_unknown.assert_called_once()
|
fake_unknown.assert_called_once()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_ack_without_duration_nor_matchers(
|
async def test_ack_without_duration(self, fake_send_text_to_room: Mock) -> None:
|
||||||
self, fake_send_text_to_room: Mock
|
|
||||||
) -> None:
|
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
self.fake_cache.get.side_effect = fake_cache_dict.get
|
||||||
|
|
||||||
command = AckAlertCommand(
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -205,162 +234,64 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.create_silence.assert_has_calls(
|
self.fake_alertmanager.create_silence.assert_called_once_with(
|
||||||
[
|
"fingerprint1", self.fake_sender, None, None
|
||||||
call(fingerprint, 86400, self.fake_sender, [])
|
|
||||||
for fingerprint in self.fake_fingerprints
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_room.room_id,
|
self.fake_room.room_id,
|
||||||
"Created 2 silences with a duration of 1d.",
|
"Created silence with ID silence1.",
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
||||||
self.fake_cache.set.assert_called_once_with(
|
self.fake_cache.get.assert_called_once_with("fingerprint1")
|
||||||
"".join(self.fake_fingerprints) + "86400",
|
self.fake_cache.set.assert_has_calls(
|
||||||
tuple(self.fake_silences),
|
[
|
||||||
expire=86400,
|
call("some event id", "fingerprint1", expire=None),
|
||||||
|
call("fingerprint1", "silence1", expire=None),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_ack_without_duration_and_with_matchers(
|
async def test_ack_with_duration(self, fake_send_text_to_room: Mock) -> None:
|
||||||
self, fake_send_text_to_room: Mock
|
|
||||||
) -> None:
|
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
matchers = [
|
fake_cache_dict = {
|
||||||
AlertMatcher(label="alertname", value="alert1"),
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
AlertRegexMatcher(label="severity", regex="critical"),
|
}
|
||||||
]
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
self.fake_cache.get.side_effect = fake_cache_dict.get
|
||||||
|
|
||||||
command = AckAlertCommand(
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack alertname=alert1 severity=~critical",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
self.fake_alert_event_id,
|
self.fake_alert_event_id,
|
||||||
|
("1w", "3d"),
|
||||||
)
|
)
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.create_silence.assert_has_calls(
|
self.fake_alertmanager.create_silence.assert_called_once_with(
|
||||||
[
|
"fingerprint1", self.fake_sender, 864000, None
|
||||||
call(
|
|
||||||
fingerprint,
|
|
||||||
86400,
|
|
||||||
self.fake_sender,
|
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
for fingerprint in self.fake_fingerprints
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_room.room_id,
|
self.fake_room.room_id,
|
||||||
"Created 2 silences with a duration of 1d.",
|
"Created silence with ID silence1.",
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
||||||
self.fake_cache.set.assert_called_once_with(
|
self.fake_cache.get.assert_called_once_with("fingerprint1")
|
||||||
"".join(self.fake_fingerprints)
|
self.fake_cache.set.assert_has_calls(
|
||||||
+ "86400"
|
|
||||||
+ "".join(str(matcher) for matcher in matchers),
|
|
||||||
tuple(self.fake_silences),
|
|
||||||
expire=86400,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
|
||||||
async def test_ack_with_duration_and_without_matchers(
|
|
||||||
self, fake_send_text_to_room: Mock
|
|
||||||
) -> None:
|
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
|
||||||
|
|
||||||
command = AckAlertCommand(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_cache,
|
|
||||||
self.fake_alertmanager,
|
|
||||||
self.fake_config,
|
|
||||||
"ack 1w 3d",
|
|
||||||
self.fake_room,
|
|
||||||
self.fake_sender,
|
|
||||||
self.fake_event_id,
|
|
||||||
self.fake_alert_event_id,
|
|
||||||
)
|
|
||||||
await command.process()
|
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
|
||||||
self.fake_alertmanager.create_silence.assert_has_calls(
|
|
||||||
[
|
[
|
||||||
call(fingerprint, 864000, self.fake_sender, [])
|
call("some event id", "fingerprint1", expire=864000),
|
||||||
for fingerprint in self.fake_fingerprints
|
call("fingerprint1", "silence1", expire=864000),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_room.room_id,
|
|
||||||
"Created 2 silences with a duration of 1w 3d.",
|
|
||||||
)
|
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
|
||||||
self.fake_cache.set.assert_called_once_with(
|
|
||||||
"".join(self.fake_fingerprints) + "864000",
|
|
||||||
tuple(self.fake_silences),
|
|
||||||
expire=864000,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
|
||||||
async def test_ack_with_duration_and_matchers(
|
|
||||||
self, fake_send_text_to_room: Mock
|
|
||||||
) -> None:
|
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
|
||||||
matchers = [
|
|
||||||
AlertMatcher(label="alertname", value="alert1"),
|
|
||||||
AlertMatcher(label="severity", value="critical"),
|
|
||||||
]
|
|
||||||
|
|
||||||
command = AckAlertCommand(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_cache,
|
|
||||||
self.fake_alertmanager,
|
|
||||||
self.fake_config,
|
|
||||||
"ack 1w 3d alertname=alert1 severity=critical",
|
|
||||||
self.fake_room,
|
|
||||||
self.fake_sender,
|
|
||||||
self.fake_event_id,
|
|
||||||
self.fake_alert_event_id,
|
|
||||||
)
|
|
||||||
await command.process()
|
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
|
||||||
self.fake_alertmanager.create_silence.assert_has_calls(
|
|
||||||
[
|
|
||||||
call(
|
|
||||||
fingerprint,
|
|
||||||
864000,
|
|
||||||
self.fake_sender,
|
|
||||||
matchers,
|
|
||||||
)
|
|
||||||
for fingerprint in self.fake_fingerprints
|
|
||||||
]
|
|
||||||
)
|
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_room.room_id,
|
|
||||||
"Created 2 silences with a duration of 1w 3d.",
|
|
||||||
)
|
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
|
||||||
self.fake_cache.set.assert_called_once_with(
|
|
||||||
"".join(self.fake_fingerprints)
|
|
||||||
+ "864000"
|
|
||||||
+ "".join(str(matcher) for matcher in matchers),
|
|
||||||
tuple(self.fake_silences),
|
|
||||||
expire=864000,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_ack_raise_alertmanager_error(
|
async def test_ack_raise_alertmanager_error(
|
||||||
|
@ -368,13 +299,18 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
self.fake_cache.get.side_effect = fake_cache_dict.get
|
||||||
|
|
||||||
command = AckAlertCommand(
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -387,23 +323,17 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.create_silence.assert_has_calls(
|
self.fake_alertmanager.create_silence.assert_called_once_with(
|
||||||
[
|
"fingerprint1", self.fake_sender, None, None
|
||||||
call(fingerprint, 86400, self.fake_sender, [])
|
|
||||||
for fingerprint in self.fake_fingerprints
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_room.room_id,
|
self.fake_room.room_id,
|
||||||
"Created 1 silences with a duration of 1d.",
|
"Something went wrong with Alertmanager, therefore I couldn't create silence for alert fingerprint fingerprint1.",
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
||||||
self.fake_cache.set.assert_called_once_with(
|
self.fake_cache.get.assert_called_once_with("fingerprint1")
|
||||||
"".join(self.fake_fingerprints) + "86400",
|
self.fake_cache.set.assert_not_called()
|
||||||
("silence1",),
|
|
||||||
expire=86400,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_ack_raise_alert_not_found_error(
|
async def test_ack_raise_alert_not_found_error(
|
||||||
|
@ -411,13 +341,18 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
self.fake_cache.get.side_effect = fake_cache_dict.get
|
||||||
|
|
||||||
command = AckAlertCommand(
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -430,32 +365,17 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.create_silence.assert_has_calls(
|
self.fake_alertmanager.create_silence.assert_called_once_with(
|
||||||
[
|
"fingerprint1", self.fake_sender, None, None
|
||||||
call(fingerprint, 86400, self.fake_sender, [])
|
|
||||||
for fingerprint in self.fake_fingerprints
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_has_calls(
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
[
|
self.fake_client,
|
||||||
call(
|
self.fake_room.room_id,
|
||||||
self.fake_client,
|
"Sorry, I couldn't find alert with fingerprint fingerprint1, therefore I couldn't create the silence.",
|
||||||
self.fake_room.room_id,
|
|
||||||
"Sorry, I couldn't find 1 alerts, therefore I couldn't create their silence.",
|
|
||||||
),
|
|
||||||
call(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_room.room_id,
|
|
||||||
"Created 1 silences with a duration of 1d.",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
||||||
self.fake_cache.set.assert_called_once_with(
|
self.fake_cache.get.assert_called_once_with("fingerprint1")
|
||||||
"".join(self.fake_fingerprints) + "86400",
|
self.fake_cache.set.assert_not_called()
|
||||||
("silence1",),
|
|
||||||
expire=86400,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_ack_with_invalid_duration(
|
async def test_ack_with_invalid_duration(
|
||||||
|
@ -463,17 +383,16 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
|
||||||
command = AckAlertCommand(
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack invalid duration",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
self.fake_alert_event_id,
|
self.fake_alert_event_id,
|
||||||
|
("invalid duration",),
|
||||||
)
|
)
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
@ -486,23 +405,25 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
"I tried really hard, but I can't convert the duration 'invalid duration' to a number of seconds.",
|
"I tried really hard, but I can't convert the duration 'invalid duration' to a number of seconds.",
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_not_called()
|
self.fake_cache.__getitem__.assert_not_called()
|
||||||
|
self.fake_cache.get.assert_not_called()
|
||||||
self.fake_cache.set.assert_not_called()
|
self.fake_cache.set.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_ack_with_event_not_found_in_cache(
|
async def test_ack_with_alert_event_not_found_in_cache(
|
||||||
self, fake_send_text_to_room: Mock
|
self, fake_send_text_to_room: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict: Dict = {}
|
||||||
|
|
||||||
self.fake_cache.__contains__.return_value = False
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
self.fake_cache.get.side_effect = fake_cache_dict.get
|
||||||
|
|
||||||
command = AckAlertCommand(
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"ack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -514,52 +435,71 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.create_silence.assert_not_called()
|
self.fake_alertmanager.create_silence.assert_not_called()
|
||||||
fake_send_text_to_room.assert_not_called()
|
fake_send_text_to_room.assert_not_called()
|
||||||
self.fake_cache.__getitem__.assert_not_called()
|
self.fake_cache.__getitem__.assert_called_once_with("some alert event id")
|
||||||
|
self.fake_cache.get.assert_not_called()
|
||||||
self.fake_cache.set.assert_not_called()
|
self.fake_cache.set.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_unack_without_matchers(self, fake_send_text_to_room: Mock) -> None:
|
async def test_ack_with_silence_in_cache(
|
||||||
|
self, fake_send_text_to_room: Mock
|
||||||
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
"fingerprint1": "silence2",
|
||||||
|
}
|
||||||
|
|
||||||
command = UnackAlertCommand(
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
self.fake_cache.get.side_effect = fake_cache_dict.get
|
||||||
|
|
||||||
|
command = AckAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
self.fake_alert_event_id,
|
self.fake_alert_event_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.delete_silences.assert_has_calls(
|
self.fake_alertmanager.create_silence.assert_called_once_with(
|
||||||
[call(fingerprint, []) for fingerprint in self.fake_fingerprints]
|
"fingerprint1", self.fake_sender, None, "silence2"
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_with(
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
self.fake_client, self.fake_room.room_id, "Removed 4 silences."
|
self.fake_client,
|
||||||
|
self.fake_room.room_id,
|
||||||
|
"Created silence with ID silence1.",
|
||||||
|
)
|
||||||
|
self.fake_cache.__getitem__.assert_called_once_with("some alert event id")
|
||||||
|
self.fake_cache.get.assert_called_once_with("fingerprint1")
|
||||||
|
self.fake_cache.set.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(self.fake_event_id, "fingerprint1", expire=None),
|
||||||
|
call("fingerprint1", "silence1", expire=None),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_unack_with_matchers(self, fake_send_text_to_room: Mock) -> None:
|
async def test_unack(self, fake_send_text_to_room: Mock) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
"fingerprint1": "silence1",
|
||||||
|
}
|
||||||
|
|
||||||
matchers = [
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
AlertMatcher(label="alertname", value="alert1"),
|
|
||||||
AlertRegexMatcher(label="severity", regex="critical"),
|
|
||||||
]
|
|
||||||
|
|
||||||
command = UnackAlertCommand(
|
command = UnackAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack alertname=alert1 severity=~critical",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -568,13 +508,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.delete_silences.assert_has_calls(
|
self.fake_alertmanager.delete_silence.assert_called_once_with("silence1")
|
||||||
[call(fingerprint, matchers) for fingerprint in self.fake_fingerprints]
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
|
self.fake_client,
|
||||||
|
self.fake_room.room_id,
|
||||||
|
"Removed silence with ID silence1.",
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_with(
|
self.fake_cache.__getitem__.assert_has_calls(
|
||||||
self.fake_client, self.fake_room.room_id, "Removed 4 silences."
|
[call(self.fake_alert_event_id), call("fingerprint1")]
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_unack_silence_raise_alertmanager_error(
|
async def test_unack_silence_raise_alertmanager_error(
|
||||||
|
@ -582,32 +524,39 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
"fingerprint1": "silence1",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
command = UnackAlertCommand(
|
command = UnackAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
self.fake_alert_event_id,
|
self.fake_alert_event_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fake_alertmanager.delete_silences.side_effect = (
|
self.fake_alertmanager.delete_silence.side_effect = (
|
||||||
delete_silence_raise_alertmanager_error
|
delete_silence_raise_alertmanager_error
|
||||||
)
|
)
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.delete_silences.assert_has_calls(
|
self.fake_alertmanager.delete_silence.assert_called_once_with("silence1")
|
||||||
[call(fingerprint, []) for fingerprint in self.fake_fingerprints]
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
|
self.fake_client,
|
||||||
|
self.fake_room.room_id,
|
||||||
|
"Something went wrong with Alertmanager, therefore I couldn't delete silence for alert fingerprint fingerprint1.",
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_with(
|
self.fake_cache.__getitem__.assert_has_calls(
|
||||||
self.fake_client, self.fake_room.room_id, "Removed 1 silences."
|
[call(self.fake_alert_event_id), call("fingerprint1")]
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_unack_raise_silence_not_found_error(
|
async def test_unack_raise_silence_not_found_error(
|
||||||
|
@ -615,43 +564,39 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {
|
||||||
|
self.fake_alert_event_id: "fingerprint1",
|
||||||
|
"fingerprint1": "silence1",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
command = UnackAlertCommand(
|
command = UnackAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
self.fake_alert_event_id,
|
self.fake_alert_event_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fake_alertmanager.delete_silences.side_effect = (
|
self.fake_alertmanager.delete_silence.side_effect = (
|
||||||
delete_silence_raise_silence_not_found_error
|
delete_silence_raise_silence_not_found_error
|
||||||
)
|
)
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.delete_silences.assert_has_calls(
|
self.fake_alertmanager.delete_silence.assert_called_once_with("silence1")
|
||||||
[call(fingerprint, []) for fingerprint in self.fake_fingerprints]
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
|
self.fake_client,
|
||||||
|
self.fake_room.room_id,
|
||||||
|
"Sorry, I couldn't find alert with fingerprint fingerprint1, therefore I couldn't remove its silence.",
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_has_calls(
|
self.fake_cache.__getitem__.assert_has_calls(
|
||||||
[
|
[call(self.fake_alert_event_id), call("fingerprint1")]
|
||||||
call(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_room.room_id,
|
|
||||||
"Sorry, I couldn't find 1 alerts, therefore I couldn't remove their silences.",
|
|
||||||
),
|
|
||||||
call(
|
|
||||||
self.fake_client,
|
|
||||||
self.fake_room.room_id,
|
|
||||||
"Removed 1 silences.",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_unack_with_event_not_found_in_cache(
|
async def test_unack_with_event_not_found_in_cache(
|
||||||
|
@ -659,15 +604,15 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests the callback for InviteMemberEvents"""
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
# Tests that the bot attempts to join a room after being invited to it
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict: Dict = {}
|
||||||
|
|
||||||
self.fake_cache.__contains__.return_value = False
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
command = UnackAlertCommand(
|
command = UnackAlertCommand(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"unack",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -677,9 +622,39 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
await command.process()
|
await command.process()
|
||||||
|
|
||||||
# Check that we attempted to create silences
|
# Check that we attempted to create silences
|
||||||
self.fake_alertmanager.create_silence.assert_not_called()
|
self.fake_alertmanager.delete_silence.assert_not_called()
|
||||||
fake_send_text_to_room.assert_not_called()
|
fake_send_text_to_room.assert_not_called()
|
||||||
self.fake_cache.__getitem__.assert_not_called()
|
self.fake_cache.__getitem__.assert_called_once_with(self.fake_alert_event_id)
|
||||||
|
|
||||||
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
|
async def test_unack_with_silence_not_found_in_cache(
|
||||||
|
self, fake_send_text_to_room: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Tests the callback for InviteMemberEvents"""
|
||||||
|
# Tests that the bot attempts to join a room after being invited to it
|
||||||
|
fake_cache_dict = {self.fake_alert_event_id: "fingerprint1"}
|
||||||
|
|
||||||
|
self.fake_cache.__getitem__.side_effect = fake_cache_dict.__getitem__
|
||||||
|
|
||||||
|
command = UnackAlertCommand(
|
||||||
|
self.fake_client,
|
||||||
|
self.fake_cache,
|
||||||
|
self.fake_alertmanager,
|
||||||
|
self.fake_config,
|
||||||
|
self.fake_room,
|
||||||
|
self.fake_sender,
|
||||||
|
self.fake_event_id,
|
||||||
|
self.fake_alert_event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await command.process()
|
||||||
|
|
||||||
|
# Check that we attempted to create silences
|
||||||
|
self.fake_alertmanager.delete_silence.assert_not_called()
|
||||||
|
fake_send_text_to_room.assert_not_called()
|
||||||
|
self.fake_cache.__getitem__.assert_has_calls(
|
||||||
|
[call(self.fake_alert_event_id), call("fingerprint1")]
|
||||||
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
@patch.object(matrix_alertbot.command, "send_text_to_room")
|
||||||
async def test_help_without_topic(self, fake_send_text_to_room: Mock) -> None:
|
async def test_help_without_topic(self, fake_send_text_to_room: Mock) -> None:
|
||||||
|
@ -691,7 +666,6 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"help",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -714,10 +688,10 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"help rules",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
("rules",),
|
||||||
)
|
)
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
@ -737,10 +711,10 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"help commands",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
("commands",),
|
||||||
)
|
)
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
@ -760,10 +734,10 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"help unknown",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
("unknown",),
|
||||||
)
|
)
|
||||||
|
|
||||||
await command.process()
|
await command.process()
|
||||||
|
@ -783,7 +757,6 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
self.fake_cache,
|
self.fake_cache,
|
||||||
self.fake_alertmanager,
|
self.fake_alertmanager,
|
||||||
self.fake_config,
|
self.fake_config,
|
||||||
"",
|
|
||||||
self.fake_room,
|
self.fake_room,
|
||||||
self.fake_sender,
|
self.fake_sender,
|
||||||
self.fake_event_id,
|
self.fake_event_id,
|
||||||
|
@ -795,7 +768,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
fake_send_text_to_room.assert_called_once_with(
|
||||||
self.fake_client,
|
self.fake_client,
|
||||||
self.fake_room.room_id,
|
self.fake_room.room_id,
|
||||||
"Unknown command ''. Try the 'help' command for more information.",
|
"Unknown command. Try the 'help' command for more information.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
import aiohttp.test_utils
|
import aiohttp.test_utils
|
||||||
import nio
|
import nio
|
||||||
|
@ -62,26 +62,35 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
||||||
return webhook.app
|
return webhook.app
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||||
async def test_post_alert(self, fake_send_text_to_room: Mock) -> None:
|
async def test_post_alerts(self, fake_send_text_to_room: Mock) -> None:
|
||||||
data = self.fake_alerts
|
data = self.fake_alerts
|
||||||
async with self.client.request("POST", "/alerts", json=data) as response:
|
async with self.client.request("POST", "/alerts", json=data) as response:
|
||||||
self.assertEqual(200, response.status)
|
self.assertEqual(200, response.status)
|
||||||
fake_send_text_to_room.assert_called_once_with(
|
fake_send_text_to_room.assert_has_calls(
|
||||||
self.fake_client,
|
[
|
||||||
self.fake_config.room_id,
|
call(
|
||||||
"[🔥 CRITICAL] alert1: some description1\n"
|
self.fake_client,
|
||||||
"[🥦 RESOLVED] alert2: some description2",
|
self.fake_config.room_id,
|
||||||
"<font color='#dc3545'><b>[🔥 CRITICAL]</b></font> "
|
"[🔥 CRITICAL] alert1: some description1",
|
||||||
"<a href='http://example.com/alert1'>alert1</a> (job1)<br/>"
|
"<font color='#dc3545'><b>[🔥 CRITICAL]</b></font> "
|
||||||
"some description1<br/>\n"
|
"<a href='http://example.com/alert1'>alert1</a> (job1)<br/>"
|
||||||
"<font color='#33cc33'><b>[🥦 RESOLVED]</b></font> "
|
"some description1",
|
||||||
"<a href='http://example.com/alert2'>alert2</a> (job2)<br/>"
|
notice=False,
|
||||||
"some description2",
|
),
|
||||||
notice=False,
|
call(
|
||||||
|
self.fake_client,
|
||||||
|
self.fake_config.room_id,
|
||||||
|
"[🥦 RESOLVED] alert2: some description2",
|
||||||
|
"<font color='#33cc33'><b>[🥦 RESOLVED]</b></font> "
|
||||||
|
"<a href='http://example.com/alert2'>alert2</a> (job2)<br/>"
|
||||||
|
"some description2",
|
||||||
|
notice=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||||
async def test_post_alert_with_empty_data(
|
async def test_post_alerts_with_empty_data(
|
||||||
self, fake_send_text_to_room: Mock
|
self, fake_send_text_to_room: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
async with self.client.request("POST", "/alerts", json={}) as response:
|
async with self.client.request("POST", "/alerts", json={}) as response:
|
||||||
|
@ -91,9 +100,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
||||||
fake_send_text_to_room.assert_not_called()
|
fake_send_text_to_room.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||||
async def test_post_alert_with_empty_alerts(
|
async def test_post_empty_alerts(self, fake_send_text_to_room: Mock) -> None:
|
||||||
self, fake_send_text_to_room: Mock
|
|
||||||
) -> None:
|
|
||||||
data: Dict = {"alerts": []}
|
data: Dict = {"alerts": []}
|
||||||
async with self.client.request("POST", "/alerts", json=data) as response:
|
async with self.client.request("POST", "/alerts", json=data) as response:
|
||||||
self.assertEqual(400, response.status)
|
self.assertEqual(400, response.status)
|
||||||
|
@ -102,9 +109,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
||||||
fake_send_text_to_room.assert_not_called()
|
fake_send_text_to_room.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||||
async def test_post_alert_with_invalid_alerts(
|
async def test_post_invalid_alerts(self, fake_send_text_to_room: Mock) -> None:
|
||||||
self, fake_send_text_to_room: Mock
|
|
||||||
) -> None:
|
|
||||||
data = {"alerts": "invalid"}
|
data = {"alerts": "invalid"}
|
||||||
async with self.client.request("POST", "/alerts", json=data) as response:
|
async with self.client.request("POST", "/alerts", json=data) as response:
|
||||||
self.assertEqual(400, response.status)
|
self.assertEqual(400, response.status)
|
||||||
|
@ -113,7 +118,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
||||||
fake_send_text_to_room.assert_not_called()
|
fake_send_text_to_room.assert_not_called()
|
||||||
|
|
||||||
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
@patch.object(matrix_alertbot.webhook, "send_text_to_room")
|
||||||
async def test_post_alert_with_empty_items(
|
async def test_post_alerts_with_empty_items(
|
||||||
self, fake_send_text_to_room: Mock
|
self, fake_send_text_to_room: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
data: Dict = {"alerts": [{}]}
|
data: Dict = {"alerts": [{}]}
|
||||||
|
@ -128,7 +133,7 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
||||||
"send_text_to_room",
|
"send_text_to_room",
|
||||||
side_effect=send_text_to_room_raise_error,
|
side_effect=send_text_to_room_raise_error,
|
||||||
)
|
)
|
||||||
async def test_post_alert_with_send_error(
|
async def test_post_alerts_raise_send_error(
|
||||||
self, fake_send_text_to_room: Mock
|
self, fake_send_text_to_room: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
data = self.fake_alerts
|
data = self.fake_alerts
|
||||||
|
@ -136,7 +141,8 @@ class WebhookApplicationTestCase(aiohttp.test_utils.AioHTTPTestCase):
|
||||||
self.assertEqual(500, response.status)
|
self.assertEqual(500, response.status)
|
||||||
error_msg = await response.text()
|
error_msg = await response.text()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"An error occured when sending alerts to Matrix room.", error_msg
|
"An error occured when sending alert with fingerprint 'fingerprint1' to Matrix room.",
|
||||||
|
error_msg,
|
||||||
)
|
)
|
||||||
fake_send_text_to_room.assert_called_once()
|
fake_send_text_to_room.assert_called_once()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue