import json from unittest.mock import patch import pytest from django.urls import reverse from django.utils import timezone from rest_framework import status from rest_framework.response import Response from rest_framework.serializers import ValidationError from rest_framework.test import APIClient from apps.alerts.models import EscalationPolicy from apps.api.permissions import LegacyAccessControlRole from apps.api.serializers.user import ScheduleUserSerializer from apps.schedules.ical_utils import memoized_users_in_ical from apps.schedules.models import ( CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb, ) from common.api_helpers.utils import create_engine_url ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics" @pytest.fixture() def schedule_internal_api_setup( make_organization_and_user_with_plugin_token, make_slack_channel, make_schedule, ): organization, user, token = make_organization_and_user_with_plugin_token() slack_channel = make_slack_channel( organization.slack_team_identity, ) calendar_schedule = make_schedule( organization, schedule_class=OnCallScheduleCalendar, name="test_calendar_schedule", ) ical_schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) web_schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) return user, token, calendar_schedule, ical_schedule, web_schedule, slack_channel @pytest.mark.django_db def test_get_list_schedules( schedule_internal_api_setup, make_escalation_chain, make_escalation_policy, make_user_auth_headers ): user, token, calendar_schedule, ical_schedule, web_schedule, slack_channel = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-list") # setup escalation chain linked to web schedule escalation_chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=escalation_chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=web_schedule, ) expected_payload = { "count": 3, "next": None, "previous": None, "results": [ { "id": calendar_schedule.public_primary_key, "type": 0, "team": None, "name": "test_calendar_schedule", "time_zone": "UTC", "slack_channel": None, "user_group": None, "warnings": [], "ical_url_overrides": None, "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, }, { "id": ical_schedule.public_primary_key, "type": 1, "team": None, "name": "test_ical_schedule", "ical_url_primary": ICAL_URL, "ical_url_overrides": None, "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, }, { "id": web_schedule.public_primary_key, "type": 2, "time_zone": "UTC", "team": None, "name": "test_web_schedule", "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 1, "enable_web_overrides": True, }, ], "current_page_number": 1, "page_size": 15, "total_pages": 1, } response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload @pytest.mark.django_db def test_get_list_schedules_pagination( schedule_internal_api_setup, make_escalation_chain, make_escalation_policy, make_user_auth_headers ): user, token, calendar_schedule, ical_schedule, web_schedule, _ = schedule_internal_api_setup # setup escalation chain linked to web schedule escalation_chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=escalation_chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=web_schedule, ) available_schedules = [ { "id": calendar_schedule.public_primary_key, "type": 0, "team": None, "name": "test_calendar_schedule", "time_zone": "UTC", "slack_channel": None, "user_group": None, "warnings": [], "ical_url_overrides": None, "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, }, { "id": ical_schedule.public_primary_key, "type": 1, "team": None, "name": "test_ical_schedule", "ical_url_primary": ICAL_URL, "ical_url_overrides": None, "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, }, { "id": web_schedule.public_primary_key, "type": 2, "time_zone": "UTC", "team": None, "name": "test_web_schedule", "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 1, "enable_web_overrides": True, }, ] client = APIClient() schedule_list_url = reverse("api-internal:schedule-list") absolute_url = create_engine_url(schedule_list_url) for p, schedule in enumerate(available_schedules, start=1): # patch oncall_users to check a paginated queryset is used def mock_oncall_now(qs, events_datetime): # only one schedule is passed here assert qs.count() == 1 return {} url = "{}?page={}&perpage=1".format(schedule_list_url, p) with patch( "apps.schedules.models.on_call_schedule.get_oncall_users_for_multiple_schedules", side_effect=mock_oncall_now, ): response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK previous_url = None next_url = "{}?page={}&perpage=1".format(absolute_url, p + 1) if p == 2: previous_url = "{}?perpage=1".format(absolute_url) elif p > 2: previous_url = "{}?page={}&perpage=1".format(absolute_url, p - 1) next_url = None expected_payload = { "count": 3, "next": next_url, "previous": previous_url, "results": [schedule], "current_page_number": p, "page_size": 1, "total_pages": 3, } assert response.json() == expected_payload @pytest.mark.django_db def test_get_list_schedules_by_type( schedule_internal_api_setup, make_escalation_chain, make_escalation_policy, make_user_auth_headers ): user, token, calendar_schedule, ical_schedule, web_schedule, slack_channel = schedule_internal_api_setup client = APIClient() # setup escalation chain linked to web schedule escalation_chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=escalation_chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=web_schedule, ) available_schedules = [ { "id": calendar_schedule.public_primary_key, "type": 0, "team": None, "name": "test_calendar_schedule", "time_zone": "UTC", "slack_channel": None, "user_group": None, "warnings": [], "ical_url_overrides": None, "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, }, { "id": ical_schedule.public_primary_key, "type": 1, "team": None, "name": "test_ical_schedule", "ical_url_primary": ICAL_URL, "ical_url_overrides": None, "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, }, { "id": web_schedule.public_primary_key, "type": 2, "time_zone": "UTC", "team": None, "name": "test_web_schedule", "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 1, "enable_web_overrides": True, }, ] for schedule_type in range(3): url = reverse("api-internal:schedule-list") + "?type={}".format(schedule_type) response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK expected_payload = { "count": 1, "next": None, "previous": None, "results": [available_schedules[schedule_type]], "current_page_number": 1, "page_size": 15, "total_pages": 1, } assert response.json() == expected_payload # request multiple types url = reverse("api-internal:schedule-list") + "?type=0&type=1" response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK expected_payload = { "count": 2, "next": None, "previous": None, "results": [available_schedules[0], available_schedules[1]], "current_page_number": 1, "page_size": 15, "total_pages": 1, } assert response.json() == expected_payload @pytest.mark.django_db @pytest.mark.parametrize( "query_param, expected_schedule_names", [ ("?used=true", ["test_web_schedule"]), ("?used=false", ["test_calendar_schedule", "test_ical_schedule"]), ("?used=null", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), ("", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), ], ) def test_get_list_schedules_by_used( schedule_internal_api_setup, make_escalation_chain, make_escalation_policy, make_user_auth_headers, query_param, expected_schedule_names, ): user, token, calendar_schedule, ical_schedule, web_schedule, slack_channel = schedule_internal_api_setup client = APIClient() # setup escalation chain linked to web schedule escalation_chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=escalation_chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=web_schedule, ) url = reverse("api-internal:schedule-list") + query_param response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == len(expected_schedule_names) schedule_names = [schedule["name"] for schedule in response.json()["results"]] assert set(schedule_names) == set(expected_schedule_names) @pytest.mark.django_db @pytest.mark.parametrize( "query_param, expected_schedule_names", [ ("?mine=true", ["test_web_schedule"]), ("?mine=false", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), ("?mine=null", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), ("", ["test_calendar_schedule", "test_ical_schedule", "test_web_schedule"]), ], ) def test_get_list_schedules_by_mine( schedule_internal_api_setup, make_user_auth_headers, make_on_call_shift, query_param, expected_schedule_names, ): user, token, calendar_schedule, ical_schedule, web_schedule, slack_channel = schedule_internal_api_setup client = APIClient() today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) # setup user shift in web schedule override_data = { "start": today + timezone.timedelta(hours=22), "rotation_start": today + timezone.timedelta(hours=22), "duration": timezone.timedelta(hours=1), "schedule": web_schedule, } override = make_on_call_shift( organization=user.organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) override.add_rolling_users([[user]]) web_schedule.refresh_ical_file() url = reverse("api-internal:schedule-list") + query_param response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == len(expected_schedule_names) schedule_names = [schedule["name"] for schedule in response.json()["results"]] assert set(schedule_names) == set(expected_schedule_names) @pytest.mark.django_db def test_get_list_schedules_pagination_respects_search( schedule_internal_api_setup, make_escalation_chain, make_escalation_policy, make_user_auth_headers, ): user, token, _, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-list") + "?search=ical" response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.json()["count"] == 1 assert len(response.json()["results"]) == 1 @pytest.mark.django_db def test_get_detail_calendar_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, calendar_schedule, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": calendar_schedule.public_primary_key}) expected_payload = { "id": calendar_schedule.public_primary_key, "type": 0, "team": None, "name": "test_calendar_schedule", "time_zone": "UTC", "slack_channel": None, "user_group": None, "warnings": [], "ical_url_overrides": None, "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, } response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.data == expected_payload @pytest.mark.django_db def test_get_detail_ical_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, ical_schedule, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": ical_schedule.public_primary_key}) expected_payload = { "id": ical_schedule.public_primary_key, "team": None, "ical_url_primary": ICAL_URL, "ical_url_overrides": None, "name": "test_ical_schedule", "type": 1, "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 0, "enable_web_overrides": False, } response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.data == expected_payload @pytest.mark.django_db def test_get_detail_web_schedule( schedule_internal_api_setup, make_escalation_chain, make_escalation_policy, make_user_auth_headers ): user, token, _, _, web_schedule, _ = schedule_internal_api_setup # setup escalation chain linked to web schedule escalation_chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=escalation_chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=web_schedule, ) client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": web_schedule.public_primary_key}) expected_payload = { "id": web_schedule.public_primary_key, "team": None, "name": "test_web_schedule", "type": 2, "time_zone": "UTC", "slack_channel": None, "user_group": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "number_of_escalation_chains": 1, "enable_web_overrides": True, } response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.data == expected_payload @pytest.mark.django_db def test_create_calendar_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-list") data = { "ical_url_overrides": None, "type": 0, "name": "created_calendar_schedule", "time_zone": "UTC", "slack_channel_id": None, "user_group": None, "team": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, "enable_web_overrides": True, } response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) # modify initial data by adding id and None for optional fields schedule = OnCallSchedule.objects.get(public_primary_key=response.data["id"]) data["id"] = schedule.public_primary_key data["number_of_escalation_chains"] = 0 assert response.status_code == status.HTTP_201_CREATED assert response.data == data @pytest.mark.django_db def test_create_ical_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-list") with patch( "apps.api.serializers.schedule_ical.ScheduleICalSerializer.validate_ical_url_primary", return_value=ICAL_URL ): data = { "ical_url_primary": ICAL_URL, "ical_url_overrides": None, "name": "created_ical_schedule", "type": 1, "slack_channel_id": None, "user_group": None, "team": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, } response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) # modify initial data by adding id and None for optional fields schedule = OnCallSchedule.objects.get(public_primary_key=response.data["id"]) data["id"] = schedule.public_primary_key data["number_of_escalation_chains"] = 0 data["enable_web_overrides"] = False assert response.status_code == status.HTTP_201_CREATED assert response.data == data @pytest.mark.django_db def test_create_web_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-list") data = { "name": "created_web_schedule", "type": 2, "time_zone": "UTC", "slack_channel_id": None, "user_group": None, "team": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, } response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) # modify initial data by adding id and None for optional fields schedule = OnCallSchedule.objects.get(public_primary_key=response.data["id"]) data["id"] = schedule.public_primary_key data["number_of_escalation_chains"] = 0 data["enable_web_overrides"] = True assert response.status_code == status.HTTP_201_CREATED assert response.data == data @pytest.mark.django_db def test_create_invalid_ical_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:custom_button-list") with patch( "apps.api.serializers.schedule_ical.ScheduleICalSerializer.validate_ical_url_primary", side_effect=ValidationError("Ical download failed"), ): data = { "ical_url_primary": ICAL_URL, "ical_url_overrides": None, "name": "created_ical_schedule", "type": 1, } response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db @pytest.mark.parametrize("calendar_type", [0, 2]) def test_create_schedule_invalid_time_zone(schedule_internal_api_setup, make_user_auth_headers, calendar_type): user, token, _, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-list") data = { "name": "created_web_schedule", "type": calendar_type, "time_zone": "asdfasdfasdf", "slack_channel_id": None, "user_group": None, "team": None, "warnings": [], "on_call_now": [], "has_gaps": False, "mention_oncall_next": False, "mention_oncall_start": True, "notify_empty_oncall": 0, "notify_oncall_shift_freq": 1, } response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == {"time_zone": ["Invalid timezone"]} @pytest.mark.django_db def test_update_calendar_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, calendar_schedule, _, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": calendar_schedule.public_primary_key}) data = { "name": "updated_calendar_schedule", "type": 0, "team": None, } response = client.put( url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token) ) updated_instance = OnCallSchedule.objects.get(public_primary_key=calendar_schedule.public_primary_key) assert response.status_code == status.HTTP_200_OK assert updated_instance.name == "updated_calendar_schedule" @pytest.mark.django_db def test_update_ical_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, ical_schedule, _, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": ical_schedule.public_primary_key}) data = { "name": "updated_ical_schedule", "type": 1, "team": None, } response = client.put( url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token) ) updated_instance = OnCallSchedule.objects.get(public_primary_key=ical_schedule.public_primary_key) assert response.status_code == status.HTTP_200_OK assert updated_instance.name == "updated_ical_schedule" @pytest.mark.django_db def test_update_web_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, _, _, web_schedule, _ = schedule_internal_api_setup client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": web_schedule.public_primary_key}) data = { "name": "updated_web_schedule", "type": 2, "team": None, } response = client.put( url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token) ) updated_instance = OnCallSchedule.objects.get(public_primary_key=web_schedule.public_primary_key) assert response.status_code == status.HTTP_200_OK assert updated_instance.name == "updated_web_schedule" @pytest.mark.django_db @pytest.mark.parametrize("calendar_type", [0, 2]) def test_update_schedule_invalid_time_zone(schedule_internal_api_setup, make_user_auth_headers, calendar_type): user, token, *calendars, _ = schedule_internal_api_setup schedule = calendars[calendar_type] client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule.public_primary_key}) data = {"name": "updated_web_schedule", "type": calendar_type, "time_zone": "asdfasdfasdf"} response = client.put( url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token) ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == {"time_zone": ["Invalid timezone"]} @pytest.mark.django_db def test_delete_schedule(schedule_internal_api_setup, make_user_auth_headers): user, token, calendar_schedule, ical_schedule, _, _ = schedule_internal_api_setup client = APIClient() for calendar in (calendar_schedule, ical_schedule): url = reverse("api-internal:schedule-detail", kwargs={"pk": calendar.public_primary_key}) response = client.delete(url, **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_204_NO_CONTENT @pytest.mark.django_db def test_events_calendar( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleCalendar, name="test_calendar_schedule", ) start_date = timezone.now().replace(microsecond=0) data = { "start": start_date, "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), "priority_level": 2, } on_call_shift = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, **data ) on_call_shift.users.add(user) schedule.custom_on_call_shifts.add(on_call_shift) url = reverse("api-internal:schedule-events", kwargs={"pk": schedule.public_primary_key}) response = client.get(url, format="json", **make_user_auth_headers(user, token)) expected_result = { "id": schedule.public_primary_key, "name": "test_calendar_schedule", "type": 0, "slack_channel": None, "events": [ { "all_day": False, "start": on_call_shift.start, "end": on_call_shift.start + on_call_shift.duration, "users": [ { "display_name": user.username, "pk": user.public_primary_key, "email": user.email, "avatar_full": user.avatar_full_url, }, ], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, } ], } assert response.status_code == status.HTTP_200_OK assert response.data == expected_result @pytest.mark.django_db def test_filter_events_calendar( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) 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=7200), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, "by_day": ["MO", "FR"], "schedule": schedule, } on_call_shift = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data ) on_call_shift.users.add(user) url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?type=rotation" response = client.get(url, format="json", **make_user_auth_headers(user, token)) # current week events are expected mon_start = now - timezone.timedelta(days=start_date.weekday()) fri_start = mon_start + timezone.timedelta(days=4) expected_result = { "id": schedule.public_primary_key, "name": "test_web_schedule", "type": 2, "events": [ { "all_day": False, "start": mon_start, "end": mon_start + on_call_shift.duration, "users": [ { "display_name": user.username, "pk": user.public_primary_key, "email": user.email, "avatar_full": user.avatar_full_url, }, ], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, }, { "all_day": False, "start": fri_start, "end": fri_start + on_call_shift.duration, "users": [ { "display_name": user.username, "pk": user.public_primary_key, "email": user.email, "avatar_full": user.avatar_full_url, } ], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, }, ], } assert response.status_code == status.HTTP_200_OK assert response.data == expected_result @pytest.mark.django_db def test_filter_events_range_calendar( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) now = timezone.now().replace(microsecond=0) start_date = now - timezone.timedelta(days=7) mon_start = now - timezone.timedelta(days=start_date.weekday()) request_date = mon_start + timezone.timedelta(days=2) data = { "start": start_date, "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, "by_day": ["MO", "FR"], "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]]) # add override shift override_start = request_date + timezone.timedelta(seconds=3600) override_data = { "start": override_start, "rotation_start": override_start, "duration": timezone.timedelta(seconds=3600), "schedule": schedule, } override = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) other_user = make_user_for_organization(organization) override.users.add(other_user) url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?date={}&days=3&type=rotation".format(request_date.strftime("%Y-%m-%d")) response = client.get(url, format="json", **make_user_auth_headers(user, token)) # only friday occurrence is expected fri_start = mon_start + timezone.timedelta(days=4) expected_result = { "id": schedule.public_primary_key, "name": "test_web_schedule", "type": 2, "events": [ { "all_day": False, "start": fri_start, "end": fri_start + on_call_shift.duration, "users": [ { "display_name": user.username, "pk": user.public_primary_key, "email": user.email, "avatar_full": user.avatar_full_url, }, ], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", "calendar_type": OnCallSchedule.PRIMARY, "is_empty": False, "is_gap": False, "is_override": False, "shift": { "pk": on_call_shift.public_primary_key, }, } ], } assert response.status_code == status.HTTP_200_OK assert response.data == expected_result @pytest.mark.django_db def test_filter_events_overrides( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) now = timezone.now().replace(microsecond=0) start_date = now - timezone.timedelta(days=7) mon_start = now - timezone.timedelta(days=start_date.weekday()) request_date = mon_start + timezone.timedelta(days=2) data = { "start": start_date, "rotation_start": start_date, "duration": timezone.timedelta(seconds=7200), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, "by_day": ["MO", "FR"], "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]]) # add override shift override_start = request_date + timezone.timedelta(seconds=3600) override_data = { "start": override_start, "rotation_start": override_start, "duration": timezone.timedelta(seconds=3600), "schedule": schedule, } override = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) other_user = make_user_for_organization(organization) override.add_rolling_users([[other_user]]) url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?date={}&days=3&type=override".format(request_date.strftime("%Y-%m-%d")) response = client.get(url, format="json", **make_user_auth_headers(user, token)) # only override occurrence is expected expected_result = { "id": schedule.public_primary_key, "name": "test_web_schedule", "type": 2, "events": [ { "all_day": False, "start": override_start, "end": override_start + override.duration, "users": [ { "display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email, "avatar_full": other_user.avatar_full_url, } ], "missing_users": [], "priority_level": None, "source": "api", "calendar_type": OnCallSchedule.OVERRIDES, "is_empty": False, "is_gap": False, "is_override": True, "shift": { "pk": override.public_primary_key, }, } ], } assert response.status_code == status.HTTP_200_OK assert response.data == expected_result @pytest.mark.django_db def test_filter_events_final_schedule( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) request_date = start_date user_a, user_b, user_c, user_d, user_e = (make_user_for_organization(organization, username=i) for i in "ABCDE") # clear users pks <-> organization cache (persisting between tests) memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) (user_a, 1, 10, 5), # r1-1: 10-15 / A (user_b, 1, 11, 2), # r1-2: 11-13 / B (user_a, 1, 16, 3), # r1-3: 16-19 / A (user_a, 1, 21, 1), # r1-4: 21-22 / A (user_b, 1, 22, 2), # r1-5: 22-00 / B (user_c, 2, 12, 2), # r2-1: 12-14 / C (user_d, 2, 14, 1), # r2-2: 14-15 / D (user_d, 2, 17, 1), # r2-3: 17-18 / D (user_d, 2, 20, 3), # r2-4: 20-23 / D ) for user, priority, start_h, duration in shifts: data = { "start": start_date + timezone.timedelta(hours=start_h), "rotation_start": start_date, "duration": timezone.timedelta(hours=duration), "priority_level": priority, "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]]) # override: 22-23 / E override_data = { "start": start_date + timezone.timedelta(hours=22), "rotation_start": start_date + timezone.timedelta(hours=22), "duration": timezone.timedelta(hours=1), "schedule": schedule, } override = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) override.add_rolling_users([[user_e]]) url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?date={}&days=1".format(request_date.strftime("%Y-%m-%d")) response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK expected = ( # start (h), duration (H), user, priority, is_gap, is_override (0, 10, None, None, True, False), # 0-10 gap (10, 2, "A", 1, False, False), # 10-12 A (11, 1, "B", 1, False, False), # 11-12 B (12, 2, "C", 2, False, False), # 12-14 C (14, 1, "D", 2, False, False), # 14-15 D (15, 1, None, None, True, False), # 15-16 gap (16, 1, "A", 1, False, False), # 16-17 A (17, 1, "D", 2, False, False), # 17-18 D (18, 1, "A", 1, False, False), # 18-19 A (19, 1, None, None, True, False), # 19-20 gap (20, 2, "D", 2, False, False), # 20-22 D (22, 1, "E", None, False, True), # 22-23 E (override) (23, 1, "B", 1, False, False), # 23-00 B ) expected_events = [ { "calendar_type": 1 if is_override else None if is_gap else 0, "end": start_date + timezone.timedelta(hours=start + duration), "is_gap": is_gap, "is_override": is_override, "priority_level": priority, "start": start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0), "user": user, } for start, duration, user, priority, is_gap, is_override in expected ] returned_events = [ { "calendar_type": e["calendar_type"], "end": e["end"], "is_gap": e["is_gap"], "is_override": e["is_override"], "priority_level": e["priority_level"], "start": e["start"], "user": e["users"][0]["display_name"] if e["users"] else None, } for e in response.data["events"] ] assert returned_events == expected_events @pytest.mark.django_db def test_next_shifts_per_user( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, admin, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timezone.timedelta(days=1) user_a, user_b, user_c, user_d = (make_user_for_organization(organization, username=i) for i in "ABCD") # clear users pks <-> organization cache (persisting between tests) memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) (user_a, 1, 8, 2), # r1-1: 8-10 / A (user_a, 1, 15, 2), # r1-2: 15-17 / A (user_b, 2, 7, 5), # r2-1: 7-12 / B (user_b, 2, 16, 2), # r2-2: 16-18 / B (user_c, 2, 18, 2), # r2-3: 18-20 / C ) for user, priority, start_h, duration in shifts: data = { "start": tomorrow + timezone.timedelta(hours=start_h), "rotation_start": tomorrow + timezone.timedelta(hours=start_h), "duration": timezone.timedelta(hours=duration), "priority_level": priority, "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]]) # override in the past: 17-18 / D # won't be listed, but user D will still be included in the response override_data = { "start": tomorrow - timezone.timedelta(days=3), "rotation_start": tomorrow - timezone.timedelta(days=3), "duration": timezone.timedelta(hours=1), "schedule": schedule, } override = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) override.add_rolling_users([[user_d]]) # override: 17-18 / C override_data = { "start": tomorrow + timezone.timedelta(hours=17), "rotation_start": tomorrow + timezone.timedelta(hours=17), "duration": timezone.timedelta(hours=1), "schedule": schedule, } override = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) override.add_rolling_users([[user_c]]) # final schedule: 7-12: B, 15-16: A, 16-17: B, 17-18: C (override), 18-20: C url = reverse("api-internal:schedule-next-shifts-per-user", kwargs={"pk": schedule.public_primary_key}) response = client.get(url, format="json", **make_user_auth_headers(admin, token)) assert response.status_code == status.HTTP_200_OK expected = { user_a.public_primary_key: (tomorrow + timezone.timedelta(hours=15), tomorrow + timezone.timedelta(hours=16)), user_b.public_primary_key: (tomorrow + timezone.timedelta(hours=7), tomorrow + timezone.timedelta(hours=12)), user_c.public_primary_key: (tomorrow + timezone.timedelta(hours=17), tomorrow + timezone.timedelta(hours=18)), user_d.public_primary_key: None, } returned_data = { u: (ev["start"], ev["end"]) if ev is not None else None for u, ev in response.data["users"].items() } assert returned_data == expected @pytest.mark.django_db def test_related_users( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, admin, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timezone.timedelta(days=1) user_a, user_b, user_c, _ = (make_user_for_organization(organization, username=i) for i in "ABCD") # clear users pks <-> organization cache (persisting between tests) memoized_users_in_ical.cache_clear() shifts = ( # user, priority, start time (h), duration (hs) (user_a, 1, 8, 2), # r1-1: 8-10 / A (user_b, 2, 16, 2), # r2-2: 16-18 / B ) for user, priority, start_h, duration in shifts: data = { "start": tomorrow + timezone.timedelta(hours=start_h), "rotation_start": tomorrow + timezone.timedelta(hours=start_h), "duration": timezone.timedelta(hours=duration), "priority_level": priority, "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]]) # override: 17-18 / C override_data = { "start": tomorrow + timezone.timedelta(hours=17), "rotation_start": tomorrow + timezone.timedelta(hours=17), "duration": timezone.timedelta(hours=1), "schedule": schedule, } override = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data ) override.add_rolling_users([[user_c]]) schedule.refresh_ical_file() url = reverse("api-internal:schedule-related-users", kwargs={"pk": schedule.public_primary_key}) response = client.get(url, format="json", **make_user_auth_headers(admin, token)) assert response.status_code == status.HTTP_200_OK expected = [ScheduleUserSerializer(u).data for u in (user_a, user_b, user_c)] assert sorted(response.data["users"], key=lambda u: u["username"]) == sorted(expected, key=lambda u: u["username"]) @pytest.mark.django_db def test_related_escalation_chains( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, make_escalation_chain, make_escalation_policy, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) # setup escalation chains linked to web schedule escalation_chains = [] for i in range(3): chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=schedule, ) escalation_chains.append(chain) # setup other unrelated schedule other_schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) other_chain = make_escalation_chain(user.organization) make_escalation_policy( escalation_chain=other_chain, escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE, notify_schedule=other_schedule, ) url = reverse("api-internal:schedule-related-escalation-chains", kwargs={"pk": schedule.public_primary_key}) response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK expected = [{"name": chain.name, "pk": chain.public_primary_key} for chain in escalation_chains] assert sorted(response.data, key=lambda e: e["name"]) == sorted(expected, key=lambda e: e["name"]) @pytest.mark.django_db def test_merging_same_shift_events( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers, make_schedule, make_on_call_shift, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) # tomorrow request_date = now + timezone.timedelta(days=1) user_a = make_user_for_organization(organization) user_b = make_user_for_organization(organization) user_c = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) data = { "start": start_date + timezone.timedelta(hours=10), "rotation_start": start_date + timezone.timedelta(hours=10), "duration": timezone.timedelta(hours=2), "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_a, user_c, user_b]]) expected_users = { "users": sorted([user_a.username, user_b.username]), "missing_users": [user_c.username], } # final schedule url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?date={}&days=3".format(request_date.strftime("%Y-%m-%d")) response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK returned_users = [ { "users": sorted([u["display_name"] for u in e["users"]]) if e["users"] else None, "missing_users": e["missing_users"], } for e in response.data["events"] if not e["is_gap"] ] for users in returned_users: assert users == expected_users # rotations url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?date={}&days=3&type=rotation".format(request_date.strftime("%Y-%m-%d")) response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK returned_users = [ { "users": sorted([u["display_name"] for u in e["users"]]) if e["users"] else None, "missing_users": e["missing_users"], } for e in response.data["events"] if not e["is_gap"] ] for users in returned_users: assert users == expected_users @pytest.mark.django_db def test_filter_events_invalid_type( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, ): organization, user, token = make_organization_and_user_with_plugin_token() client = APIClient() schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) url = reverse("api-internal:schedule-filter-events", kwargs={"pk": schedule.public_primary_key}) url += "?type=invalid" response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_create_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-list") with patch( "apps.api.views.schedule.ScheduleView.create", return_value=Response( status=status.HTTP_200_OK, ), ): response = client.post(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_update_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule.public_primary_key}) with patch( "apps.api.views.schedule.ScheduleView.update", return_value=Response( status=status.HTTP_200_OK, ), ): response = client.put(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status response = client.patch(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_list_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-list") with patch( "apps.api.views.schedule.ScheduleView.list", return_value=Response( status=status.HTTP_200_OK, ), ): response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_retrieve_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule.public_primary_key}) with patch( "apps.api.views.schedule.ScheduleView.retrieve", return_value=Response( status=status.HTTP_200_OK, ), ): response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_delete_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule.public_primary_key}) with patch( "apps.api.views.schedule.ScheduleView.destroy", return_value=Response( status=status.HTTP_204_NO_CONTENT, ), ): response = client.delete(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_events_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-events", kwargs={"pk": schedule.public_primary_key}) with patch( "apps.api.views.schedule.ScheduleView.events", return_value=Response( status=status.HTTP_200_OK, ), ): response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_reload_ical_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, name="test_ical_schedule", ical_url_primary=ICAL_URL, ) client = APIClient() url = reverse("api-internal:schedule-reload-ical", kwargs={"pk": schedule.public_primary_key}) with patch( "apps.api.views.schedule.ScheduleView.reload_ical", return_value=Response( status=status.HTTP_200_OK, ), ): response = client.post(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_notify_oncall_shift_freq_options_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:schedule-notify-oncall-shift-freq-options") client = APIClient() response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_notify_empty_oncall_options_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:schedule-notify-empty-oncall-options") client = APIClient() response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_mention_options_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, make_schedule, role, expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:schedule-mention-options") client = APIClient() response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db def test_get_schedule_from_other_team_with_flag( make_organization_and_user_with_plugin_token, make_team, make_user_auth_headers, make_schedule, ): organization, user, token = make_organization_and_user_with_plugin_token() team = make_team(organization) calendar_schedule = make_schedule( organization, schedule_class=OnCallScheduleCalendar, name="test_calendar_schedule", team=team, ) client = APIClient() url = reverse("api-internal:schedule-detail", kwargs={"pk": calendar_schedule.public_primary_key}) url = f"{url}?from_organization=true" response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK @pytest.mark.django_db def test_get_schedule_on_call_now( make_organization, make_user_for_organization, make_token_for_organization, make_schedule, make_user_auth_headers ): organization = make_organization(grafana_url="https://example.com") user = make_user_for_organization(organization, username="test", avatar_url="/avatar/test123") _, token = make_token_for_organization(organization) schedule = make_schedule( organization, schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) client = APIClient() url = reverse("api-internal:schedule-list") with patch( "apps.schedules.models.on_call_schedule.OnCallScheduleQuerySet.get_oncall_users", return_value={schedule.pk: [user]}, ): response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK assert response.json()["results"][0]["on_call_now"] == [ { "pk": user.public_primary_key, "username": "test", "avatar": "/avatar/test123", "avatar_full": "https://example.com/avatar/test123", } ]