commit
00d7e8679b
30 changed files with 1661 additions and 291 deletions
|
|
@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v1.2.35 (2023-06-01)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix a bug with permissions for telegram user settings by @alexintech ([#2075](https://github.com/grafana/oncall/pull/2075))
|
||||
- Fix orphaned messages in Slack by @vadimkerr ([#2023](https://github.com/grafana/oncall/pull/2023))
|
||||
- Fix duplicated slack shift-changed notifications ([#2080](https://github.com/grafana/oncall/pull/2080))
|
||||
|
||||
## v1.2.34 (2023-05-31)
|
||||
|
||||
### Added
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -16,6 +16,7 @@ ENGINE_PROFILE = engine
|
|||
UI_PROFILE = oncall_ui
|
||||
REDIS_PROFILE = redis
|
||||
RABBITMQ_PROFILE = rabbitmq
|
||||
PROMETHEUS_PROFILE = prometheus
|
||||
GRAFANA_PROFILE = grafana
|
||||
|
||||
DEV_ENV_DIR = ./dev
|
||||
|
|
|
|||
1
dev/.gitignore
vendored
1
dev/.gitignore
vendored
|
|
@ -7,4 +7,5 @@
|
|||
!.env.postgres.dev
|
||||
!.env.sqlite.dev
|
||||
!add_env_var.sh
|
||||
!prometheus.yml
|
||||
!README.md
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ make start COMPOSE_PROFILES=postgres,engine,grafana,rabbitmq
|
|||
The possible profiles values are:
|
||||
|
||||
- `grafana`
|
||||
- `prometheus`
|
||||
- `engine`
|
||||
- `oncall_ui`
|
||||
- `redis`
|
||||
|
|
@ -138,6 +139,13 @@ license_text = <content-of-the-license-jwt-that-you-downloaded>
|
|||
|
||||
(_Note_: you may need to restart your `grafana` container after modifying its configuration)
|
||||
|
||||
### Enabling OnCall prometheus exporter for local development
|
||||
|
||||
Add `prometheus` to your `COMPOSE_PROFILES` and set `FEATURE_PROMETHEUS_EXPORTER_ENABLED=True` in your
|
||||
`dev/.env.dev` file. You may need to restart your `grafana` container to make sure the new datasource
|
||||
is added (or add it manually using the UI; Prometheus will be running in `host.docker.internal:9090`
|
||||
by default, using default settings).
|
||||
|
||||
### Django Silk Profiling
|
||||
|
||||
In order to setup [`django-silk`](https://github.com/jazzband/django-silk) for local profiling, perform the following
|
||||
|
|
|
|||
8
dev/prometheus.yml
Normal file
8
dev/prometheus.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: prometheus
|
||||
static_configs:
|
||||
- targets: ["host.docker.internal:8080"]
|
||||
|
|
@ -270,6 +270,17 @@ services:
|
|||
profiles:
|
||||
- postgres
|
||||
|
||||
prometheus:
|
||||
container_name: prometheus
|
||||
labels: *oncall-labels
|
||||
image: prom/prometheus
|
||||
volumes:
|
||||
- ./dev/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
profiles:
|
||||
- prometheus
|
||||
|
||||
grafana:
|
||||
container_name: grafana
|
||||
labels: *oncall-labels
|
||||
|
|
@ -294,6 +305,7 @@ services:
|
|||
volumes:
|
||||
- grafanadata_dev:/var/lib/grafana
|
||||
- ./grafana-plugin:/var/lib/grafana/plugins/grafana-plugin
|
||||
- ./provisioning:/etc/grafana/provisioning
|
||||
- ${GRAFANA_DEV_PROVISIONING:-/dev/null}:/etc/grafana/grafana.ini
|
||||
depends_on:
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"name": ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep").routing_uid(),
|
||||
"text": "Unattach",
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"emoji": True,
|
||||
},
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step(
|
||||
"distribute_alerts",
|
||||
"AcknowledgeGroupStep",
|
||||
|
|
@ -196,7 +196,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"emoji": True,
|
||||
},
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step(
|
||||
"distribute_alerts",
|
||||
"UnAcknowledgeGroupStep",
|
||||
|
|
@ -208,7 +208,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"text": {"type": "plain_text", "text": "Resolve", "emoji": True},
|
||||
"type": "button",
|
||||
"style": "primary",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep").routing_uid(),
|
||||
},
|
||||
)
|
||||
|
|
@ -221,7 +221,10 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
|
||||
if not self.alert_group.silenced:
|
||||
silence_options = [
|
||||
{"text": {"type": "plain_text", "text": text, "emoji": True}, "value": str(value)}
|
||||
{
|
||||
"text": {"type": "plain_text", "text": text, "emoji": True},
|
||||
"value": self._alert_group_action_value(delay=value),
|
||||
}
|
||||
for value, text in AlertGroup.SILENCE_DELAY_OPTIONS
|
||||
]
|
||||
buttons.append(
|
||||
|
|
@ -230,7 +233,6 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"type": "static_select",
|
||||
"options": silence_options,
|
||||
"action_id": ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep").routing_uid(),
|
||||
# "value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
|
@ -238,7 +240,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
{
|
||||
"text": {"type": "plain_text", "text": "Unsilence", "emoji": True},
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep").routing_uid(),
|
||||
},
|
||||
)
|
||||
|
|
@ -247,12 +249,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"text": {"type": "plain_text", "text": "Attach to ...", "emoji": True},
|
||||
"type": "button",
|
||||
"action_id": ScenarioStep.get_step("distribute_alerts", "SelectAttachGroupStep").routing_uid(),
|
||||
"value": json.dumps(
|
||||
{
|
||||
"alert_group_pk": self.alert_group.pk,
|
||||
"organization_id": self.alert_group.channel.organization_id,
|
||||
}
|
||||
),
|
||||
"value": self._alert_group_action_value(),
|
||||
}
|
||||
buttons.append(attach_button)
|
||||
else:
|
||||
|
|
@ -260,7 +257,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
{
|
||||
"text": {"type": "plain_text", "text": "Unresolve", "emoji": True},
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep").routing_uid(),
|
||||
},
|
||||
)
|
||||
|
|
@ -270,12 +267,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
{
|
||||
"text": {"type": "plain_text", "text": ":mag: Format Alert", "emoji": True},
|
||||
"type": "button",
|
||||
"value": json.dumps(
|
||||
{
|
||||
"alert_group_pk": str(self.alert_group.pk),
|
||||
"organization_id": self.alert_group.channel.organization_id,
|
||||
}
|
||||
),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step(
|
||||
"alertgroup_appearance", "OpenAlertAppearanceDialogStep"
|
||||
).routing_uid(),
|
||||
|
|
@ -292,13 +284,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
},
|
||||
"type": "button",
|
||||
"action_id": ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep").routing_uid(),
|
||||
"value": json.dumps(
|
||||
{
|
||||
"resolution_note_window_action": "edit",
|
||||
"alert_group_pk": self.alert_group.pk,
|
||||
"organization_id": self.alert_group.channel.organization_id,
|
||||
}
|
||||
),
|
||||
"value": self._alert_group_action_value(resolution_note_window_action="edit"),
|
||||
}
|
||||
if resolution_notes_count == 0:
|
||||
resolution_notes_button["style"] = "primary"
|
||||
|
|
@ -322,7 +308,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
"text": {"type": "plain_text", "text": "Resolve", "emoji": True},
|
||||
"type": "button",
|
||||
"style": "primary",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(),
|
||||
"action_id": ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep").routing_uid(),
|
||||
},
|
||||
)
|
||||
|
|
@ -339,13 +325,11 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
invitee_name = invitation.invitee.get_username_with_slack_verbal()
|
||||
buttons.append(
|
||||
{
|
||||
"name": "{}_{}".format(
|
||||
ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess").routing_uid(), invitation.pk
|
||||
),
|
||||
"name": ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess").routing_uid(),
|
||||
"text": "Stop inviting {}".format(invitee_name),
|
||||
"type": "button",
|
||||
"style": "primary",
|
||||
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
|
||||
"value": self._alert_group_action_value(invitation_id=invitation.pk),
|
||||
},
|
||||
)
|
||||
return [
|
||||
|
|
@ -359,6 +343,13 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
def _get_select_user_element(
|
||||
self, action_id, multi_select=False, initial_user=None, initial_users_list=None, text=None
|
||||
):
|
||||
def get_action_value(user_id):
|
||||
"""
|
||||
In contrast to other buttons and select menus, self._alert_group_action_value is not used here.
|
||||
It's because there could be a lot of users, and we don't want to increase the payload size too much.
|
||||
"""
|
||||
return json.dumps({"user_id": user_id})
|
||||
|
||||
MAX_STATIC_SELECT_OPTIONS = 100
|
||||
|
||||
if not text:
|
||||
|
|
@ -382,7 +373,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
user_verbal = f"{user.get_username_with_slack_verbal()}"
|
||||
if len(user_verbal) > 75:
|
||||
user_verbal = user_verbal[:72] + "..."
|
||||
option = {"text": {"type": "plain_text", "text": user_verbal}, "value": json.dumps({"user_id": user.pk})}
|
||||
option = {"text": {"type": "plain_text", "text": user_verbal}, "value": get_action_value(user.pk)}
|
||||
options.append(option)
|
||||
|
||||
if users_count > MAX_STATIC_SELECT_OPTIONS:
|
||||
|
|
@ -397,7 +388,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
elif users_count == 0: # strange case when there are no users to select
|
||||
option = {
|
||||
"text": {"type": "plain_text", "text": "No users to select"},
|
||||
"value": json.dumps({"user_id": None}),
|
||||
"value": get_action_value(None),
|
||||
}
|
||||
options.append(option)
|
||||
element["options"] = options
|
||||
|
|
@ -413,7 +404,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
user_verbal = f"{user.get_username_with_slack_verbal()}"
|
||||
option = {
|
||||
"text": {"type": "plain_text", "text": user_verbal},
|
||||
"value": json.dumps({"user_id": user.pk}),
|
||||
"value": get_action_value(user.pk),
|
||||
}
|
||||
initial_options.append(option)
|
||||
element["initial_options"] = initial_options
|
||||
|
|
@ -421,8 +412,22 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
|
|||
user_verbal = f"{initial_user.get_username_with_slack_verbal()}"
|
||||
initial_option = {
|
||||
"text": {"type": "plain_text", "text": user_verbal},
|
||||
"value": json.dumps({"user_id": initial_user.pk}),
|
||||
"value": get_action_value(initial_user.pk),
|
||||
}
|
||||
element["initial_option"] = initial_option
|
||||
|
||||
return element
|
||||
|
||||
def _alert_group_action_value(self, **kwargs):
|
||||
"""
|
||||
Store organization and alert group IDs in Slack button or select menu values.
|
||||
alert_group_pk is used in apps.slack.scenarios.step_mixins.AlertGroupActionsMixin to get the right alert group
|
||||
when handling AG actions in Slack.
|
||||
"""
|
||||
|
||||
data = {
|
||||
"organization_id": self.alert_group.channel.organization_id,
|
||||
"alert_group_pk": self.alert_group.pk,
|
||||
**kwargs,
|
||||
}
|
||||
return json.dumps(data) # Slack block elements allow to pass value as string only (max 2000 chars)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import datetime
|
|||
import json
|
||||
from copy import copy
|
||||
|
||||
import icalendar
|
||||
from django.apps import apps
|
||||
from django.utils import timezone
|
||||
|
||||
|
|
@ -12,7 +11,6 @@ from apps.schedules.ical_utils import (
|
|||
event_start_end_all_day_with_respect_to_type,
|
||||
get_icalendar_tz_or_utc,
|
||||
get_usernames_from_ical_event,
|
||||
is_icals_equal,
|
||||
memoized_users_in_ical,
|
||||
)
|
||||
from apps.slack.scenarios import scenario_step
|
||||
|
|
@ -191,8 +189,6 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
|
||||
MIN_DAYS_TO_LOOKUP_FOR_THE_END_OF_EVENT = 3
|
||||
|
||||
ical_changed = False
|
||||
|
||||
now = timezone.datetime.now(timezone.utc)
|
||||
# get list of iCalendars from current iCal files. If there is more than one calendar, primary calendar will always
|
||||
# be the first
|
||||
|
|
@ -240,60 +236,15 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
for item in drop:
|
||||
current_shifts.pop(item)
|
||||
|
||||
is_prev_ical_diff = False
|
||||
prev_overrides_priority = 0
|
||||
prev_shifts = {}
|
||||
prev_users = {}
|
||||
# compare events from prev and current shifts
|
||||
prev_shifts = json.loads(schedule.current_shifts) if not schedule.empty_oncall else {}
|
||||
# convert datetimes which was dumped to str back to datetime to calculate shift diff correct
|
||||
str_format = "%Y-%m-%d %X%z"
|
||||
for prev_shift in prev_shifts.values():
|
||||
prev_shift["start"] = datetime.datetime.strptime(prev_shift["start"], str_format)
|
||||
prev_shift["end"] = datetime.datetime.strptime(prev_shift["end"], str_format)
|
||||
|
||||
# Get list of tuples with prev and current ical file for each calendar. If there is more than one calendar, primary
|
||||
# calendar will be the first.
|
||||
# example result for ical calendar:
|
||||
# [(prev_ical_file_primary, current_ical_file_primary), (prev_ical_file_overrides, current_ical_file_overrides)]
|
||||
# example result for calendar with custom events:
|
||||
# [(prev_ical_file, current_ical_file)]
|
||||
prev_and_current_ical_files = schedule.get_prev_and_current_ical_files()
|
||||
|
||||
for prev_ical_file, current_ical_file in prev_and_current_ical_files:
|
||||
if prev_ical_file and (not current_ical_file or not is_icals_equal(current_ical_file, prev_ical_file)):
|
||||
task_logger.info(f"ical files are different")
|
||||
# If icals are not equal then compare current_events from them
|
||||
is_prev_ical_diff = True
|
||||
prev_calendar = icalendar.Calendar.from_ical(prev_ical_file)
|
||||
|
||||
prev_shifts_result, prev_users_result = get_current_shifts_from_ical(
|
||||
prev_calendar,
|
||||
schedule,
|
||||
prev_overrides_priority,
|
||||
)
|
||||
if prev_overrides_priority == 0 and prev_shifts_result:
|
||||
prev_overrides_priority = max([prev_shifts_result[uid]["priority"] for uid in prev_shifts_result]) + 1
|
||||
|
||||
prev_shifts.update(prev_shifts_result)
|
||||
prev_users.update(prev_users_result)
|
||||
|
||||
recalculate_shifts_with_respect_to_priority(prev_shifts, prev_users)
|
||||
|
||||
if is_prev_ical_diff:
|
||||
# drop events that don't intersection with current time
|
||||
drop = []
|
||||
for uid, prev_shift in prev_shifts.items():
|
||||
if not prev_shift["start"] < now < prev_shift["end"]:
|
||||
drop.append(uid)
|
||||
for item in drop:
|
||||
prev_shifts.pop(item)
|
||||
|
||||
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
|
||||
|
||||
else:
|
||||
# Else comparing events from prev and current shifts
|
||||
prev_shifts = json.loads(schedule.current_shifts) if not schedule.empty_oncall else {}
|
||||
# convert datetimes which was dumped to str back to datetime to calculate shift diff correct
|
||||
str_format = "%Y-%m-%d %X%z"
|
||||
for prev_shift in prev_shifts.values():
|
||||
prev_shift["start"] = datetime.datetime.strptime(prev_shift["start"], str_format)
|
||||
prev_shift["end"] = datetime.datetime.strptime(prev_shift["end"], str_format)
|
||||
|
||||
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
|
||||
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
|
||||
|
||||
if shift_changed:
|
||||
task_logger.info(f"shifts_changed: {diff_uids}")
|
||||
|
|
@ -370,11 +321,6 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
|
||||
if schedule.notify_oncall_shift_freq != OnCallSchedule.NotifyOnCallShiftFreq.NEVER:
|
||||
try:
|
||||
if ical_changed:
|
||||
slack_client.api_call(
|
||||
"chat.postMessage", channel=schedule.channel, text=f"Schedule {schedule.name} was changed"
|
||||
)
|
||||
|
||||
slack_client.api_call(
|
||||
"chat.postMessage",
|
||||
channel=schedule.channel,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import json
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import icalendar
|
||||
import pytest
|
||||
import pytz
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.alerts.tasks.notify_ical_schedule_shift import notify_ical_schedule_shift
|
||||
from apps.alerts.tasks.notify_ical_schedule_shift import get_current_shifts_from_ical, notify_ical_schedule_shift
|
||||
from apps.schedules.ical_utils import memoized_users_in_ical
|
||||
from apps.schedules.models import OnCallScheduleICal
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar, OnCallScheduleICal
|
||||
|
||||
ICAL_DATA = """
|
||||
BEGIN:VCALENDAR
|
||||
|
|
@ -105,3 +108,199 @@ def test_next_shift_notification_long_shifts(
|
|||
notification = slack_blocks[0]["text"]["text"]
|
||||
assert "*New on-call shift:*\nuser2" in notification
|
||||
assert "*Next on-call shift:*\nuser1" in notification
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_overrides_changes_no_current_no_triggering_notification(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1")
|
||||
# clear users pks <-> organization cache (persisting between tests)
|
||||
memoized_users_in_ical.cache_clear()
|
||||
|
||||
ical_before = textwrap.dedent(
|
||||
"""
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20230101T020000
|
||||
DTEND:20230101T170000
|
||||
DTSTAMP:20230101T000000
|
||||
UID:id1@google.com
|
||||
CREATED:20230101T000000
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20230101T000000
|
||||
LOCATION:
|
||||
SEQUENCE:1
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:user1
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
)
|
||||
|
||||
# event outside current time is changed
|
||||
ical_after = textwrap.dedent(
|
||||
"""
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20230101T020000
|
||||
DTEND:20230101T210000
|
||||
DTSTAMP:20230101T000000
|
||||
UID:id1@google.com
|
||||
CREATED:20230101T000000
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20230101T000000
|
||||
LOCATION:
|
||||
SEQUENCE:2
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:user1
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
END:VCALENDAR"""
|
||||
)
|
||||
|
||||
schedule = make_schedule(
|
||||
organization,
|
||||
schedule_class=OnCallScheduleCalendar,
|
||||
name="test_schedule",
|
||||
channel="channel",
|
||||
prev_ical_file_overrides=ical_before,
|
||||
cached_ical_file_overrides=ical_after,
|
||||
)
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - timezone.timedelta(days=7)
|
||||
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": timezone.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
on_call_shift.schedules.add(schedule)
|
||||
|
||||
# setup current shifts before checking/triggering for notifications
|
||||
calendar = icalendar.Calendar.from_ical(schedule._ical_file_primary)
|
||||
current_shifts, _ = get_current_shifts_from_ical(calendar, schedule, 0)
|
||||
schedule.current_shifts = json.dumps(current_shifts, default=str)
|
||||
schedule.empty_oncall = False
|
||||
schedule.save()
|
||||
|
||||
with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call:
|
||||
notify_ical_schedule_shift(schedule.oncallschedule_ptr_id)
|
||||
|
||||
assert not mock_slack_api_call.called
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_changes_no_triggering_notification(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1")
|
||||
# clear users pks <-> organization cache (persisting between tests)
|
||||
memoized_users_in_ical.cache_clear()
|
||||
|
||||
schedule = make_schedule(
|
||||
organization,
|
||||
schedule_class=OnCallScheduleCalendar,
|
||||
name="test_schedule",
|
||||
channel="channel",
|
||||
prev_ical_file_overrides=None,
|
||||
cached_ical_file_overrides=None,
|
||||
)
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - timezone.timedelta(days=7)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": timezone.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
on_call_shift.schedules.add(schedule)
|
||||
|
||||
# setup current shifts before checking/triggering for notifications
|
||||
calendar = icalendar.Calendar.from_ical(schedule._ical_file_primary)
|
||||
current_shifts, _ = get_current_shifts_from_ical(calendar, schedule, 0)
|
||||
schedule.current_shifts = json.dumps(current_shifts, default=str)
|
||||
schedule.empty_oncall = False
|
||||
schedule.save()
|
||||
|
||||
with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call:
|
||||
notify_ical_schedule_shift(schedule.oncallschedule_ptr_id)
|
||||
|
||||
assert not mock_slack_api_call.called
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_current_shift_changes_trigger_notification(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_user,
|
||||
make_schedule,
|
||||
make_on_call_shift,
|
||||
):
|
||||
organization, _, _, _ = make_organization_and_user_with_slack_identities()
|
||||
user1 = make_user(organization=organization, username="user1")
|
||||
# clear users pks <-> organization cache (persisting between tests)
|
||||
memoized_users_in_ical.cache_clear()
|
||||
|
||||
schedule = make_schedule(
|
||||
organization,
|
||||
schedule_class=OnCallScheduleCalendar,
|
||||
name="test_schedule",
|
||||
channel="channel",
|
||||
prev_ical_file_overrides=None,
|
||||
cached_ical_file_overrides=None,
|
||||
)
|
||||
|
||||
now = timezone.now().replace(microsecond=0)
|
||||
start_date = now - timezone.timedelta(days=7)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": timezone.timedelta(seconds=3600 * 24),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user1]])
|
||||
on_call_shift.schedules.add(schedule)
|
||||
schedule.refresh_ical_file()
|
||||
|
||||
# setup empty current shifts before checking/triggering for notifications
|
||||
schedule.current_shifts = json.dumps({}, default=str)
|
||||
schedule.empty_oncall = False
|
||||
schedule.save()
|
||||
|
||||
with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call:
|
||||
notify_ical_schedule_shift(schedule.oncallschedule_ptr_id)
|
||||
|
||||
assert mock_slack_api_call.called
|
||||
|
|
|
|||
|
|
@ -527,9 +527,9 @@ def test_channel_filter_convert_from_regex_to_jinja2(
|
|||
make_channel_filter(alert_receive_channel, is_default=True)
|
||||
|
||||
# r"..." used to keep this string as raw string
|
||||
regex_filtering_term = r"\".*\": \"This alert was sent by user for the demonstration purposes\""
|
||||
final_filtering_term = r'{{ payload | json_dumps | regex_search("\".*\": \"This alert was sent by user for the demonstration purposes\"") }}'
|
||||
payload = {"description": "This alert was sent by user for the demonstration purposes"}
|
||||
regex_filtering_term = r"\".*\": \"This alert was sent by user for demonstration purposes\""
|
||||
final_filtering_term = r'{{ payload | json_dumps | regex_search("\".*\": \"This alert was sent by user for demonstration purposes\"") }}'
|
||||
payload = {"description": "This alert was sent by user for demonstration purposes"}
|
||||
|
||||
regex_channel_filter = make_channel_filter(
|
||||
alert_receive_channel,
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ class SlackMessage(models.Model):
|
|||
self.alert_group.slack_message = self
|
||||
self.alert_group.save(update_fields=["slack_message"])
|
||||
return self.alert_group
|
||||
return self.alert.group
|
||||
else:
|
||||
raise
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
|
|
@ -217,32 +218,3 @@ class SlackMessage(models.Model):
|
|||
pass
|
||||
else:
|
||||
raise e
|
||||
|
||||
@classmethod
|
||||
def get_alert_group_from_slack_message_payload(cls, slack_team_identity, payload):
|
||||
|
||||
message_ts = payload.get("message_ts") or payload["container"]["message_ts"] # interactive message or block
|
||||
channel_id = payload["channel"]["id"]
|
||||
|
||||
try:
|
||||
slack_message = cls.objects.get(
|
||||
slack_id=message_ts,
|
||||
_slack_team_identity=slack_team_identity,
|
||||
channel_id=channel_id,
|
||||
)
|
||||
alert_group = slack_message.get_alert_group()
|
||||
except cls.DoesNotExist as e:
|
||||
logger.error(
|
||||
f"Tried to get SlackMessage from message_ts:"
|
||||
f"slack_team_identity_id={slack_team_identity.pk},"
|
||||
f"message_ts={message_ts}"
|
||||
)
|
||||
raise e
|
||||
except cls.alert.RelatedObjectDoesNotExist as e:
|
||||
logger.error(
|
||||
f"Tried to get AlertGroup from SlackMessage:"
|
||||
f"slack_team_identity_id={slack_team_identity.pk},"
|
||||
f"message_ts={message_ts}"
|
||||
)
|
||||
raise e
|
||||
return alert_group
|
||||
|
|
|
|||
|
|
@ -5,38 +5,25 @@ from django.apps import apps
|
|||
from apps.api.permissions import RBACPermission
|
||||
from apps.slack.scenarios import scenario_step
|
||||
|
||||
from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin
|
||||
from .step_mixins import AlertGroupActionsMixin, CheckAlertIsUnarchivedMixin
|
||||
|
||||
|
||||
class OpenAlertAppearanceDialogStep(
|
||||
CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin, scenario_step.ScenarioStep
|
||||
):
|
||||
class OpenAlertAppearanceDialogStep(CheckAlertIsUnarchivedMixin, AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "open Alert Appearance"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
try:
|
||||
message_ts = payload["message_ts"]
|
||||
except KeyError:
|
||||
message_ts = payload["container"]["message_ts"]
|
||||
|
||||
try:
|
||||
alert_group_pk = payload["actions"][0]["action_id"].split("_")[1]
|
||||
except (KeyError, IndexError):
|
||||
value = json.loads(payload["actions"][0]["value"])
|
||||
alert_group_pk = value["alert_group_pk"]
|
||||
|
||||
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
return
|
||||
blocks = []
|
||||
|
||||
private_metadata = {
|
||||
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
|
||||
"alert_group_pk": alert_group_pk,
|
||||
"message_ts": message_ts,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
"message_ts": payload.get("message_ts") or payload["container"]["message_ts"],
|
||||
}
|
||||
|
||||
alert_receive_channel = alert_group.channel
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from apps.slack.tasks import (
|
|||
from apps.slack.utils import get_cache_key_update_incident_slack_message
|
||||
from common.utils import clean_markup, is_string_with_visible_characters
|
||||
|
||||
from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin
|
||||
from .step_mixins import AlertGroupActionsMixin, CheckAlertIsUnarchivedMixin
|
||||
|
||||
ATTACH_TO_ALERT_GROUPS_LIMIT = 20
|
||||
|
||||
|
|
@ -218,17 +218,20 @@ class AlertShootingStep(scenario_step.ScenarioStep):
|
|||
|
||||
class InviteOtherPersonToIncident(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "invite to incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
User = apps.get_model("user_management", "User")
|
||||
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
selected_user = None
|
||||
|
||||
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
|
|
@ -255,21 +258,24 @@ class InviteOtherPersonToIncident(
|
|||
|
||||
class SilenceGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "silence incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
value = payload["actions"][0]["selected_option"]["value"]
|
||||
try:
|
||||
silence_delay = int(payload["actions"][0]["selected_options"][0]["value"])
|
||||
except KeyError:
|
||||
silence_delay = int(payload["actions"][0]["selected_option"]["value"])
|
||||
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
silence_delay = json.loads(value)["delay"]
|
||||
except TypeError:
|
||||
# Deprecated handler kept for backward compatibility (so older Slack messages can still be processed)
|
||||
silence_delay = int(value)
|
||||
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
alert_group.silence_by_user(self.user, silence_delay, action_source=ActionSource.SLACK)
|
||||
|
|
@ -281,15 +287,17 @@ class SilenceGroupStep(
|
|||
|
||||
class UnSilenceGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "unsilence incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
alert_group.un_silence_by_user(self.user, action_source=ActionSource.SLACK)
|
||||
|
||||
|
|
@ -300,20 +308,20 @@ class UnSilenceGroupStep(
|
|||
|
||||
class SelectAttachGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "Select Alert Group for Attaching to"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
value = json.loads(payload["actions"][0]["value"])
|
||||
alert_group_pk = value.get("alert_group_pk")
|
||||
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
return
|
||||
|
||||
blocks = []
|
||||
view = {
|
||||
"callback_id": AttachGroupStep.routing_uid(),
|
||||
|
|
@ -326,7 +334,7 @@ class SelectAttachGroupStep(
|
|||
"private_metadata": json.dumps(
|
||||
{
|
||||
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
|
||||
"alert_group_pk": alert_group_pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
}
|
||||
),
|
||||
"close": {"type": "plain_text", "text": "Cancel", "emoji": True},
|
||||
|
|
@ -335,8 +343,8 @@ class SelectAttachGroupStep(
|
|||
if attached_incidents_exists:
|
||||
attached_incidents = alert_group.dependent_alert_groups.all()
|
||||
text = (
|
||||
f"Oops! This incident cannot be attached to another one because it already has "
|
||||
f"attached incidents ({attached_incidents.count()}):\n"
|
||||
f"Oops! This Alert Group cannot be attached to another one because it already has "
|
||||
f"attached Alert Group ({attached_incidents.count()}):\n"
|
||||
)
|
||||
for dependent_alert in attached_incidents:
|
||||
if dependent_alert.slack_permalink:
|
||||
|
|
@ -372,7 +380,7 @@ class SelectAttachGroupStep(
|
|||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Oops! There is no incidents, available to attach.",
|
||||
"text": "Oops! There are no Alert Groups available to attach.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
@ -441,7 +449,7 @@ class SelectAttachGroupStep(
|
|||
},
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "Select incident:",
|
||||
"text": "Select Alert Group:",
|
||||
"emoji": True,
|
||||
},
|
||||
}
|
||||
|
|
@ -451,11 +459,10 @@ class SelectAttachGroupStep(
|
|||
|
||||
class AttachGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "Attach incident"
|
||||
REQUIRED_PERMISSIONS = [] # Permissions are handled in SelectAttachGroupStep
|
||||
|
||||
def process_signal(self, log_record):
|
||||
alert_group = log_record.alert_group
|
||||
|
|
@ -497,8 +504,7 @@ class AttachGroupStep(
|
|||
root_alert_group_pk = int(payload["actions"][0]["selected_option"]["value"])
|
||||
|
||||
root_alert_group = AlertGroup.all_objects.get(pk=root_alert_group_pk)
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group) and self.check_alert_is_unarchived(
|
||||
slack_team_identity, payload, root_alert_group
|
||||
):
|
||||
|
|
@ -509,14 +515,17 @@ class AttachGroupStep(
|
|||
|
||||
class UnAttachGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "Unattach incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
alert_group.un_attach_by_user(self.user, action_source=ActionSource.SLACK)
|
||||
|
||||
|
|
@ -525,17 +534,26 @@ class UnAttachGroupStep(
|
|||
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
|
||||
|
||||
|
||||
class StopInvitationProcess(CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin, scenario_step.ScenarioStep):
|
||||
class StopInvitationProcess(CheckAlertIsUnarchivedMixin, AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "stop invitation"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
return
|
||||
|
||||
invitation_pk = payload["actions"][0]["name"].split("_")[1]
|
||||
Invitation.stop_invitation(invitation_pk, self.user)
|
||||
try:
|
||||
value = json.loads(payload["actions"][0]["value"])
|
||||
invitation_id = value["invitation_id"]
|
||||
except KeyError:
|
||||
# Deprecated handler kept for backward compatibility (so older Slack messages can still be processed)
|
||||
invitation_id = payload["actions"][0]["name"].split("_")[1]
|
||||
|
||||
Invitation.stop_invitation(invitation_id, self.user)
|
||||
|
||||
def process_signal(self, log_record):
|
||||
self.alert_group_slack_service.update_alert_group_slack_message(log_record.invitation.alert_group)
|
||||
|
|
@ -543,16 +561,18 @@ class StopInvitationProcess(CheckAlertIsUnarchivedMixin, IncidentActionsAccessCo
|
|||
|
||||
class CustomButtonProcessStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
# TODO:
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "click custom button"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
CustomButtom = apps.get_model("alerts", "CustomButton")
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
custom_button_pk = payload["actions"][0]["name"].split("_")[1]
|
||||
alert_group_pk = payload["actions"][0]["name"].split("_")[2]
|
||||
|
|
@ -603,16 +623,18 @@ class CustomButtonProcessStep(
|
|||
|
||||
class ResolveGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "resolve incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
ResolutionNoteModalStep = scenario_step.ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
|
||||
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
return
|
||||
|
|
@ -645,14 +667,17 @@ class ResolveGroupStep(
|
|||
|
||||
class UnResolveGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "unresolve incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
alert_group.un_resolve_by_user(self.user, action_source=ActionSource.SLACK)
|
||||
|
||||
|
|
@ -663,36 +688,38 @@ class UnResolveGroupStep(
|
|||
|
||||
class AcknowledgeGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "acknowledge incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
logger.debug(f"process_scenario in AcknowledgeGroupStep for alert_group {alert_group.pk}")
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
alert_group.acknowledge_by_user(self.user, action_source=ActionSource.SLACK)
|
||||
|
||||
def process_signal(self, log_record):
|
||||
alert_group = log_record.alert_group
|
||||
logger.debug(f"Started process_signal in AcknowledgeGroupStep for alert_group {alert_group.pk}")
|
||||
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
|
||||
logger.debug(f"Finished process_signal in AcknowledgeGroupStep for alert_group {alert_group.pk}")
|
||||
|
||||
|
||||
class UnAcknowledgeGroupStep(
|
||||
CheckAlertIsUnarchivedMixin,
|
||||
IncidentActionsAccessControlMixin,
|
||||
AlertGroupActionsMixin,
|
||||
scenario_step.ScenarioStep,
|
||||
):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
ACTION_VERBOSE = "unacknowledge incident"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
|
||||
logger.debug(f"process_scenario in UnAcknowledgeGroupStep for alert_group {alert_group.pk}")
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
alert_group.un_acknowledge_by_user(self.user, action_source=ActionSource.SLACK)
|
||||
|
||||
|
|
@ -717,7 +744,7 @@ class UnAcknowledgeGroupStep(
|
|||
]
|
||||
text = (
|
||||
f"{user_verbal} hasn't responded to an acknowledge timeout reminder."
|
||||
f" Alert Group is unacknowledged automatically"
|
||||
f" Alert Group is unacknowledged automatically."
|
||||
)
|
||||
if alert_group.slack_message.ack_reminder_message_ts:
|
||||
try:
|
||||
|
|
@ -749,8 +776,6 @@ class UnAcknowledgeGroupStep(
|
|||
|
||||
|
||||
class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
||||
ACTION_VERBOSE = "confirm acknowledge status"
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
alert_group_id = payload["actions"][0]["value"].split("_")[1]
|
||||
|
|
@ -762,7 +787,7 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
if alert_group.acknowledged_by == AlertGroup.USER:
|
||||
if self.user == alert_group.acknowledged_by_user:
|
||||
user_verbal = alert_group.acknowledged_by_user.get_username_with_slack_verbal()
|
||||
text = f"{user_verbal} confirmed that the incident is still acknowledged"
|
||||
text = f"{user_verbal} confirmed that the Alert Group is still acknowledged."
|
||||
self._slack_client.api_call(
|
||||
"chat.update",
|
||||
channel=channel,
|
||||
|
|
@ -776,11 +801,11 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
"chat.postEphemeral",
|
||||
channel=channel,
|
||||
user=slack_user_identity.slack_id,
|
||||
text="This alert is acknowledged by another user. Acknowledge it yourself first.",
|
||||
text="This Alert Group is acknowledged by another user. Acknowledge it yourself first.",
|
||||
)
|
||||
elif alert_group.acknowledged_by == AlertGroup.SOURCE:
|
||||
user_verbal = self.user.get_username_with_slack_verbal()
|
||||
text = f"{user_verbal} confirmed that the incident is still acknowledged"
|
||||
text = f"{user_verbal} confirmed that the Alert Group is still acknowledged."
|
||||
self._slack_client.api_call(
|
||||
"chat.update",
|
||||
channel=channel,
|
||||
|
|
@ -799,7 +824,7 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
"chat.postEphemeral",
|
||||
channel=channel,
|
||||
user=slack_user_identity.slack_id,
|
||||
text="This alert is already unacknowledged.",
|
||||
text="This Alert Group is already unacknowledged.",
|
||||
)
|
||||
|
||||
def process_signal(self, log_record):
|
||||
|
|
@ -809,12 +834,12 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
alert_group = log_record.alert_group
|
||||
channel_id = alert_group.slack_message.channel_id
|
||||
user_verbal = log_record.author.get_username_with_slack_verbal(mention=True)
|
||||
text = f"{user_verbal}, please confirm that you're still working on this incident."
|
||||
text = f"{user_verbal}, please confirm that you're still working on this Alert Group."
|
||||
|
||||
if alert_group.channel.organization.unacknowledge_timeout != Organization.UNACKNOWLEDGE_TIMEOUT_NEVER:
|
||||
attachments = [
|
||||
{
|
||||
"fallback": "Are you still working on this incident?",
|
||||
"fallback": "Are you still working on this Alert Group?",
|
||||
"text": text,
|
||||
"callback_id": "alert",
|
||||
"attachment_type": "default",
|
||||
|
|
@ -883,8 +908,6 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
|
|||
|
||||
|
||||
class WipeGroupStep(scenario_step.ScenarioStep):
|
||||
ACTION_VERBOSE = "wipe incident"
|
||||
|
||||
def process_signal(self, log_record):
|
||||
alert_group = log_record.alert_group
|
||||
user_verbal = log_record.author.get_username_with_slack_verbal()
|
||||
|
|
@ -894,8 +917,6 @@ class WipeGroupStep(scenario_step.ScenarioStep):
|
|||
|
||||
|
||||
class DeleteGroupStep(scenario_step.ScenarioStep):
|
||||
ACTION_VERBOSE = "delete incident"
|
||||
|
||||
def process_signal(self, log_record):
|
||||
alert_group = log_record.alert_group
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ from django.apps import apps
|
|||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.slack.scenarios import scenario_step
|
||||
from apps.slack.slack_client.exceptions import SlackAPIException
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
|
||||
from .step_mixins import CheckAlertIsUnarchivedMixin
|
||||
from .step_mixins import AlertGroupActionsMixin, CheckAlertIsUnarchivedMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -372,18 +373,28 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep):
|
|||
return blocks
|
||||
|
||||
|
||||
class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.ScenarioStep):
|
||||
class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
|
||||
RESOLUTION_NOTE_TEXT_BLOCK_ID = "resolution_note_text"
|
||||
RESOLUTION_NOTE_MESSAGES_MAX_COUNT = 25
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload, data=None):
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
if data:
|
||||
# Argument "data" is used when step is called from other step, e.g. AddRemoveThreadMessageStep
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
alert_group = AlertGroup.all_objects.get(pk=data["alert_group_pk"])
|
||||
else:
|
||||
# Handle "Add Resolution notes" button click
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
value = data or json.loads(payload["actions"][0]["value"])
|
||||
resolution_note_window_action = value.get("resolution_note_window_action", "") or value.get("action_value", "")
|
||||
alert_group_pk = value.get("alert_group_pk")
|
||||
action_resolve = value.get("action_resolve", False)
|
||||
channel_id = payload["channel"]["id"] if "channel" in payload else None
|
||||
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
|
||||
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
|
||||
return
|
||||
|
|
@ -413,7 +424,7 @@ class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari
|
|||
"private_metadata": json.dumps(
|
||||
{
|
||||
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
|
||||
"alert_group_pk": alert_group_pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
}
|
||||
),
|
||||
}
|
||||
|
|
@ -431,7 +442,7 @@ class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari
|
|||
# Ignore "not_found" error, it means that the view was closed by user before the update request.
|
||||
# It doesn't disrupt the user experience.
|
||||
logger.debug(
|
||||
f"API call to views.update failed for alert group {alert_group_pk}, error: not_found. "
|
||||
f"API call to views.update failed for alert group {alert_group.pk}, error: not_found. "
|
||||
f"Most likely the view was closed by user before the request was processed. "
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,72 +1,164 @@
|
|||
import json
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.api.permissions import user_is_authorized
|
||||
from apps.slack.models import SlackMessage, SlackTeamIdentity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccessControl(ABC):
|
||||
class AlertGroupActionsMixin:
|
||||
"""
|
||||
Mixin for alert group actions (ack, resolve, etc.). Intended to be used as a mixin along with ScenarioStep.
|
||||
"""
|
||||
|
||||
REQUIRED_PERMISSIONS = []
|
||||
ACTION_VERBOSE = ""
|
||||
|
||||
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
||||
if self.check_membership():
|
||||
return super().process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
else:
|
||||
self.send_denied_message(payload)
|
||||
def get_alert_group(self, slack_team_identity: SlackTeamIdentity, payload: dict) -> AlertGroup:
|
||||
"""
|
||||
Get AlertGroup instance on Slack message button click or select menu change.
|
||||
"""
|
||||
|
||||
def check_membership(self):
|
||||
return user_is_authorized(self.user, self.REQUIRED_PERMISSIONS)
|
||||
alert_group = (
|
||||
self._get_alert_group_from_action(payload) # Try to get alert_group_pk from PRESSED button
|
||||
or self._get_alert_group_from_message(payload) # Try to use alert_group_pk from ANY button in message
|
||||
or self._get_alert_group_from_slack_message_in_db(slack_team_identity, payload) # Fetch message from DB
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def send_denied_message(self, payload):
|
||||
pass
|
||||
# Repair alert group if Slack message is orphaned
|
||||
if alert_group.slack_message is None:
|
||||
self._repair_alert_group(slack_team_identity, alert_group, payload)
|
||||
|
||||
return alert_group
|
||||
|
||||
class IncidentActionsAccessControlMixin(AccessControl):
|
||||
"""
|
||||
Mixin for auth in incident actions
|
||||
"""
|
||||
def is_authorized(self, alert_group: AlertGroup) -> bool:
|
||||
"""
|
||||
Check that user has required permissions to perform an action.
|
||||
"""
|
||||
|
||||
def send_denied_message_to_channel(self, payload=None):
|
||||
# Send denied message to thread by default
|
||||
return False
|
||||
return (
|
||||
self.user is not None
|
||||
and self.user.organization == alert_group.channel.organization
|
||||
and user_is_authorized(self.user, self.REQUIRED_PERMISSIONS)
|
||||
)
|
||||
|
||||
def send_denied_message(self, payload):
|
||||
def open_unauthorized_warning(self, payload: dict) -> None:
|
||||
self.open_warning_window(
|
||||
payload,
|
||||
warning_text="You do not have permission to perform this action. Ask an admin to upgrade your permissions.",
|
||||
title="Permission denied",
|
||||
)
|
||||
|
||||
def _repair_alert_group(
|
||||
self, slack_team_identity: SlackTeamIdentity, alert_group: AlertGroup, payload: dict
|
||||
) -> None:
|
||||
"""
|
||||
There's a possibility that OnCall failed to create a SlackMessage instance for an AlertGroup, but the message
|
||||
was sent to Slack. This method creates SlackMessage instance for such orphaned messages.
|
||||
"""
|
||||
|
||||
channel_id = payload["channel"]["id"]
|
||||
try:
|
||||
thread_ts = payload["message_ts"]
|
||||
message_id = payload["message"]["ts"]
|
||||
except KeyError:
|
||||
thread_ts = payload["message"]["ts"]
|
||||
message_id = payload["original_message"]["ts"]
|
||||
|
||||
text = "Attempted to {} by {}, but failed due to a lack of permissions.".format(
|
||||
self.ACTION_VERBOSE,
|
||||
self.user.get_username_with_slack_verbal(),
|
||||
slack_message = SlackMessage.objects.create(
|
||||
slack_id=message_id,
|
||||
organization=alert_group.channel.organization,
|
||||
_slack_team_identity=slack_team_identity,
|
||||
channel_id=channel_id,
|
||||
alert_group=alert_group,
|
||||
)
|
||||
|
||||
self._slack_client.api_call(
|
||||
"chat.postMessage",
|
||||
channel=payload["channel"]["id"],
|
||||
text=text,
|
||||
blocks=[
|
||||
{
|
||||
"type": "section",
|
||||
"block_id": "alert",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": text,
|
||||
},
|
||||
},
|
||||
],
|
||||
thread_ts=None if self.send_denied_message_to_channel(payload) else thread_ts,
|
||||
unfurl_links=True,
|
||||
alert_group.slack_message = slack_message
|
||||
alert_group.save(update_fields=["slack_message"])
|
||||
|
||||
def _get_alert_group_from_action(self, payload: dict) -> AlertGroup | None:
|
||||
"""
|
||||
Get AlertGroup instance from action data in payload. Action data is data encoded into buttons and select
|
||||
menus in apps.alerts.incident_appearance.renderers.slack_renderer.AlertGroupSlackRenderer._get_buttons_blocks.
|
||||
"""
|
||||
|
||||
action = payload["actions"][0]
|
||||
action_type = action["type"]
|
||||
|
||||
if action_type == "button":
|
||||
value_string = action["value"]
|
||||
elif action_type == "static_select":
|
||||
value_string = action["selected_option"]["value"]
|
||||
else:
|
||||
raise ValueError(f"Unexpected action type: {action_type}")
|
||||
|
||||
try:
|
||||
value = json.loads(value_string)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
try:
|
||||
alert_group_pk = value["alert_group_pk"]
|
||||
except (KeyError, TypeError):
|
||||
return None
|
||||
|
||||
return AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
|
||||
def _get_alert_group_from_message(self, payload: dict) -> AlertGroup | None:
|
||||
"""
|
||||
Get AlertGroup instance from message data in payload. It's similar to _get_alert_group_from_action,
|
||||
but it tries to get alert_group_pk from ANY button in the message, not just the one that was clicked.
|
||||
"""
|
||||
|
||||
try:
|
||||
# sometimes message is in "original_message" field, not "message"
|
||||
message = payload.get("message") or payload["original_message"]
|
||||
elements = message["attachments"][0]["blocks"][0]["elements"]
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
||||
for element in elements:
|
||||
value_string = element.get("value")
|
||||
if not value_string:
|
||||
continue
|
||||
|
||||
try:
|
||||
value = json.loads(value_string)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
continue
|
||||
|
||||
try:
|
||||
alert_group_pk = value["alert_group_pk"]
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
|
||||
return AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
|
||||
def _get_alert_group_from_slack_message_in_db(
|
||||
self, slack_team_identity: SlackTeamIdentity, payload: dict
|
||||
) -> AlertGroup:
|
||||
"""
|
||||
Get AlertGroup instance from SlackMessage instance.
|
||||
Old messages may not have alert_group_pk encoded into buttons, so we need to query SlackMessage to figure out
|
||||
the AlertGroup.
|
||||
"""
|
||||
|
||||
message_ts = payload.get("message_ts") or payload["container"]["message_ts"] # interactive message or block
|
||||
channel_id = payload["channel"]["id"]
|
||||
|
||||
# All Slack messages from OnCall should have alert_group_pk encoded into buttons, so reaching this point means
|
||||
# something probably went wrong.
|
||||
logger.warning(f"alert_group_pk not found in payload, fetching SlackMessage from DB. message_ts: {message_ts}")
|
||||
|
||||
# Get SlackMessage from DB
|
||||
slack_message = SlackMessage.objects.get(
|
||||
slack_id=message_ts,
|
||||
_slack_team_identity=slack_team_identity,
|
||||
channel_id=channel_id,
|
||||
)
|
||||
return slack_message.get_alert_group()
|
||||
|
||||
|
||||
class CheckAlertIsUnarchivedMixin(object):
|
||||
REQUIRED_PERMISSIONS = []
|
||||
ACTION_VERBOSE = ""
|
||||
|
||||
class CheckAlertIsUnarchivedMixin:
|
||||
def check_alert_is_unarchived(self, slack_team_identity, payload, alert_group, warning=True):
|
||||
alert_group_is_unarchived = alert_group.started_at.date() > self.organization.archive_alerts_from
|
||||
if not alert_group_is_unarchived:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,852 @@
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.slack.scenarios.scenario_step import ScenarioStep
|
||||
from apps.slack.scenarios.step_mixins import AlertGroupActionsMixin
|
||||
|
||||
|
||||
class TestScenario(AlertGroupActionsMixin, ScenarioStep):
|
||||
pass
|
||||
|
||||
|
||||
# List of steps to be tested for alert group actions (getting alert group from Slack payload + user permissions check)
|
||||
ALERT_GROUP_ACTIONS_STEPS = [
|
||||
# Acknowledge / Unacknowledge buttons
|
||||
ScenarioStep.get_step("distribute_alerts", "AcknowledgeGroupStep"),
|
||||
ScenarioStep.get_step("distribute_alerts", "UnAcknowledgeGroupStep"),
|
||||
# Resolve / Unresolve buttons
|
||||
ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep"),
|
||||
ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep"),
|
||||
# Invite / Stop inviting buttons
|
||||
ScenarioStep.get_step("distribute_alerts", "InviteOtherPersonToIncident"),
|
||||
ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess"),
|
||||
# Silence / Unsilence buttons
|
||||
ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep"),
|
||||
ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep"),
|
||||
# Attach / Unattach buttons
|
||||
ScenarioStep.get_step("distribute_alerts", "SelectAttachGroupStep"),
|
||||
ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep"),
|
||||
# Format alert button
|
||||
ScenarioStep.get_step("alertgroup_appearance", "OpenAlertAppearanceDialogStep"),
|
||||
# Add resolution notes button
|
||||
ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep"),
|
||||
]
|
||||
|
||||
|
||||
# Constants to simplify parametrized tests
|
||||
ORGANIZATION_ID = 42
|
||||
ALERT_GROUP_ID = 24
|
||||
SLACK_MESSAGE_TS = "RANDOM_MESSAGE_TS"
|
||||
SLACK_CHANNEL_ID = "RANDOM_CHANNEL_ID"
|
||||
USER_ID = 56
|
||||
INVITATION_ID = 78
|
||||
|
||||
|
||||
def _get_payload(action_type="button", **kwargs):
|
||||
"""
|
||||
Utility function to generate payload to be used by scenario steps.
|
||||
"""
|
||||
if action_type == "button":
|
||||
return {
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps(
|
||||
{"organization_id": ORGANIZATION_ID, "alert_group_pk": ALERT_GROUP_ID, **kwargs}
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
elif action_type == "static_select":
|
||||
return {
|
||||
"actions": [
|
||||
{
|
||||
"type": "static_select",
|
||||
"selected_option": {
|
||||
"value": json.dumps(
|
||||
{"organization_id": ORGANIZATION_ID, "alert_group_pk": ALERT_GROUP_ID, **kwargs}
|
||||
)
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step_class", ALERT_GROUP_ACTIONS_STEPS)
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_actions_unauthorized(
|
||||
step_class, make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
|
||||
):
|
||||
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities(
|
||||
role=LegacyAccessControlRole.VIEWER
|
||||
)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
payload = {
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": alert_group.pk}),
|
||||
}
|
||||
],
|
||||
"channel": {"id": "RANDOM_CHANNEL_ID"},
|
||||
"message": {"ts": "RANDOM_MESSAGE_TS"},
|
||||
"trigger_id": "RANDOM_TRIGGER_ID",
|
||||
}
|
||||
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
|
||||
with patch.object(step, "open_unauthorized_warning") as mock_open_unauthorized_warning:
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
mock_open_unauthorized_warning.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_alert_group_button(
|
||||
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
|
||||
):
|
||||
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
payload = {
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": alert_group.pk}),
|
||||
}
|
||||
],
|
||||
"channel": {"id": "RANDOM_CHANNEL_ID"},
|
||||
"message": {"ts": "RANDOM_MESSAGE_TS"},
|
||||
}
|
||||
|
||||
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
result = step.get_alert_group(slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group == result # check it's the right alert group
|
||||
assert alert_group.slack_message is not None # check that orphaned Slack message is repaired
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_alert_group_static_select(
|
||||
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
|
||||
):
|
||||
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
payload = {
|
||||
"actions": [
|
||||
{
|
||||
"type": "static_select",
|
||||
"selected_option": {
|
||||
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": alert_group.pk})
|
||||
},
|
||||
}
|
||||
],
|
||||
"channel": {"id": "RANDOM_CHANNEL_ID"},
|
||||
"message": {"ts": "RANDOM_MESSAGE_TS"},
|
||||
}
|
||||
|
||||
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
result = step.get_alert_group(slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group == result # check it's the right alert group
|
||||
assert alert_group.slack_message is not None # check that orphaned Slack message is repaired
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_alert_group_from_message(
|
||||
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
|
||||
):
|
||||
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
payload = {
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": "no alert_group_pk",
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"ts": "RANDOM_MESSAGE_TS",
|
||||
"attachments": [{"blocks": [{"elements": [{"value": json.dumps({"alert_group_pk": alert_group.pk})}]}]}],
|
||||
},
|
||||
"channel": {"id": "RANDOM_CHANNEL_ID"},
|
||||
}
|
||||
|
||||
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
result = step.get_alert_group(slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group == result # check it's the right alert group
|
||||
assert alert_group.slack_message is not None # check that orphaned Slack message is repaired
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_alert_group_from_slack_message_in_db(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
slack_channel = make_slack_channel(slack_team_identity)
|
||||
slack_message = make_slack_message(alert_group=alert_group, channel_id=slack_channel.slack_id)
|
||||
|
||||
payload = {
|
||||
"message_ts": slack_message.slack_id,
|
||||
"channel": {"id": slack_channel.slack_id},
|
||||
"actions": [{"type": "button", "value": "RANDOM_VALUE"}],
|
||||
}
|
||||
|
||||
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
result = step.get_alert_group(slack_team_identity, payload)
|
||||
|
||||
assert alert_group == result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_acknowledge(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, acknowledged=False, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "AcknowledgeGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.acknowledged is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_unacknowledge(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, acknowledged=True, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "UnAcknowledgeGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.acknowledged is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_resolve(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=False, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.resolved is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_unresolve(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=True, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.resolved is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
# Usual data such as alert_group_pk is not passed to InviteOtherPersonToIncident, so it doesn't increase
|
||||
# payload size too much.
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [
|
||||
{
|
||||
"type": "static_select",
|
||||
"selected_option": {"value": json.dumps({"user_id": USER_ID})},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_invite(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
second_user = make_user(organization=organization, pk=USER_ID)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=True, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "InviteOtherPersonToIncident")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.invitations.count() == 1
|
||||
|
||||
invitation = alert_group.invitations.first()
|
||||
assert invitation.author == user
|
||||
assert invitation.invitee == second_user
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(invitation_id=INVITATION_ID),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [
|
||||
{
|
||||
"name": f"StopInvitationProcess_{INVITATION_ID}",
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": ORGANIZATION_ID}),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_stop_invite(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
make_invitation,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
second_user = make_user(organization=organization, pk=USER_ID)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=True, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
invitation = make_invitation(alert_group, user, second_user, pk=INVITATION_ID)
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
invitation.refresh_from_db()
|
||||
assert invitation.is_active is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(action_type="static_select", delay=1800),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [
|
||||
{
|
||||
"type": "static_select",
|
||||
"selected_option": {"value": "1800"},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_silence(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, silenced=False, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.silenced is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload(action_type="static_select", delay=1800),
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": ORGANIZATION_ID}),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_unsilence(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, silenced=True, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.silenced is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload() | {"trigger_id": "RANDOM_TRIGGER_ID"},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_select_attach(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "SelectAttachGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
|
||||
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
assert mock_slack_api_call.call_args.args == ("views.open",)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload() | {"trigger_id": "RANDOM_TRIGGER_ID"},
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"trigger_id": "RANDOM_TRIGGER_ID",
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": ORGANIZATION_ID}),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_unattach(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_group = make_alert_group(alert_receive_channel, root_alert_group=root_alert_group, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.root_alert_group is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
_get_payload() | {"message_ts": "RANDOM_TS", "trigger_id": "RANDOM_TRIGGER_ID"},
|
||||
# deprecated payload shape, but still supported to handle older Slack messages
|
||||
{
|
||||
"message_ts": SLACK_MESSAGE_TS,
|
||||
"channel": {"id": SLACK_CHANNEL_ID},
|
||||
"trigger_id": "RANDOM_TRIGGER_ID",
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps({"organization_id": ORGANIZATION_ID, "alert_group_pk": str(ALERT_GROUP_ID)}),
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_step_format_alert(
|
||||
payload,
|
||||
make_organization,
|
||||
make_slack_team_identity,
|
||||
make_user,
|
||||
make_slack_user_identity,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_slack_channel,
|
||||
make_slack_message,
|
||||
):
|
||||
slack_team_identity = make_slack_team_identity()
|
||||
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
|
||||
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
|
||||
|
||||
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, pk=ALERT_GROUP_ID)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
step_class = ScenarioStep.get_step("alertgroup_appearance", "OpenAlertAppearanceDialogStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
|
||||
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
assert mock_slack_api_call.call_args.args == ("views.open",)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_step_resolution_note(
|
||||
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group, raw_request_data={})
|
||||
|
||||
payload = {
|
||||
"trigger_id": "RANDOM_TRIGGER_ID",
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps(
|
||||
{
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
"resolution_note_window_action": "edit",
|
||||
}
|
||||
),
|
||||
}
|
||||
],
|
||||
"channel": {"id": "RANDOM_CHANNEL_ID"},
|
||||
"message": {"ts": "RANDOM_MESSAGE_TS"},
|
||||
}
|
||||
|
||||
step_class = ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
|
||||
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
|
||||
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
assert mock_slack_api_call.call_args.args == ("views.open",)
|
||||
|
|
@ -189,26 +189,43 @@ def test_get_resolution_notes_blocks_latest_limit(
|
|||
side_effect=SlackAPIException(response={"ok": False, "error": "not_found"}),
|
||||
)
|
||||
def test_resolution_notes_modal_closed_before_update(
|
||||
mock_slack_api_call, make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
|
||||
mock_slack_api_call,
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_slack_message,
|
||||
):
|
||||
ResolutionNoteModalStep = ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
|
||||
|
||||
organization, _, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
|
||||
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
|
||||
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
slack_message = make_slack_message(
|
||||
alert_group=alert_group, channel_id="RANDOM_CHANNEL_ID", slack_id="RANDOM_MESSAGE_ID"
|
||||
)
|
||||
slack_message.get_alert_group() # fix FKs
|
||||
|
||||
payload = {
|
||||
"trigger_id": "TEST",
|
||||
"view": {"id": "TEST"},
|
||||
"actions": [
|
||||
{"value": json.dumps({"alert_group_pk": alert_group.pk, "resolution_note_window_action": "update"})}
|
||||
{
|
||||
"type": "button",
|
||||
"value": json.dumps(
|
||||
{
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
"resolution_note_window_action": "update",
|
||||
}
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Check that no error is raised even if the Slack API call fails
|
||||
step = ResolutionNoteModalStep(organization=organization, slack_team_identity=slack_team_identity)
|
||||
step = ResolutionNoteModalStep(organization=organization, user=user, slack_team_identity=slack_team_identity)
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
# Check that "views.update" API call was made
|
||||
|
|
|
|||
213
engine/apps/slack/tests/test_slack_renderer.py
Normal file
213
engine/apps/slack/tests/test_slack_renderer.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertGroupSlackRenderer
|
||||
from apps.alerts.models import AlertGroup
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_acknowledge_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[0]
|
||||
assert button["text"]["text"] == "Acknowledge"
|
||||
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_unacknowledge_button(
|
||||
make_organization, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, acknowledged=True)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[0]
|
||||
assert button["text"]["text"] == "Unacknowledge"
|
||||
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_resolve_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[1]
|
||||
assert button["text"]["text"] == "Resolve"
|
||||
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_unresolve_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, resolved=True)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[0]
|
||||
assert button["text"]["text"] == "Unresolve"
|
||||
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_invite_action(
|
||||
make_organization, make_user, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization = make_organization()
|
||||
user = make_user(organization=organization)
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
ack_button = elements[2]
|
||||
assert ack_button["placeholder"]["text"] == "Invite..."
|
||||
|
||||
# Check only user_id is passed. Otherwise, if there are a lot of users, the payload could be unnecessarily large.
|
||||
assert json.loads(ack_button["options"][0]["value"]) == {"user_id": user.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_stop_invite_button(
|
||||
make_organization, make_user, make_alert_receive_channel, make_alert_group, make_alert, make_invitation
|
||||
):
|
||||
organization = make_organization()
|
||||
user = make_user(organization=organization)
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
invitation = make_invitation(alert_group, user, user)
|
||||
|
||||
action = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[1]["actions"][0]
|
||||
|
||||
assert action["text"] == f"Stop inviting {user.username}"
|
||||
assert json.loads(action["value"]) == {
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
"invitation_id": invitation.pk,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_silence_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[3]
|
||||
assert button["placeholder"]["text"] == "Silence"
|
||||
|
||||
values = [json.loads(option["value"]) for option in button["options"]]
|
||||
assert values == [
|
||||
{"organization_id": organization.pk, "alert_group_pk": alert_group.pk, "delay": delay}
|
||||
for delay, _ in AlertGroup.SILENCE_DELAY_OPTIONS
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_unsilence_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, silenced=True)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
button = elements[3]
|
||||
|
||||
assert button["text"]["text"] == "Unsilence"
|
||||
assert json.loads(button["value"]) == {
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_attach_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel, silenced=True)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
button = elements[4]
|
||||
|
||||
assert button["text"]["text"] == "Attach to ..."
|
||||
assert json.loads(button["value"]) == {
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_unattach_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=root_alert_group, raw_request_data={})
|
||||
|
||||
alert_group = make_alert_group(alert_receive_channel, root_alert_group=root_alert_group)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
action = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["actions"][0]
|
||||
|
||||
assert action["text"] == "Unattach"
|
||||
assert json.loads(action["value"]) == {
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_format_alert_button(
|
||||
make_organization, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[5]
|
||||
assert button["text"]["text"] == ":mag: Format Alert"
|
||||
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_slack_renderer_resolution_notes_button(
|
||||
make_organization, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
|
||||
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
|
||||
|
||||
button = elements[6]
|
||||
assert button["text"]["text"] == "Add Resolution notes"
|
||||
assert json.loads(button["value"]) == {
|
||||
"organization_id": organization.pk,
|
||||
"alert_group_pk": alert_group.pk,
|
||||
"resolution_note_window_action": "edit",
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@ example_payload = {
|
|||
"labels": {"alertname": "TestAlert", "region": "eu-1", "severity": "critical"},
|
||||
"annotations": {
|
||||
"message": "This is test alert",
|
||||
"description": "This alert was sent by user for the demonstration purposes",
|
||||
"description": "This alert was sent by user for demonstration purposes",
|
||||
"runbook_url": "https://grafana.com/",
|
||||
},
|
||||
"startsAt": "2018-12-25T15:47:47.377363608Z",
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@ resolve_condition = """\
|
|||
|
||||
acknowledge_condition = None
|
||||
|
||||
example_payload = {"message": "This alert was sent by user for the demonstration purposes"}
|
||||
example_payload = {"message": "This alert was sent by user for demonstration purposes"}
|
||||
|
|
|
|||
|
|
@ -52,5 +52,5 @@ example_payload = {
|
|||
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
|
||||
"state": "alerting",
|
||||
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
|
||||
"message": "This alert was sent by user for the demonstration purposes\nSmth happened. Oh no!",
|
||||
"message": "This alert was sent by user for demonstration purposes\nSmth happened. Oh no!",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ example_payload = {
|
|||
"alertname": "TestAlert",
|
||||
"region": "eu-1",
|
||||
},
|
||||
"annotations": {"description": "This alert was sent by user for the demonstration purposes"},
|
||||
"annotations": {"description": "This alert was sent by user for demonstration purposes"},
|
||||
"startsAt": "2018-12-25T15:47:47.377363608Z",
|
||||
"endsAt": "0001-01-01T00:00:00Z",
|
||||
"generatorURL": "",
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ example_payload = {
|
|||
"alertname": "TestAlert",
|
||||
"region": "eu-1",
|
||||
},
|
||||
"annotations": {"description": "This alert was sent by user for the demonstration purposes"},
|
||||
"annotations": {"description": "This alert was sent by user for demonstration purposes"},
|
||||
"startsAt": "2018-12-25T15:47:47.377363608Z",
|
||||
"endsAt": "0001-01-01T00:00:00Z",
|
||||
"generatorURL": "",
|
||||
|
|
|
|||
|
|
@ -54,6 +54,6 @@ acknowledge_condition = None
|
|||
|
||||
example_payload = {
|
||||
"id": "TestAlert",
|
||||
"message": "This alert was sent by user for the demonstration purposes",
|
||||
"message": "This alert was sent by user for demonstration purposes",
|
||||
"data": "{foo: bar}",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@ resolve_condition = """\
|
|||
{%- endif %}"""
|
||||
acknowledge_condition = None
|
||||
|
||||
example_payload = {"message": "This alert was sent by user for the demonstration purposes"}
|
||||
example_payload = {"message": "This alert was sent by user for demonstration purposes"}
|
||||
|
|
|
|||
|
|
@ -54,5 +54,5 @@ example_payload = {
|
|||
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
|
||||
"state": "alerting",
|
||||
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
|
||||
"message": "This alert was sent by user for the demonstration purposes\nSmth happened. Oh no!",
|
||||
"message": "This alert was sent by user for demonstration purposes\nSmth happened. Oh no!",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ urlpatterns = [
|
|||
path("api/internal/v1/mobile_app/", include("apps.mobile_app.urls", namespace="mobile_app_tmp")),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
if settings.FEATURE_PROMETHEUS_EXPORTER_ENABLED:
|
||||
urlpatterns += [
|
||||
path("metrics/", include("apps.metrics_exporter.urls")),
|
||||
]
|
||||
|
||||
if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
||||
urlpatterns += [
|
||||
path("api/internal/v1/slack/", include("apps.slack.urls")),
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ FEATURE_SLACK_INTEGRATION_ENABLED = getenv_boolean("FEATURE_SLACK_INTEGRATION_EN
|
|||
FEATURE_WEB_SCHEDULES_ENABLED = getenv_boolean("FEATURE_WEB_SCHEDULES_ENABLED", default=False)
|
||||
FEATURE_MULTIREGION_ENABLED = getenv_boolean("FEATURE_MULTIREGION_ENABLED", default=False)
|
||||
FEATURE_INBOUND_EMAIL_ENABLED = getenv_boolean("FEATURE_INBOUND_EMAIL_ENABLED", default=False)
|
||||
FEATURE_PROMETHEUS_EXPORTER_ENABLED = getenv_boolean("FEATURE_PROMETHEUS_EXPORTER_ENABLED", default=False)
|
||||
GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True)
|
||||
GRAFANA_CLOUD_NOTIFICATIONS_ENABLED = getenv_boolean("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", default=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const TelegramInfo = observer((_props: TelegramInfoProps) => {
|
|||
|
||||
return (
|
||||
<WithPermissionControlDisplay
|
||||
userAction={UserActions.OtherSettingsWrite}
|
||||
userAction={UserActions.UserSettingsWrite}
|
||||
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
|
||||
>
|
||||
{telegramConfigured || !store.hasFeature(AppFeature.LiveSettings) ? (
|
||||
|
|
|
|||
11
provisioning/datasources/automatic.yml
Normal file
11
provisioning/datasources/automatic.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://host.docker.internal:9090
|
||||
jsonData:
|
||||
httpMethod: POST
|
||||
manageAlerts: true
|
||||
prometheusType: Prometheus
|
||||
Loading…
Add table
Reference in a new issue