chore: add more slack related tests (#5227)

Follow up test PR to https://github.com/grafana/oncall/pull/5199 and
https://github.com/grafana/oncall/pull/5224
This commit is contained in:
Joey Orlando 2024-11-04 15:49:22 -05:00 committed by GitHub
parent 4a5c4263e0
commit 14c1b37c87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 318 additions and 24 deletions

View file

@ -81,7 +81,7 @@ class SlackChannelArchivedEventStep(scenario_step.ScenarioStep):
clean_slack_channel_leftovers.apply_async((slack_team_identity.id, slack_id))
class SlackChannelUnArchivedEventStep(scenario_step.ScenarioStep):
class SlackChannelUnarchivedEventStep(scenario_step.ScenarioStep):
def process_scenario(
self,
slack_user_identity: "SlackUserIdentity",
@ -126,6 +126,6 @@ STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
{
"payload_type": PayloadType.EVENT_CALLBACK,
"event_type": EventType.CHANNEL_UNARCHIVED,
"step": SlackChannelUnArchivedEventStep,
"step": SlackChannelUnarchivedEventStep,
},
]

View file

@ -1,6 +1,6 @@
import logging
import random
from typing import Optional
import typing
from celery import uuid as celery_uuid
from celery.exceptions import Retry
@ -433,7 +433,7 @@ def populate_slack_channels():
def start_populate_slack_channels_for_team(
slack_team_identity_id: int, delay: int, cursor: Optional[str] = None
slack_team_identity_id: int, delay: int, cursor: typing.Optional[str] = None
) -> None:
# save active task id in cache to make only one populate task active per team
task_id = celery_uuid()
@ -445,7 +445,7 @@ def start_populate_slack_channels_for_team(
@shared_dedicated_queue_retry_task(
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
)
def populate_slack_channels_for_team(slack_team_identity_id: int, cursor: Optional[str] = None) -> None:
def populate_slack_channels_for_team(slack_team_identity_id: int, cursor: typing.Optional[str] = None) -> None:
"""
Make paginated request to get slack channels. On ratelimit - update info for got channels, save collected channels
ids in cache and restart the task with the last successful pagination cursor to avoid any data loss during delay
@ -539,7 +539,7 @@ def populate_slack_channels_for_team(slack_team_identity_id: int, cursor: Option
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=0)
def clean_slack_integration_leftovers(organization_id, *args, **kwargs):
def clean_slack_integration_leftovers(organization_id: int, *args, **kwargs) -> None:
"""
This task removes binding to slack (e.g ChannelFilter's slack channel) for a given organization.
It is used when user changes slack integration.
@ -549,11 +549,11 @@ def clean_slack_integration_leftovers(organization_id, *args, **kwargs):
logger.info(f"Cleaning up for organization {organization_id}")
ChannelFilter.objects.filter(alert_receive_channel__organization_id=organization_id).update(slack_channel=None)
OnCallSchedule.objects.filter(organization_id=organization_id).update(channel=None, user_group=None)
OnCallSchedule.objects.filter(organization_id=organization_id).update(slack_channel=None, user_group=None)
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=10)
def clean_slack_channel_leftovers(slack_team_identity_id, slack_channel_id):
def clean_slack_channel_leftovers(slack_team_identity_id: int, slack_channel_id: str) -> None:
"""
This task removes binding to slack channel after a channel is archived in Slack.
@ -561,7 +561,11 @@ def clean_slack_channel_leftovers(slack_team_identity_id, slack_channel_id):
to that channel via `on_delete=models.SET_NULL`.
"""
from apps.alerts.models import ChannelFilter
from apps.schedules.models import OnCallSchedule
from apps.slack.models import SlackTeamIdentity
from apps.user_management.models import Organization
orgs_to_clean_default_slack_channel: typing.List[Organization] = []
try:
sti = SlackTeamIdentity.objects.get(id=slack_team_identity_id)
@ -572,6 +576,25 @@ def clean_slack_channel_leftovers(slack_team_identity_id, slack_channel_id):
return
for org in sti.organizations.all():
ChannelFilter.objects.filter(alert_receive_channel__organization=org, slack_channel_id=slack_channel_id).update(
slack_channel_id=None
)
org_id = org.id
if org.default_slack_channel_slack_id == slack_channel_id:
logger.info(
f"Set default_slack_channel to None for org_id={org_id} slack_channel_id={slack_channel_id} since slack_channel is arcived or deleted"
)
org.default_slack_channel = None
orgs_to_clean_default_slack_channel.append(org)
# The channel no longer exists, so update any integration routes (ie. ChannelFilter) or schedules
# that reference it
ChannelFilter.objects.filter(
alert_receive_channel__organization=org,
slack_channel__slack_id=slack_channel_id,
).update(slack_channel=None)
OnCallSchedule.objects.filter(
organization_id=org_id,
slack_channel__slack_id=slack_channel_id,
).update(slack_channel=None)
Organization.objects.bulk_update(orgs_to_clean_default_slack_channel, ["default_slack_channel"], batch_size=5000)

View file

@ -0,0 +1,217 @@
from unittest.mock import patch
import pytest
from django.utils import timezone
from apps.slack.models import SlackChannel
from apps.slack.scenarios import slack_channel as slack_channel_scenarios
@pytest.mark.django_db
class TestSlackChannelCreatedOrRenamedEventStep:
def test_process_scenario_channel_created(
self,
make_organization_and_user_with_slack_identities,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel_id = "C12345678"
channel_name = "new-channel"
payload = {
"event": {
"channel": {
"id": slack_channel_id,
"name": channel_name,
}
}
}
# Ensure the SlackChannel does not exist
assert not SlackChannel.objects.filter(
slack_id=slack_channel_id,
slack_team_identity=slack_team_identity,
).exists()
step = slack_channel_scenarios.SlackChannelCreatedOrRenamedEventStep(slack_team_identity, organization, user)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
# Now the SlackChannel should exist with correct data
slack_channel = SlackChannel.objects.get(
slack_id=slack_channel_id,
slack_team_identity=slack_team_identity,
)
assert slack_channel.name == channel_name
assert slack_channel.last_populated == timezone.now().date()
def test_process_scenario_channel_renamed(
self,
make_organization_and_user_with_slack_identities,
make_slack_channel,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel = make_slack_channel(slack_team_identity)
slack_channel_id = slack_channel.slack_id
new_name = "renamed-channel"
payload = {
"event": {
"channel": {
"id": slack_channel_id,
"name": new_name,
}
}
}
step = slack_channel_scenarios.SlackChannelCreatedOrRenamedEventStep(slack_team_identity, organization, user)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
slack_channel.refresh_from_db()
assert slack_channel.name == new_name
assert slack_channel.last_populated == timezone.now().date()
@pytest.mark.django_db
class TestSlackChannelDeletedEventStep:
def test_process_scenario_channel_deleted(
self,
make_organization_and_user_with_slack_identities,
make_slack_channel,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel = make_slack_channel(slack_team_identity)
slack_channel_id = slack_channel.slack_id
# Ensure the SlackChannel exists
assert SlackChannel.objects.filter(
slack_id=slack_channel_id,
slack_team_identity=slack_team_identity,
).exists()
step = slack_channel_scenarios.SlackChannelDeletedEventStep(slack_team_identity, organization, user)
step.process_scenario(slack_user_identity, slack_team_identity, {"event": {"channel": slack_channel_id}})
# Now the SlackChannel should not exist
assert not SlackChannel.objects.filter(
slack_id=slack_channel_id,
slack_team_identity=slack_team_identity,
).exists()
def test_process_scenario_channel_does_not_exist(
self,
make_organization_and_user_with_slack_identities,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel_id = "C12345678"
# Ensure the SlackChannel does not exist
assert not SlackChannel.objects.filter(
slack_id=slack_channel_id,
slack_team_identity=slack_team_identity,
).exists()
step = slack_channel_scenarios.SlackChannelDeletedEventStep(slack_team_identity, organization, user)
# The step should not raise an exception even if the channel does not exist
step.process_scenario(slack_user_identity, slack_team_identity, {"event": {"channel": slack_channel_id}})
# Still, the SlackChannel does not exist
assert not SlackChannel.objects.filter(
slack_id=slack_channel_id,
slack_team_identity=slack_team_identity,
).exists()
@pytest.mark.django_db
class TestSlackChannelArchivedEventStep:
@patch("apps.slack.scenarios.slack_channel.clean_slack_channel_leftovers")
def test_process_scenario(
self,
mock_clean_slack_channel_leftovers,
make_organization_and_user_with_slack_identities,
make_slack_channel,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel = make_slack_channel(slack_team_identity)
slack_channel_id = slack_channel.slack_id
assert slack_channel.is_archived is False
step = slack_channel_scenarios.SlackChannelArchivedEventStep(slack_team_identity, organization, user)
step.process_scenario(slack_user_identity, slack_team_identity, {"event": {"channel": slack_channel_id}})
slack_channel.refresh_from_db()
assert slack_channel.is_archived is True
mock_clean_slack_channel_leftovers.apply_async.assert_called_once_with(
(slack_team_identity.id, slack_channel_id)
)
@pytest.mark.django_db
class TestSlackChannelUnarchivedEventStep:
def test_process_scenario_channel_unarchived(
self,
make_organization_and_user_with_slack_identities,
make_slack_channel,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel = make_slack_channel(slack_team_identity, is_archived=True)
slack_channel_id = slack_channel.slack_id
assert slack_channel.is_archived is True
step = slack_channel_scenarios.SlackChannelUnarchivedEventStep(slack_team_identity, organization, user)
step.process_scenario(slack_user_identity, slack_team_identity, {"event": {"channel": slack_channel_id}})
slack_channel.refresh_from_db()
assert slack_channel.is_archived is False
def test_process_scenario_channel_already_unarchived(
self,
make_organization_and_user_with_slack_identities,
make_slack_channel,
) -> None:
(
organization,
user,
slack_team_identity,
slack_user_identity,
) = make_organization_and_user_with_slack_identities()
slack_channel = make_slack_channel(slack_team_identity, is_archived=False)
slack_channel_id = slack_channel.slack_id
assert slack_channel.is_archived is False
step = slack_channel_scenarios.SlackChannelUnarchivedEventStep(slack_team_identity, organization, user)
step.process_scenario(slack_user_identity, slack_team_identity, {"event": {"channel": slack_channel_id}})
slack_channel.refresh_from_db()
# Ensure that is_archived remains False
assert slack_channel.is_archived is False

View file

@ -9,7 +9,11 @@ from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
from apps.schedules.models import OnCallScheduleWeb
from apps.slack.tasks import clean_slack_integration_leftovers, unpopulate_slack_user_identities
from apps.slack.tasks import (
clean_slack_channel_leftovers,
clean_slack_integration_leftovers,
unpopulate_slack_user_identities,
)
from apps.user_management.models import User
@ -38,6 +42,53 @@ def test_reset_slack_integration_permissions(
assert response.status_code == expected_status
@pytest.mark.django_db
def test_clean_slack_channel_leftovers(
make_slack_team_identity,
make_slack_channel,
make_organization,
make_alert_receive_channel,
make_channel_filter,
make_slack_user_group,
make_schedule,
):
slack_team_identity = make_slack_team_identity()
slack_channel = make_slack_channel(slack_team_identity)
organization = make_organization(slack_team_identity=slack_team_identity, default_slack_channel=slack_channel)
# create channel filter with Slack channel
alert_receive_channel = make_alert_receive_channel(organization)
channel_filter = make_channel_filter(alert_receive_channel, slack_channel=slack_channel)
# create schedule with Slack channel and user group
user_group = make_slack_user_group(slack_team_identity)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleWeb,
slack_channel=slack_channel,
user_group=user_group,
)
assert channel_filter.slack_channel == slack_channel
assert schedule.slack_channel == slack_channel
assert schedule.user_group == user_group
assert organization.default_slack_channel_slack_id == slack_channel.slack_id
# clean Slack channel leftovers
clean_slack_channel_leftovers(slack_team_identity.pk, slack_channel.slack_id)
channel_filter.refresh_from_db()
schedule.refresh_from_db()
organization.refresh_from_db()
# check that references to Slack objects are removed
assert channel_filter.slack_channel is None
assert organization.default_slack_channel is None
# NOTE: user groups shouldn't be updated for schedules, only the channel
assert schedule.slack_channel is None
assert schedule.user_group == user_group
@pytest.mark.django_db
def test_clean_slack_integration_leftovers(
make_slack_team_identity,
@ -58,11 +109,16 @@ def test_clean_slack_integration_leftovers(
# create schedule with Slack channel and user group
user_group = make_slack_user_group(slack_team_identity)
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, channel="test", user_group=user_group)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleWeb,
slack_channel=slack_channel,
user_group=user_group,
)
assert channel_filter.slack_channel is not None
assert schedule.channel is not None
assert schedule.user_group is not None
assert channel_filter.slack_channel == slack_channel
assert schedule.slack_channel == slack_channel
assert schedule.user_group == user_group
# clean Slack integration leftovers
clean_slack_integration_leftovers(organization.pk)
@ -71,7 +127,7 @@ def test_clean_slack_integration_leftovers(
# check that references to Slack objects are removed
assert channel_filter.slack_channel is None
assert schedule.channel is None
assert schedule.slack_channel is None
assert schedule.user_group is None
@ -117,24 +173,22 @@ def test_delete_slack_channel_and_cascade_deletes(
make_organization,
make_alert_receive_channel,
make_channel_filter,
# make_schedule,
make_schedule,
):
# TODO: add the schedule related bits once https://github.com/grafana/oncall/pull/5199 is merged
slack_team_identity = make_slack_team_identity()
slack_channel = make_slack_channel(slack_team_identity)
organization = make_organization(slack_team_identity=slack_team_identity, default_slack_channel=slack_channel)
alert_receive_channel = make_alert_receive_channel(organization)
channel_filter = make_channel_filter(alert_receive_channel, slack_channel=slack_channel)
# schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, slack_channel=slack_channel)
assert channel_filter.slack_channel == slack_channel
# assert schedule.slack_channel == slack_channel
assert schedule.slack_channel == slack_channel
slack_channel.delete()
channel_filter.refresh_from_db()
# schedule.refresh_from_db()
schedule.refresh_from_db()
assert channel_filter.slack_channel is None
# assert schedule.slack_channel is None
assert schedule.slack_channel is None