diff --git a/CHANGELOG.md b/CHANGELOG.md index a871ecbcce..febea54794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix Slack acknowledgment reminders by @vadimkerr ([#2769](https://github.com/grafana/oncall/pull/2769)) - Fix issue with updating "Require resolution note" setting by @Ferril ([#2782](https://github.com/grafana/oncall/pull/2782)) - Don't send notifications about past SSRs when turning on info notifications by @vadimkerr ([#2783](https://github.com/grafana/oncall/pull/2783)) +- Add schedule shift type validation on create/preview ([#2789](https://github.com/grafana/oncall/pull/2789)) ### Added diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index db411f8c34..2b585f9eb7 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -1,7 +1,7 @@ from django.utils import timezone from rest_framework import serializers -from apps.schedules.models import CustomOnCallShift, OnCallSchedule +from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleWeb from apps.user_management.models import User from common.api_helpers.custom_fields import ( OrganizationFilteredPrimaryKeyRelatedField, @@ -87,6 +87,11 @@ def validate_by_day(self, by_day): raise serializers.ValidationError(["Invalid day value."]) return by_day + def _validate_type(self, schedule, event_type): + if schedule and not isinstance(schedule, OnCallScheduleWeb) and event_type != CustomOnCallShift.TYPE_OVERRIDE: + # if this is not related to a web schedule, only allow override web events + raise serializers.ValidationError({"type": ["Invalid event type"]}) + def validate_week_start(self, week_start): if week_start is None: week_start = CustomOnCallShift.MONDAY @@ -158,6 +163,7 @@ def _correct_validated_data(self, event_type, validated_data): "priority_level", "rotation_start", ] + self._validate_type(validated_data.get("schedule"), event_type) if event_type == CustomOnCallShift.TYPE_OVERRIDE: for field in fields_to_update_for_overrides: value = None diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index e8b435ed07..be796a5615 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -8,7 +8,7 @@ from rest_framework.test import APIClient from apps.api.permissions import LegacyAccessControlRole -from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleWeb +from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleWeb @pytest.fixture() @@ -59,6 +59,46 @@ def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_us 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 @@ -1557,6 +1597,42 @@ def test_on_call_shift_preview( 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,