oncall-engine/engine/apps/slack/tests/test_slack_message.py
Joey Orlando e115617528
chore: drop usage of SlackMessage.organization + drop orphaned SlackMessages (#5330)
# 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.
2024-12-06 11:43:40 -05:00

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