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.test import APIClient from apps.api.permissions import LegacyAccessControlRole from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleWeb @pytest.fixture() def on_call_shift_internal_api_setup( make_organization_and_user_with_plugin_token, make_schedule, make_user_for_organization, ): organization, first_user, token = make_organization_and_user_with_plugin_token() second_user = make_user_for_organization(organization) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) return token, first_user, second_user, organization, schedule @pytest.mark.django_db def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) data = { "name": "Test Shift", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 1, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": 1, "interval": 1, "by_day": [ CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.FRIDAY], ], "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], } with patch("apps.schedules.models.CustomOnCallShift.refresh_schedule") as mock_refresh_schedule: response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) expected_payload = data | { "id": response.data["id"], "updated_shift": None, "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_201_CREATED assert response.json() == expected_payload assert mock_refresh_schedule.called @pytest.mark.django_db def test_create_on_call_shift_rotation_invalid_type( make_organization_and_user_with_plugin_token, make_schedule, make_user_auth_headers, ): organization, user, token = make_organization_and_user_with_plugin_token() schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) data = { "name": "Test Shift", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 1, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": 1, "interval": 1, "by_day": [ CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.FRIDAY], ], "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user.public_primary_key]], } with patch("apps.schedules.models.CustomOnCallShift.refresh_schedule") as mock_refresh_schedule: response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["type"][0] == "Invalid event type" assert not mock_refresh_schedule.called @pytest.mark.django_db def test_create_on_call_shift_rotation_missing_users(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) data = { "name": "Test Shift", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 1, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": 1, "interval": 1, "by_day": [ CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.FRIDAY], ], "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["rolling_users"][0] == "User(s) are required" @pytest.mark.django_db def test_create_on_call_shift_override(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) data = { "name": "Test Shift Override", "type": CustomOnCallShift.TYPE_OVERRIDE, "schedule": schedule.public_primary_key, "priority_level": 99, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key, user2.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) returned_rolling_users = response.data["rolling_users"] assert len(returned_rolling_users) == 1 assert sorted(returned_rolling_users[0]) == sorted(data["rolling_users"][0]) expected_payload = data | { "id": response.data["id"], "updated_shift": None, "rolling_users": returned_rolling_users, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_201_CREATED assert response.json() == expected_payload @pytest.mark.django_db def test_get_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = timezone.now().replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}, {user2.pk: user2.public_primary_key}], ) url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.get(url, format="json", **make_user_auth_headers(user1, token)) expected_payload = { "id": response.data["id"], "name": name, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload @pytest.mark.django_db def test_get_calendar_on_call_shift( on_call_shift_internal_api_setup, make_schedule, make_on_call_shift, make_user_auth_headers, ): token, user1, user2, organization, _ = on_call_shift_internal_api_setup schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) client = APIClient() start_date = timezone.now().replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}, {user2.pk: user2.public_primary_key}], ) on_call_shift.schedules.add(schedule) url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.get(url, format="json", **make_user_auth_headers(user1, token)) expected_payload = { "id": response.data["id"], "name": name, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload @pytest.mark.django_db def test_list_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = timezone.now().replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}, {user2.pk: user2.public_primary_key}], ) url = reverse("api-internal:oncall_shifts-list") response = client.get(url, format="json", **make_user_auth_headers(user1, token)) expected_payload = { "count": 1, "next": None, "previous": None, "results": [ { "id": on_call_shift.public_primary_key, "name": name, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, "start_rotation_from_user_index": None, } ], "current_page_number": 1, "page_size": 50, "total_pages": 1, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload @pytest.mark.django_db def test_list_on_call_shift_filter_schedule_id( on_call_shift_internal_api_setup, make_schedule, make_on_call_shift, make_user_auth_headers, ): token, user1, user2, organization, schedule = on_call_shift_internal_api_setup schedule_without_shifts = make_schedule(organization, schedule_class=OnCallScheduleWeb) client = APIClient() start_date = timezone.now().replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}, {user2.pk: user2.public_primary_key}], ) url = reverse("api-internal:oncall_shifts-list") response = client.get( url + f"?schedule_id={schedule.public_primary_key}", format="json", **make_user_auth_headers(user1, token) ) expected_payload = { "count": 1, "next": None, "previous": None, "results": [ { "id": on_call_shift.public_primary_key, "name": name, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, "start_rotation_from_user_index": None, } ], "current_page_number": 1, "page_size": 50, "total_pages": 1, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload expected_payload = { "count": 0, "next": None, "previous": None, "results": [], "current_page_number": 1, "page_size": 50, "total_pages": 1, } response = client.get( url + f"?schedule_id={schedule_without_shifts.public_primary_key}", format="json", **make_user_auth_headers(user1, token), ) assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload @pytest.mark.django_db def test_update_calendar_shift_is_disabled( on_call_shift_internal_api_setup, make_schedule, make_on_call_shift, make_user_auth_headers, ): token, user1, user2, organization, _ = on_call_shift_internal_api_setup schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) client = APIClient() start_date = timezone.now().replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}, {user2.pk: user2.public_primary_key}], ) on_call_shift.schedules.add(schedule) client = APIClient() data_to_update = { "name": name, } url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db def test_update_future_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test updating the shift that has not started (rotation_start > now)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) data_to_update = { "name": name, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], } assert on_call_shift.priority_level != data_to_update["priority_level"] url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) expected_payload = { "id": on_call_shift.public_primary_key, "name": name, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user1.public_primary_key]], "updated_shift": None, "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload on_call_shift.refresh_from_db() assert on_call_shift.priority_level == data_to_update["priority_level"] @pytest.mark.django_db def test_update_future_on_call_shift_removing_users( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) data_to_update = { "name": name, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [], } url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["rolling_users"][0] == "User(s) are required" @pytest.mark.django_db def test_update_on_call_shift_invalid_rolling_users( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) data_to_update = { "name": name, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [["fuzz"]], } url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == {"rolling_users": {"0": ["User does not exist {'fuzz'}"]}} @pytest.mark.django_db def test_update_started_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test updating the shift that has started (rotation_start < now)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=3), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) data_to_update = { "name": name, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, "by_day": None, "rolling_users": [[user1.public_primary_key]], } assert on_call_shift.priority_level != data_to_update["priority_level"] url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) with patch("apps.schedules.models.CustomOnCallShift.refresh_schedule") as mock_refresh_schedule: response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) expected_payload = { "id": response.data["id"], "name": name, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": response.data["rotation_start"], "until": None, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user1.public_primary_key]], "updated_shift": None, "start_rotation_from_user_index": 0, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload # check that another shift was created assert response.data["id"] != on_call_shift.public_primary_key on_call_shift.refresh_from_db() assert on_call_shift.priority_level != data_to_update["priority_level"] assert on_call_shift.updated_shift.public_primary_key == response.data["id"] # check if until date was changed assert on_call_shift.until is not None assert on_call_shift.until == on_call_shift.updated_shift.rotation_start assert mock_refresh_schedule.called @pytest.mark.django_db def test_update_started_on_call_shift_force_update( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=3), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) data_to_update = { "name": name, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key]], } assert on_call_shift.priority_level != data_to_update["priority_level"] url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) + "?force=true" response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_200_OK # check no shift was created assert response.data["id"] == on_call_shift.public_primary_key on_call_shift.refresh_from_db() assert on_call_shift.priority_level == data_to_update["priority_level"] assert on_call_shift.updated_shift is None assert on_call_shift.until is None @pytest.mark.django_db def test_update_old_on_call_shift_with_future_version( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test updating the shift that has the newer version (updated_shift is not None)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() now = timezone.now().replace(microsecond=0) start_date = now - timezone.timedelta(days=3) next_rotation_start_date = now + timezone.timedelta(days=1) updated_duration = timezone.timedelta(hours=4) name = "Test Shift Rotation" new_on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=next_rotation_start_date, duration=timezone.timedelta(hours=3), rotation_start=next_rotation_start_date, rolling_users=[{user1.pk: user1.public_primary_key}], frequency=CustomOnCallShift.FREQUENCY_DAILY, ) old_on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=3), rotation_start=start_date, until=next_rotation_start_date, rolling_users=[{user1.pk: user1.public_primary_key}], updated_shift=new_on_call_shift, frequency=CustomOnCallShift.FREQUENCY_DAILY, ) # update shift_end and priority_level data_to_update = { "name": name, "priority_level": 2, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + updated_duration).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, "by_day": None, "rolling_users": [[user1.public_primary_key]], } assert old_on_call_shift.duration != updated_duration assert old_on_call_shift.priority_level != data_to_update["priority_level"] assert new_on_call_shift.duration != updated_duration assert new_on_call_shift.priority_level != data_to_update["priority_level"] url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": old_on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) response_data = response.json() for key in ["shift_start", "shift_end", "rotation_start"]: data_to_update.pop(key) response_data.pop(key) expected_payload = data_to_update | { "id": new_on_call_shift.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "updated_shift": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "start_rotation_from_user_index": 0, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload assert old_on_call_shift.duration != updated_duration assert old_on_call_shift.priority_level != data_to_update["priority_level"] new_on_call_shift.refresh_from_db() # check if the newest version of shift was changed assert new_on_call_shift.start - now < timezone.timedelta(minutes=1) assert new_on_call_shift.rotation_start - now < timezone.timedelta(minutes=1) assert new_on_call_shift.duration == updated_duration assert new_on_call_shift.priority_level == data_to_update["priority_level"] @pytest.mark.django_db def test_update_started_on_call_shift_name( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test updating the name for the shift that has started (rotation_start < now)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) name = "Test Shift Rotation" new_name = "Test Shift Rotation RENAMED" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], source=CustomOnCallShift.SOURCE_WEB, week_start=CustomOnCallShift.MONDAY, ) # update only name data_to_update = { "name": new_name, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "week_start": "MO", "rolling_users": [[user1.public_primary_key]], } assert on_call_shift.name != new_name url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) expected_payload = data_to_update | { "id": on_call_shift.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "updated_shift": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_payload on_call_shift.refresh_from_db() assert on_call_shift.name == new_name @pytest.mark.django_db def test_delete_started_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test deleting the shift that has started (rotation_start < now)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) assert on_call_shift.until is None response = client.delete(url, **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_204_NO_CONTENT on_call_shift.refresh_from_db() assert on_call_shift.until is not None @pytest.mark.django_db def test_force_delete_started_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test deleting the shift that has started (rotation_start < now)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) # set force=true to hard delete the shift url = "{}?force=true".format( reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) ) response = client.delete(url, **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_204_NO_CONTENT with pytest.raises(CustomOnCallShift.DoesNotExist): on_call_shift.refresh_from_db() @pytest.mark.django_db def test_delete_future_on_call_shift( on_call_shift_internal_api_setup, make_on_call_shift, make_user_auth_headers, ): """Test deleting the shift that has not started (rotation_start > now)""" token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0) name = "Test Shift Rotation" on_call_shift = make_on_call_shift( schedule.organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, name=name, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, rolling_users=[{user1.pk: user1.public_primary_key}], ) url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) response = client.delete(url, **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_204_NO_CONTENT with pytest.raises(CustomOnCallShift.DoesNotExist): on_call_shift.refresh_from_db() @pytest.mark.django_db def test_create_on_call_shift_invalid_data_rotation_start( on_call_shift_internal_api_setup, make_user_auth_headers, ): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) # rotation_start < shift_start data = { "name": "Test Shift 1", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": (start_date - timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["rotation_start"][0] == "Incorrect rotation start date" @pytest.mark.django_db def test_create_on_call_shift_invalid_data_until(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) # until < rotation_start data = { "name": "Test Shift", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 1, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": (start_date - timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "frequency": 1, "interval": 1, "by_day": [ CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.FRIDAY], ], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["until"][0] == "Incorrect rotation end date" # until with non-recurrent shift data = { "name": "Test Shift 2", "type": CustomOnCallShift.TYPE_OVERRIDE, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "frequency": None, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["until"][0] == "Cannot set 'until' for non-recurrent shifts" @pytest.mark.django_db def test_create_on_call_shift_invalid_data_by_day(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) # by_day with non-recurrent shift data = { "name": "Test Shift 1", "type": CustomOnCallShift.TYPE_OVERRIDE, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": [CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY]], "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["by_day"][0] == "Cannot set days value for non-recurrent shifts" @pytest.mark.django_db def test_create_on_call_shift_invalid_data_interval(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) # interval with non-recurrent shift data = { "name": "Test Shift 2", "type": CustomOnCallShift.TYPE_OVERRIDE, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": 2, "by_day": None, "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["interval"][0] == "Cannot set interval for non-recurrent shifts" invalid_intervals = ( (None, "If frequency is set, interval must be a positive integer"), (2, "Interval must be less than or equal to the number of selected days"), ) for interval, expected_error in invalid_intervals: # by_day, daily shift data = { "name": "Test Shift 2", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "by_day": [CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY]], "rolling_users": [[user1.public_primary_key]], } if interval: data["interval"] = interval response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["interval"][0] == expected_error @pytest.mark.django_db def test_create_on_call_shift_invalid_data_shift_end(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) # shift_end is None data = { "name": "Test Shift 1", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": None, "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": 1, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["shift_end"][0] == "This field is required." # shift_end < shift_start data = { "name": "Test Shift 2", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date - timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["shift_end"][0] == "Incorrect shift end date" @pytest.mark.django_db def test_create_on_call_shift_invalid_data_rolling_users( on_call_shift_internal_api_setup, make_user_auth_headers, ): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) data = { "name": "Test Shift 1", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": None, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["rolling_users"][0] == "Cannot set multiple user groups for non-recurrent shifts" @pytest.mark.django_db def test_create_on_call_shift_invalid_rolling_users(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) data = { "name": "Test Shift", "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "schedule": schedule.public_primary_key, "priority_level": 1, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": 1, "interval": 1, "by_day": [ CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.FRIDAY], ], "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key, "fuzz"]], } with patch("apps.schedules.models.CustomOnCallShift.refresh_schedule") as mock_refresh_schedule: response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) expected_payload = {"rolling_users": {"1": ["User does not exist {'fuzz'}"]}} assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json() == expected_payload mock_refresh_schedule.assert_not_called() @pytest.mark.django_db def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) # override shift with frequency data = { "name": "Test Shift Override", "type": CustomOnCallShift.TYPE_OVERRIDE, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "until": None, "frequency": 1, "interval": None, "by_day": None, "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["frequency"][0] == "Cannot set 'frequency' for shifts with type 'override'" @pytest.mark.django_db def test_create_on_call_shift_override_in_past(on_call_shift_internal_api_setup, make_user_auth_headers): token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) - timezone.timedelta(hours=2) data = { "name": "Test Shift Override", "type": CustomOnCallShift.TYPE_OVERRIDE, "schedule": schedule.public_primary_key, "priority_level": 0, "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "rolling_users": [[user1.public_primary_key]], } response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["shift_end"][0] == "Cannot create or update an override in the past" @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED), (LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_create_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:oncall_shifts-list") with patch( "apps.api.views.on_call_shifts.OnCallShiftView.create", return_value=Response( status=status.HTTP_201_CREATED, ), ): 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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_update_permissions( make_organization_and_user_with_plugin_token, make_schedule, make_on_call_shift, make_user_auth_headers, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) start_date = timezone.now() on_call_shift = make_on_call_shift( organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, ) url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) with patch( "apps.api.views.on_call_shifts.OnCallShiftView.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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_list_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:oncall_shifts-list") with patch( "apps.api.views.on_call_shifts.OnCallShiftView.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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_retrieve_permissions( make_organization_and_user_with_plugin_token, make_schedule, make_on_call_shift, make_user_auth_headers, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) start_date = timezone.now() on_call_shift = make_on_call_shift( organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, ) client = APIClient() url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) with patch( "apps.api.views.on_call_shifts.OnCallShiftView.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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_delete_permissions( make_organization_and_user_with_plugin_token, make_schedule, make_on_call_shift, make_user_auth_headers, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) start_date = timezone.now() on_call_shift = make_on_call_shift( organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, schedule=schedule, start=start_date, duration=timezone.timedelta(hours=1), rotation_start=start_date, ) client = APIClient() url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) with patch( "apps.api.views.on_call_shifts.OnCallShiftView.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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_frequency_options_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:oncall_shifts-frequency-options") with patch( "apps.api.views.on_call_shifts.OnCallShiftView.frequency_options", 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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_days_options_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:oncall_shifts-days-options") with patch( "apps.api.views.on_call_shifts.OnCallShiftView.days_options", 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), (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_preview_permissions( make_organization_and_user_with_plugin_token, make_schedule, make_user_auth_headers, role, expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) start_date = timezone.now() client = APIClient() shift_start = (start_date + timezone.timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (start_date + timezone.timedelta(hours=13)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, "rolling_users": [[user.public_primary_key]], "priority_level": 2, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } url = reverse("api-internal:oncall_shifts-preview") response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status @pytest.mark.django_db def test_on_call_shift_preview_missing_data( make_organization_and_user_with_plugin_token, make_schedule, make_user_auth_headers, ): organization, user, token = make_organization_and_user_with_plugin_token() schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) client = APIClient() shift_data = { "schedule": schedule.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rolling_users": [[user.public_primary_key]], "priority_level": 2, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } url = reverse("api-internal:oncall_shifts-preview") response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db def test_on_call_shift_preview( 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 = make_user_for_organization(organization) other_user = make_user_for_organization(organization) data = { "start": start_date + timezone.timedelta(hours=9), "rotation_start": start_date + timezone.timedelta(hours=9), "duration": timezone.timedelta(hours=9), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, "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]]) url = "{}?date={}&days={}".format( reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1 ) shift_start = (start_date + timezone.timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (start_date + timezone.timedelta(hours=13)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, "rolling_users": [[other_user.public_primary_key]], "priority_level": 2, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK # check rotation events rotation_events = response.json()["rotation"] expected_rotation_events = [ { "calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY, "start": shift_start, "end": shift_end, "all_day": False, "is_override": False, "is_empty": False, "is_gap": False, "priority_level": 2, "missing_users": [], "users": [ { "display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email, "avatar_full": other_user.avatar_full_url(organization), }, ], "source": "web", } ] # there isn't a saved shift, we don't care/know the temp pk _ = [r.pop("shift") for r in rotation_events] assert rotation_events == expected_rotation_events # check final schedule events final_events = response.json()["final"] expected = ( # start (h), duration (H), user, priority (9, 3, user.username, 1), # 9-12 user (12, 1, other_user.username, 2), # 12-13 other_user (13, 5, user.username, 1), # 13-18 C ) expected_events = [ { "end": (start_date + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"), "priority_level": priority, "start": (start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime( "%Y-%m-%dT%H:%M:%SZ" ), "user": user, } for start, duration, user, priority in expected ] returned_events = [ { "end": e["end"], "priority_level": e["priority_level"], "start": e["start"], "user": e["users"][0]["display_name"] if e["users"] else None, } for e in final_events if not e["is_override"] and not e["is_gap"] ] assert returned_events == expected_events @pytest.mark.django_db def test_on_call_shift_preview_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=OnCallScheduleCalendar) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) request_date = start_date url = "{}?date={}&days={}".format( reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1 ) shift_start = (start_date + timezone.timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (start_date + timezone.timedelta(hours=13)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, "rolling_users": [[user.public_primary_key]], "priority_level": 2, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data["type"][0] == "Invalid event type" @pytest.mark.django_db def test_on_call_shift_preview_without_users( make_organization_and_user_with_plugin_token, make_user_for_organization, 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", ) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) request_date = start_date user = make_user_for_organization(organization) url = "{}?date={}&days={}".format( reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1 ) shift_start = (start_date + timezone.timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (start_date + timezone.timedelta(hours=13)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, # passing empty users "rolling_users": [], "priority_level": 2, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK # check rotation events rotation_events = response.json()["rotation"] expected_rotation_events = [ { "calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY, "start": shift_start, "end": shift_end, "all_day": False, "is_override": False, "is_empty": True, "is_gap": False, "priority_level": 0, "missing_users": [], "users": [], "source": "web", } ] # there isn't a saved shift, we don't care/know the temp pk _ = [r.pop("shift") for r in rotation_events] assert rotation_events == expected_rotation_events # check final schedule events final_events = response.json()["final"] expected_events = [] returned_events = [ { "end": e["end"], "start": e["start"], "user": e["users"][0]["display_name"] if e["users"] else None, "is_empty": e["is_empty"], } for e in final_events if not e["is_override"] and not e["is_gap"] ] assert returned_events == expected_events @pytest.mark.django_db def test_on_call_shift_preview_merge_events( make_organization_and_user_with_plugin_token, make_user_for_organization, 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", ) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) request_date = start_date user = make_user_for_organization(organization) other_user = make_user_for_organization(organization) url = "{}?date={}&days={}".format( reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1 ) shift_start = (start_date + timezone.timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (start_date + timezone.timedelta(hours=13)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, "rolling_users": [[user.public_primary_key, other_user.public_primary_key]], "priority_level": 2, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK # check rotation events rotation_events = response.json()["rotation"] expected_rotation_events = [ { "calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY, "start": shift_start, "end": shift_end, "all_day": False, "is_override": False, "is_empty": False, "is_gap": False, "priority_level": 2, "missing_users": [], "source": "web", } ] expected_users = sorted([user.username, other_user.username]) returned_event = rotation_events[0] # there isn't a saved shift, we don't care/know the temp pk returned_event.pop("shift") returned_users = sorted(u["display_name"] for u in returned_event.pop("users")) assert sorted(returned_users) == expected_users assert rotation_events == expected_rotation_events # check final schedule events final_events = response.json()["final"] expected = ( # start (h), duration (H), users, priority (12, 1, expected_users, 2), # 12-13 other_user ) expected_events = [ { "end": (start_date + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"), "priority_level": priority, "start": (start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime( "%Y-%m-%dT%H:%M:%SZ" ), "users": users, } for start, duration, users, priority in expected ] returned_events = [ { "end": e["end"], "priority_level": e["priority_level"], "start": e["start"], "users": sorted(u["display_name"] for u in e["users"]), } for e in final_events if not e["is_override"] and not e["is_gap"] ] assert returned_events == expected_events @pytest.mark.django_db def test_on_call_shift_preview_update( 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) tomorrow = now + timezone.timedelta(days=1) user = make_user_for_organization(organization) other_user = make_user_for_organization(organization) data = { "start": start_date + timezone.timedelta(hours=8), "rotation_start": start_date + timezone.timedelta(hours=8), "duration": timezone.timedelta(hours=1), "priority_level": 1, "interval": 4, "frequency": CustomOnCallShift.FREQUENCY_HOURLY, "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]]) url = "{}?date={}&days={}".format(reverse("api-internal:oncall_shifts-preview"), tomorrow.strftime("%Y-%m-%d"), 1) shift_start = (tomorrow + timezone.timedelta(hours=10)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (tomorrow + timezone.timedelta(hours=18)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "shift_pk": on_call_shift.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, "rolling_users": [[other_user.public_primary_key]], "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK # check rotation events rotation_events = response.json()["rotation"] assert len(rotation_events) == 1 # previewing an update reuse shift PK if rotation already started new_shift_pk = rotation_events[-1]["shift"]["pk"] assert new_shift_pk == on_call_shift.public_primary_key expected_shift_preview = { "calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY, "shift": {"pk": new_shift_pk}, "start": shift_start, "end": shift_end, "all_day": False, "is_override": False, "is_empty": False, "is_gap": False, "priority_level": 1, "missing_users": [], "users": [ { "display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email, "avatar_full": other_user.avatar_full_url(organization), }, ], "source": "web", } assert rotation_events[-1] == expected_shift_preview # check final schedule events final_events = response.json()["final"] expected = ( # start (h), duration (H), user, priority (10, 8, other_user.username, 1), # 10-18 other_user ) expected_events = [ { "end": (tomorrow + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"), "priority_level": priority, "start": (tomorrow + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime( "%Y-%m-%dT%H:%M:%SZ" ), "user": user, } for start, duration, user, priority in expected ] returned_events = [ { "end": e["end"], "priority_level": e["priority_level"], "start": e["start"], "user": e["users"][0]["display_name"] if e["users"] else None, } for e in final_events if not e["is_override"] and not e["is_gap"] ] assert returned_events == expected_events @pytest.mark.django_db def test_on_call_shift_preview_update_not_started_reuse_pk( 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 = make_user_for_organization(organization) other_user = make_user_for_organization(organization) data = { "start": start_date + timezone.timedelta(hours=8), "rotation_start": start_date + timezone.timedelta(hours=8), "duration": timezone.timedelta(hours=1), "priority_level": 1, "interval": 4, "frequency": CustomOnCallShift.FREQUENCY_HOURLY, "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]]) url = "{}?date={}&days={}".format( reverse("api-internal:oncall_shifts-preview"), request_date.strftime("%Y-%m-%d"), 1 ) shift_start = (start_date + timezone.timedelta(hours=6)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_end = (start_date + timezone.timedelta(hours=18)).strftime("%Y-%m-%dT%H:%M:%SZ") shift_data = { "schedule": schedule.public_primary_key, "shift_pk": on_call_shift.public_primary_key, "type": CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, "rotation_start": shift_start, "shift_start": shift_start, "shift_end": shift_end, "rolling_users": [[other_user.public_primary_key]], "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "interval": 1, } response = client.post(url, shift_data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK # check rotation events rotation_events = response.json()["rotation"] # previewing an update reuses shift PK when rotation is not started expected_rotation_events = [ { "calendar_type": OnCallSchedule.TYPE_ICAL_PRIMARY, "shift": {"pk": on_call_shift.public_primary_key}, "start": shift_start, "end": shift_end, "all_day": False, "is_override": False, "is_empty": False, "is_gap": False, "priority_level": 1, "missing_users": [], "users": [ { "display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email, "avatar_full": other_user.avatar_full_url(organization), }, ], "source": "web", }, ] assert rotation_events == expected_rotation_events # check final schedule events final_events = response.json()["final"] expected = ( # start (h), duration (H), user, priority (6, 12, other_user.username, 1), # 6-18 other_user ) expected_events = [ { "end": (start_date + timezone.timedelta(hours=start + duration)).strftime("%Y-%m-%dT%H:%M:%SZ"), "priority_level": priority, "start": (start_date + timezone.timedelta(hours=start, milliseconds=1 if start == 0 else 0)).strftime( "%Y-%m-%dT%H:%M:%SZ" ), "user": user, } for start, duration, user, priority in expected ] returned_events = [ { "end": e["end"], "priority_level": e["priority_level"], "start": e["start"], "user": e["users"][0]["display_name"] if e["users"] else None, } for e in final_events if not e["is_override"] and not e["is_gap"] ] assert returned_events == expected_events