# What this PR does - Stops writing `SlackMessage.organization` + removes references to this field. [As we discussed](https://raintank-corp.slack.com/archives/C083TU81TCH/p1733315887463279?thread_ts=1733311105.095309&cid=C083TU81TCH), we do not need this field on this model/table, `SlackMessage._slack_team_identity` is sufficient (`organization` will be dropped in a separate PR) - Adds a data migration script which: - drops orphaned `SlackMessage` records; ie. ones which, even after the [`engine/apps/slack/migrations/0007_migrate_slackmessage_channel_id.py`](https://github.com/grafana/oncall/blob/dev/engine/apps/slack/migrations/0007_migrate_slackmessage_channel_id.py) migration, still don't have a `SlackMessage.channel` id filled in (we discussed + agreed on dropping these records [here](https://raintank-corp.slack.com/archives/C083TU81TCH/p1733329914516859?thread_ts=1733311105.095309&cid=C083TU81TCH)) - fills in empty `SlackMessage.slack_team_identity` values (from `slack_message.channel.slack_team_identity`) ### Other notes On the `organization` topic. We store records in `SlackMessage` for two purposes (AFAICT), and in both cases, we have references back to the `organization`: - alert groups - `slack_message.alert_group.channel.organization` - shift swap requests - `shift_swap_request.schedule.organization` ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
370 lines
14 KiB
Python
370 lines
14 KiB
Python
from datetime import timedelta
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from django.utils import timezone
|
|
|
|
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
|
from apps.slack.client import SlackClient
|
|
from apps.slack.errors import SlackAPIError
|
|
from apps.slack.models import SlackMessage
|
|
from apps.slack.tests.conftest import build_slack_response
|
|
|
|
|
|
@pytest.fixture
|
|
def slack_message_setup(
|
|
make_organization_and_user_with_slack_identities,
|
|
make_alert_receive_channel,
|
|
make_alert_group,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
):
|
|
def _slack_message_setup(cached_permalink):
|
|
organization, _, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
|
|
integration = make_alert_receive_channel(organization)
|
|
alert_group = make_alert_group(integration)
|
|
slack_channel = make_slack_channel(slack_team_identity)
|
|
|
|
return make_slack_message(slack_channel, alert_group=alert_group, cached_permalink=cached_permalink)
|
|
|
|
return _slack_message_setup
|
|
|
|
|
|
@patch.object(
|
|
SlackClient,
|
|
"chat_getPermalink",
|
|
return_value=build_slack_response({"ok": True, "permalink": "test_permalink"}),
|
|
)
|
|
@pytest.mark.django_db
|
|
def test_slack_message_permalink(mock_slack_api_call, slack_message_setup):
|
|
slack_message = slack_message_setup(cached_permalink=None)
|
|
assert slack_message.permalink == "test_permalink"
|
|
mock_slack_api_call.assert_called_once()
|
|
|
|
|
|
@patch.object(
|
|
SlackClient,
|
|
"chat_getPermalink",
|
|
side_effect=SlackAPIError(response=build_slack_response({"ok": False, "error": "message_not_found"})),
|
|
)
|
|
@pytest.mark.django_db
|
|
def test_slack_message_permalink_error(mock_slack_api_call, slack_message_setup):
|
|
slack_message = slack_message_setup(cached_permalink=None)
|
|
assert slack_message.permalink is None
|
|
mock_slack_api_call.assert_called_once()
|
|
|
|
|
|
@patch.object(
|
|
SlackClient,
|
|
"chat_getPermalink",
|
|
return_value=build_slack_response({"ok": True, "permalink": "test_permalink"}),
|
|
)
|
|
@pytest.mark.django_db
|
|
def test_slack_message_permalink_cache(mock_slack_api_call, slack_message_setup):
|
|
slack_message = slack_message_setup(cached_permalink="cached_permalink")
|
|
assert slack_message.permalink == "cached_permalink"
|
|
mock_slack_api_call.assert_not_called()
|
|
|
|
|
|
@patch.object(
|
|
SlackClient,
|
|
"chat_getPermalink",
|
|
return_value=build_slack_response({"ok": False, "error": "account_inactive"}),
|
|
)
|
|
@pytest.mark.django_db
|
|
def test_slack_message_permalink_token_revoked(mock_slack_api_call, slack_message_setup):
|
|
slack_message = slack_message_setup(cached_permalink=None)
|
|
slack_message.slack_team_identity.detected_token_revoked = timezone.now()
|
|
slack_message.slack_team_identity.save()
|
|
|
|
assert slack_message.slack_team_identity is not None
|
|
assert slack_message.permalink is None
|
|
|
|
mock_slack_api_call.assert_not_called()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_send_slack_notification(
|
|
make_organization_and_user_with_slack_identities,
|
|
make_alert_receive_channel,
|
|
make_alert_group,
|
|
make_alert,
|
|
make_user_notification_policy,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
):
|
|
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
|
|
|
|
# set up notification policy and alert group
|
|
notification_policy = make_user_notification_policy(
|
|
user,
|
|
UserNotificationPolicy.Step.NOTIFY,
|
|
notify_by=UserNotificationPolicy.NotificationChannel.SLACK,
|
|
)
|
|
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={})
|
|
|
|
slack_channel = make_slack_channel(slack_team_identity)
|
|
slack_message = make_slack_message(slack_channel, alert_group=alert_group)
|
|
|
|
with patch("apps.slack.client.SlackClient.conversations_members") as mock_members:
|
|
mock_members.return_value = {"members": [slack_user_identity.slack_id]}
|
|
slack_message.send_slack_notification(user, alert_group, notification_policy)
|
|
|
|
log_record = notification_policy.personal_log_records.last()
|
|
assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_slack_message_deep_link(
|
|
make_organization_and_user_with_slack_identities,
|
|
make_alert_receive_channel,
|
|
make_alert_group,
|
|
make_alert,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
):
|
|
organization, _, 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)
|
|
make_alert(alert_group=alert_group, raw_request_data={})
|
|
|
|
slack_channel = make_slack_channel(slack_team_identity)
|
|
slack_message = make_slack_message(slack_channel, alert_group=alert_group)
|
|
|
|
expected = (
|
|
f"https://slack.com/app_redirect?channel={slack_channel.slack_id}"
|
|
f"&team={slack_team_identity.slack_id}&message={slack_message.slack_id}"
|
|
)
|
|
assert slack_message.deep_link == expected
|
|
|
|
|
|
class TestSlackMessageUpdateAlertGroupsMessage:
|
|
@patch("apps.slack.models.slack_message.update_alert_group_slack_message")
|
|
@pytest.mark.django_db
|
|
def test_update_alert_groups_message_no_alert_group(
|
|
self,
|
|
mock_update_alert_group_slack_message,
|
|
make_organization_with_slack_team_identity,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
):
|
|
"""
|
|
Test that the method exits early if alert_group is None.
|
|
"""
|
|
_, slack_team_identity = make_organization_with_slack_team_identity()
|
|
slack_channel = make_slack_channel(slack_team_identity)
|
|
slack_message = make_slack_message(slack_channel)
|
|
|
|
slack_message.update_alert_groups_message(debounce=True)
|
|
|
|
# Ensure no task is scheduled
|
|
mock_update_alert_group_slack_message.apply_async.assert_not_called()
|
|
|
|
@patch("apps.slack.models.slack_message.update_alert_group_slack_message")
|
|
@pytest.mark.django_db
|
|
def test_update_alert_groups_message_active_task_exists(
|
|
self,
|
|
mock_update_alert_group_slack_message,
|
|
make_organization_with_slack_team_identity,
|
|
make_alert_receive_channel,
|
|
make_alert_group,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
):
|
|
"""
|
|
Test that the method exits early if a task ID is set in the cache and debounce is True.
|
|
"""
|
|
task_id = "some-task-id"
|
|
|
|
organization, slack_team_identity = make_organization_with_slack_team_identity()
|
|
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(slack_channel, alert_group=alert_group)
|
|
slack_message.set_active_update_task_id(task_id)
|
|
|
|
slack_message.update_alert_groups_message(debounce=True)
|
|
|
|
# Ensure no task is scheduled
|
|
mock_update_alert_group_slack_message.apply_async.assert_not_called()
|
|
|
|
# Ensure task ID in the cache remains unchanged
|
|
assert slack_message.get_active_update_task_id() == task_id
|
|
|
|
@patch("apps.slack.models.slack_message.celery_uuid")
|
|
@patch("apps.slack.models.slack_message.update_alert_group_slack_message")
|
|
@pytest.mark.django_db
|
|
def test_update_alert_groups_message_last_updated_none(
|
|
self,
|
|
mock_update_alert_group_slack_message,
|
|
mock_celery_uuid,
|
|
make_organization_with_slack_team_identity,
|
|
make_alert_receive_channel,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
make_alert_group,
|
|
):
|
|
"""
|
|
Test that the method handles last_updated being None and schedules with default debounce interval.
|
|
"""
|
|
task_id = "some-task-id"
|
|
mock_celery_uuid.return_value = task_id
|
|
|
|
organization, slack_team_identity = make_organization_with_slack_team_identity()
|
|
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(slack_channel, alert_group=alert_group, last_updated=None)
|
|
|
|
assert slack_message.get_active_update_task_id() is None
|
|
|
|
slack_message.update_alert_groups_message(debounce=True)
|
|
|
|
# Verify that apply_async was called with correct countdown
|
|
mock_update_alert_group_slack_message.apply_async.assert_called_once_with(
|
|
(slack_message.pk,),
|
|
countdown=SlackMessage.ALERT_GROUP_UPDATE_DEBOUNCE_INTERVAL_SECONDS,
|
|
task_id=task_id,
|
|
)
|
|
|
|
# Verify task ID is set in the cache
|
|
assert slack_message.get_active_update_task_id() == task_id
|
|
|
|
@patch("apps.slack.models.slack_message.celery_uuid")
|
|
@patch("apps.slack.models.slack_message.update_alert_group_slack_message")
|
|
@pytest.mark.django_db
|
|
def test_update_alert_groups_message_schedules_task_correctly(
|
|
self,
|
|
mock_update_alert_group_slack_message,
|
|
mock_celery_uuid,
|
|
make_organization_with_slack_team_identity,
|
|
make_alert_receive_channel,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
make_alert_group,
|
|
):
|
|
"""
|
|
Test that the method schedules the task with correct countdown and updates the task ID in the cache
|
|
"""
|
|
task_id = "some-task-id"
|
|
mock_celery_uuid.return_value = task_id
|
|
|
|
organization, slack_team_identity = make_organization_with_slack_team_identity()
|
|
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(
|
|
slack_channel,
|
|
alert_group=alert_group,
|
|
last_updated=timezone.now() - timedelta(seconds=10),
|
|
)
|
|
|
|
assert slack_message.get_active_update_task_id() is None
|
|
|
|
slack_message.update_alert_groups_message(debounce=True)
|
|
|
|
# Verify that apply_async was called with correct countdown
|
|
mock_update_alert_group_slack_message.apply_async.assert_called_once_with(
|
|
(slack_message.pk,),
|
|
countdown=35,
|
|
task_id=task_id,
|
|
)
|
|
|
|
# Verify the task ID in the cache is updated to new task_id
|
|
slack_message.refresh_from_db()
|
|
assert slack_message.get_active_update_task_id() == task_id
|
|
|
|
@patch("apps.slack.models.slack_message.celery_uuid")
|
|
@patch("apps.slack.models.slack_message.update_alert_group_slack_message")
|
|
@pytest.mark.django_db
|
|
def test_update_alert_groups_message_handles_minimum_countdown(
|
|
self,
|
|
mock_update_alert_group_slack_message,
|
|
mock_celery_uuid,
|
|
make_organization_with_slack_team_identity,
|
|
make_alert_receive_channel,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
make_alert_group,
|
|
):
|
|
"""
|
|
Test that the countdown is at least 10 seconds when the debounce interval has passed.
|
|
"""
|
|
task_id = "some-task-id"
|
|
mock_celery_uuid.return_value = task_id
|
|
|
|
organization, slack_team_identity = make_organization_with_slack_team_identity()
|
|
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(
|
|
slack_channel,
|
|
alert_group=alert_group,
|
|
last_updated=timezone.now()
|
|
- timedelta(seconds=SlackMessage.ALERT_GROUP_UPDATE_DEBOUNCE_INTERVAL_SECONDS + 1),
|
|
)
|
|
|
|
assert slack_message.get_active_update_task_id() is None
|
|
|
|
slack_message.update_alert_groups_message(debounce=True)
|
|
|
|
# Verify that apply_async was called with correct countdown
|
|
mock_update_alert_group_slack_message.apply_async.assert_called_once_with(
|
|
(slack_message.pk,),
|
|
# Since the time since last update exceeds the debounce interval, countdown should be 10
|
|
countdown=10,
|
|
task_id=task_id,
|
|
)
|
|
|
|
# Verify the task ID in the cache is updated to new task_id
|
|
slack_message.refresh_from_db()
|
|
assert slack_message.get_active_update_task_id() == task_id
|
|
|
|
@patch("apps.slack.models.slack_message.celery_uuid")
|
|
@patch("apps.slack.models.slack_message.update_alert_group_slack_message")
|
|
@pytest.mark.django_db
|
|
def test_update_alert_groups_message_debounce_false_schedules_immediately(
|
|
self,
|
|
mock_update_alert_group_slack_message,
|
|
mock_celery_uuid,
|
|
make_organization_with_slack_team_identity,
|
|
make_alert_receive_channel,
|
|
make_slack_channel,
|
|
make_slack_message,
|
|
make_alert_group,
|
|
):
|
|
"""
|
|
Test that when debounce is False, the task is scheduled immediately with countdown=0,
|
|
even if a task ID is set in the cache.
|
|
"""
|
|
new_task_id = "new-task-id"
|
|
mock_celery_uuid.return_value = new_task_id
|
|
|
|
organization, slack_team_identity = make_organization_with_slack_team_identity()
|
|
alert_receive_channel = make_alert_receive_channel(organization)
|
|
alert_group = make_alert_group(alert_receive_channel)
|
|
slack_channel = make_slack_channel(slack_team_identity)
|
|
|
|
# Set up SlackMessage with existing task ID in the cache
|
|
slack_message = make_slack_message(slack_channel, alert_group=alert_group)
|
|
slack_message.set_active_update_task_id("existing-task-id")
|
|
|
|
slack_message.update_alert_groups_message(debounce=False)
|
|
|
|
# Verify that apply_async was called with countdown=0
|
|
mock_update_alert_group_slack_message.apply_async.assert_called_once_with(
|
|
(slack_message.pk,),
|
|
countdown=0,
|
|
task_id=new_task_id,
|
|
)
|
|
|
|
# Verify the task ID in the cache is updated to new task_id
|
|
slack_message.refresh_from_db()
|
|
assert slack_message.get_active_update_task_id() == new_task_id
|