import json from unittest.mock import patch import pytest from django.utils import timezone from apps.alerts.models import AlertReceiveChannel from apps.api.permissions import LegacyAccessControlRole from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb from apps.slack.chatops_proxy_routing import make_private_metadata, make_value from apps.slack.scenarios.paging import ( DIRECT_PAGING_MESSAGE_INPUT_ID, DIRECT_PAGING_ORG_SELECT_ID, DIRECT_PAGING_TEAM_SELECT_ID, DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID, DIRECT_PAGING_USER_SELECT_ID, DataKey, FinishDirectPaging, OnPagingItemActionChange, OnPagingOrgChange, OnPagingTeamChange, OnPagingTeamSeverityCheckboxChange, OnPagingUserChange, Policy, StartDirectPaging, _get_organization_select, _get_team_select_blocks, ) from apps.user_management.models import Organization def make_paging_view_slack_payload( selected_org=None, predefined_org=None, team=None, important_team_escalation=False, user=None, current_users=None, actions=None, ): """ Helper function to create a payload for paging view. Args: selected_org: selected organization predefined_org: predefined organization parsed from chatops-proxy headers team: selected team object. user: selected user object. current_users: Dictionary of current users. actions: List of actions. """ organization = selected_org or predefined_org if organization is None: raise Exception("either selected or predifined org must be defined") private_metadata = { "input_id_prefix": "", "channel_id": "123", "submit_routing_uid": "FinishStepUID", DataKey.USERS: current_users or {}, } if predefined_org: private_metadata["organization_id"] = str(predefined_org.pk) payload = { "channel_id": "123", "trigger_id": "111", "view": { "id": "view-id", "private_metadata": make_private_metadata(private_metadata, organization), "state": { "values": { DIRECT_PAGING_ORG_SELECT_ID: { OnPagingOrgChange.routing_uid(): { "selected_option": { "value": make_value({"id": organization.pk if selected_org else None}, organization) } } }, DIRECT_PAGING_TEAM_SEVERITY_CHECKBOXES_ID: { OnPagingTeamSeverityCheckboxChange.routing_uid(): { "selected_options": [ {"value": "important"}, ] if important_team_escalation else [] }, }, DIRECT_PAGING_TEAM_SELECT_ID: { OnPagingTeamChange.routing_uid(): { "selected_option": {"value": make_value({"id": team.pk if team else None}, organization)} } }, DIRECT_PAGING_USER_SELECT_ID: { OnPagingUserChange.routing_uid(): { "selected_option": {"value": make_value({"id": user.pk}, organization)} if user else None } }, DIRECT_PAGING_MESSAGE_INPUT_ID: {FinishDirectPaging.routing_uid(): {"value": "The Message"}}, } }, }, } if actions is not None: payload["actions"] = actions return payload @pytest.mark.django_db def test_initial_state( make_organization_and_user_with_slack_identities, ): _, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() payload = {"channel_id": "123", "trigger_id": "111"} step = StartDirectPaging(slack_team_identity, user=user) with patch.object(step._slack_client, "views_open") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {} @pytest.mark.django_db def test_org_predefined( make_organization_and_user_with_slack_identities, ): """ See get_org_from_chatops_proxy_header function. """ org, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() payload = {"channel_id": "123", "trigger_id": "111"} step = StartDirectPaging(slack_team_identity, user=user) with patch.object(step._slack_client, "views_open") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload, predefined_org=org) view = mock_slack_api_call.call_args.kwargs["view"] metadata = json.loads(view["private_metadata"]) # Test that organization is injected to private metadata if it is defined by chatops-proxy. assert metadata["organization_id"] == org.pk # Test that organization select is not present if org defined by chatops-proxy. for block in view["blocks"]: if block.get("block_id") == DIRECT_PAGING_ORG_SELECT_ID: raise AssertionError("Organization select block should not be present in the view") @pytest.mark.django_db def test_page_team_with_predefined_org(make_organization_and_user_with_slack_identities, make_team): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() team = make_team(organization) payload = make_paging_view_slack_payload(predefined_org=organization, team=team) step = FinishDirectPaging(slack_team_identity) with patch("apps.slack.scenarios.paging.direct_paging") as mock_direct_paging: with patch.object(step._slack_client, "api_call"): step.process_scenario(slack_user_identity, slack_team_identity, payload) mock_direct_paging.assert_called_once_with( organization=organization, from_user=user, message="The Message", team=team, important_team_escalation=False, users=[], ) @pytest.mark.parametrize("role", (LegacyAccessControlRole.VIEWER, LegacyAccessControlRole.NONE)) @pytest.mark.django_db def test_initial_unauthorized(make_organization_and_user_with_slack_identities, role): _, _, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities(role=role) payload = {"channel_id": "123", "trigger_id": "111"} step = StartDirectPaging(slack_team_identity) with patch.object(step._slack_client, "views_open") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) view = mock_slack_api_call.call_args.kwargs["view"] assert ( view["blocks"][0]["text"]["text"] == ":warning: You do not have permission to perform this action.\nAsk an admin to upgrade your permissions." ) @pytest.mark.parametrize("use_important_policy", (False, True)) @pytest.mark.django_db def test_add_user_no_warning( make_organization_and_user_with_slack_identities, make_schedule, make_on_call_shift, use_important_policy ): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() if use_important_policy: organization.direct_paging_prefer_important_policy = use_important_policy organization.save() # set up schedule: user is on call schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, team=None, ) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) data = { "start": start_date, "rotation_start": start_date, "duration": timezone.timedelta(hours=23, minutes=59, seconds=59), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "schedule": schedule, } on_call_shift = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) on_call_shift.add_rolling_users([[user]]) schedule.refresh_ical_file() schedule.refresh_ical_final_schedule() payload = make_paging_view_slack_payload(selected_org=organization, user=user) step = OnPagingUserChange(slack_team_identity) with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) if use_important_policy: assert metadata[DataKey.USERS] == {str(user.pk): Policy.IMPORTANT} else: assert metadata[DataKey.USERS] == {str(user.pk): Policy.DEFAULT} @pytest.mark.django_db def test_add_user_maximum_exceeded(make_organization_and_user_with_slack_identities, make_schedule, make_on_call_shift): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() # set up schedule: user is on call schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, team=None, ) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) data = { "start": start_date, "rotation_start": start_date, "duration": timezone.timedelta(hours=23, minutes=59, seconds=59), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "schedule": schedule, } on_call_shift = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) on_call_shift.add_rolling_users([[user]]) schedule.refresh_ical_file() schedule.refresh_ical_final_schedule() payload = make_paging_view_slack_payload(selected_org=organization, user=user) step = OnPagingUserChange(slack_team_identity) with patch("apps.slack.scenarios.paging.PRIVATE_METADATA_MAX_LENGTH", 100): with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) view_data = mock_slack_api_call.call_args.kwargs["view"] metadata = json.loads(view_data["private_metadata"]) # metadata unchanged, ignoring the prefix original_metadata = json.loads(payload["view"]["private_metadata"]) metadata.pop("input_id_prefix") original_metadata.pop("input_id_prefix") assert metadata == original_metadata # error message is displayed error_block = { "type": "section", "block_id": "error_message", "text": {"type": "mrkdwn", "text": ":warning: Cannot add user, maximum responders exceeded"}, } assert error_block in view_data["blocks"] @pytest.mark.django_db def test_add_user_raise_warning(make_organization_and_user_with_slack_identities): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() # user is not on call payload = make_paging_view_slack_payload(selected_org=organization, user=user) step = OnPagingUserChange(slack_team_identity) with patch.object(step._slack_client, "views_push") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) assert mock_slack_api_call.call_args.kwargs["view"]["callback_id"] == "OnPagingConfirmUserChange" text_from_blocks = "".join( b["text"]["text"] for b in mock_slack_api_call.call_args.kwargs["view"]["blocks"] if b["type"] == "section" ) assert ( "This user is not currently on-call. We don't recommend to page users outside on-call hours." in text_from_blocks ) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {} @pytest.mark.parametrize("use_important_policy", (False, True)) @pytest.mark.django_db def test_change_user_policy(make_organization_and_user_with_slack_identities, use_important_policy): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() if use_important_policy: organization.direct_paging_prefer_important_policy = use_important_policy organization.save() value = Policy.IMPORTANT if not use_important_policy else Policy.DEFAULT payload = make_paging_view_slack_payload( selected_org=organization, actions=[ { "selected_option": { "value": make_value({"action": value, "key": DataKey.USERS, "id": user.pk}, organization) } } ], ) step = OnPagingItemActionChange(slack_team_identity) with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {str(user.pk): value} @pytest.mark.django_db def test_remove_user(make_organization_and_user_with_slack_identities): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() payload = make_paging_view_slack_payload( selected_org=organization, actions=[ { "selected_option": { "value": make_value( {"action": Policy.REMOVE_ACTION, "key": DataKey.USERS, "id": user.pk}, organization ) } } ], ) step = OnPagingItemActionChange(slack_team_identity) with patch.object(step._slack_client, "views_update") as mock_slack_api_call: step.process_scenario(slack_user_identity, slack_team_identity, payload) metadata = json.loads(mock_slack_api_call.call_args.kwargs["view"]["private_metadata"]) assert metadata[DataKey.USERS] == {} @pytest.mark.django_db def test_trigger_paging_no_team_or_user_selected(make_organization_and_user_with_slack_identities): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() payload = make_paging_view_slack_payload(selected_org=organization) step = FinishDirectPaging(slack_team_identity, user=user) with patch.object(step._slack_client, "api_call"): response = step.process_scenario(slack_user_identity, slack_team_identity, payload) response = response.data assert response["response_action"] == "update" assert ( response["view"]["blocks"][0]["text"]["text"] == ":warning: At least one team or one user must be selected to directly page" ) @pytest.mark.parametrize("role", (LegacyAccessControlRole.VIEWER, LegacyAccessControlRole.NONE)) @pytest.mark.django_db def test_trigger_paging_unauthorized(make_organization_and_user_with_slack_identities, role): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities( role=role ) payload = make_paging_view_slack_payload(selected_org=organization) step = FinishDirectPaging(slack_team_identity) with patch.object(step._slack_client, "api_call"): response = step.process_scenario(slack_user_identity, slack_team_identity, payload) response = response.data assert response["response_action"] == "update" assert ( response["view"]["blocks"][0]["text"]["text"] == ":no_entry: You do not have permission to perform this action." ) @pytest.mark.django_db def test_trigger_paging_additional_responders(make_organization_and_user_with_slack_identities, make_team): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() team = make_team(organization) payload = make_paging_view_slack_payload( selected_org=organization, team=team, current_users={str(user.pk): Policy.IMPORTANT} ) step = FinishDirectPaging(slack_team_identity) with patch("apps.slack.scenarios.paging.direct_paging") as mock_direct_paging: with patch.object(step._slack_client, "api_call"): step.process_scenario(slack_user_identity, slack_team_identity, payload) mock_direct_paging.assert_called_once_with( organization=organization, from_user=user, message="The Message", team=team, important_team_escalation=False, users=[(user, True)], ) @pytest.mark.parametrize("important_team_escalation", [True, False]) @pytest.mark.django_db def test_page_team(make_organization_and_user_with_slack_identities, make_team, important_team_escalation): organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() team = make_team(organization) payload = make_paging_view_slack_payload( selected_org=organization, team=team, important_team_escalation=important_team_escalation, ) step = FinishDirectPaging(slack_team_identity) with patch("apps.slack.scenarios.paging.direct_paging") as mock_direct_paging: with patch.object(step._slack_client, "api_call"): step.process_scenario(slack_user_identity, slack_team_identity, payload) mock_direct_paging.assert_called_once_with( organization=organization, from_user=user, message="The Message", team=team, important_team_escalation=important_team_escalation, users=[], ) @pytest.mark.django_db def test_get_organization_select(make_organization): organization = make_organization(org_title="Organization", stack_slug="stack_slug") select = _get_organization_select(Organization.objects.filter(pk=organization.pk), organization, "test") assert len(select["element"]["options"]) == 1 assert json.loads(select["element"]["options"][0]["value"]) == json.loads( make_value({"id": organization.pk}, organization) ) assert select["element"]["options"][0]["text"]["text"] == "Organization (stack_slug)" @pytest.mark.parametrize("is_team_escalation_important", [True, False]) @pytest.mark.django_db def test_get_team_select_blocks( make_organization_and_user_with_slack_identities, make_team, make_alert_receive_channel, make_escalation_chain, make_channel_filter, is_team_escalation_important, ): info_msg = ( "*Note*: You can only page teams which have a Direct Paging integration that is configured. " "" ) input_id_prefix = "nmxcnvmnxv" def _contstruct_team_option(team): return { "text": {"emoji": True, "text": team.name, "type": "plain_text"}, "value": make_value({"id": team.pk}, organization), } # no team selected - no team direct paging integrations available organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() blocks = _get_team_select_blocks( slack_user_identity, organization, False, None, is_team_escalation_important, input_id_prefix, ) assert len(blocks) == 1 context_block = blocks[0] assert context_block["type"] == "context" assert ( context_block["elements"][0]["text"] == info_msg + ".\n\nThere are currently no teams which have a Direct Paging integration that is configured." ) # no team selected - 1 team direct paging integration available organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() team = make_team(organization) arc = make_alert_receive_channel(organization, team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING) escalation_chain = make_escalation_chain(organization) make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain) blocks = _get_team_select_blocks( slack_user_identity, organization, False, None, is_team_escalation_important, input_id_prefix, ) assert len(blocks) == 2 input_block, context_block = blocks assert input_block["type"] == "input" assert len(input_block["element"]["options"]) == 1 assert input_block["element"]["options"] == [_contstruct_team_option(team)] assert context_block["elements"][0]["text"] == info_msg # team selected - team severity checkbox should also now appear organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() team1 = make_team(organization) team2 = make_team(organization) def _setup_direct_paging_integration(team): arc = make_alert_receive_channel( organization, team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING ) escalation_chain = make_escalation_chain(organization) make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain) return arc _setup_direct_paging_integration(team1) team2_direct_paging_arc = _setup_direct_paging_integration(team2) blocks = _get_team_select_blocks( slack_user_identity, organization, True, team2, is_team_escalation_important, input_id_prefix, ) assert len(blocks) == 4 input_block, context_block, team_severity_checkboxes, team_severity_context_block = blocks team_severity_important_checkbox_option = { "text": { "type": "mrkdwn", "text": "Important escalation", }, "value": "important", } team1_option = _contstruct_team_option(team1) team2_option = _contstruct_team_option(team2) def _sort_team_options(options): return sorted(options, key=lambda o: o["value"]) assert input_block["type"] == "input" assert len(input_block["element"]["options"]) == 2 assert _sort_team_options(input_block["element"]["options"]) == _sort_team_options([team1_option, team2_option]) assert input_block["element"]["initial_option"] == team2_option assert ( context_block["elements"][0]["text"] == f"Integration <{team2_direct_paging_arc.web_link}|{team2_direct_paging_arc.verbal_name}> will be used for notification." ) assert team_severity_context_block["elements"][0]["text"] == ( "Check the above box if you would like to escalate to this team as an 'important' " "escalation. Teams can configure their Direct Paging Integration to route to different " "escalation chains based on this. " "" ) assert team_severity_checkboxes["accessory"]["type"] == "checkboxes" assert team_severity_checkboxes["accessory"]["options"] == [team_severity_important_checkbox_option] if is_team_escalation_important: assert team_severity_checkboxes["accessory"]["initial_options"] == [team_severity_important_checkbox_option] else: assert "initial_options" not in team_severity_checkboxes["accessory"] # team's direct paging integration has two routes associated with it # the team should only be displayed once organization, _, _, slack_user_identity = make_organization_and_user_with_slack_identities() team = make_team(organization) arc = make_alert_receive_channel(organization, team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING) escalation_chain = make_escalation_chain(organization) make_channel_filter(arc, is_default=True, escalation_chain=escalation_chain) make_channel_filter(arc, escalation_chain=escalation_chain) blocks = _get_team_select_blocks( slack_user_identity, organization, False, None, is_team_escalation_important, input_id_prefix, ) assert len(blocks) == 2 input_block, context_block = blocks assert input_block["type"] == "input" assert len(input_block["element"]["options"]) == 1 assert json.loads(input_block["element"]["options"][0]["value"]) == json.loads( _contstruct_team_option(team)["value"] ) assert context_block["elements"][0]["text"] == info_msg