Skip to content

Commit 29d532e

Browse files
authored
Improve APIs for creating/updating direct paging integrations (#2603)
# What this PR does - Disallow creating direct paging integrations for a team that already has one + make sure this constraint is also enforced on update - Refactor some direct paging API parts to be more reusable & simple - Add tests ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
1 parent 3ba321c commit 29d532e

File tree

7 files changed

+198
-48
lines changed

7 files changed

+198
-48
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
### Changed
1717

1818
- Update direct paging docs by @vadimkerr ([#2600](https://github.com/grafana/oncall/pull/2600))
19+
- Improve APIs for creating/updating direct paging integrations by @vadimkerr ([#2603](https://github.com/grafana/oncall/pull/2603))
1920

2021
### Fixed
2122

engine/apps/alerts/models/alert_receive_channel.py

+19
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,25 @@ def delete(self):
251251
def hard_delete(self):
252252
super(AlertReceiveChannel, self).delete()
253253

254+
class DuplicateDirectPagingError(Exception):
255+
"""Only one Direct Paging integration is allowed per team."""
256+
257+
DETAIL = "Direct paging integration already exists for this team" # Returned in BadRequest responses
258+
259+
def save(self, *args, **kwargs):
260+
# Don't allow multiple Direct Paging integrations per team
261+
if (
262+
self.integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
263+
and AlertReceiveChannel.objects.filter(
264+
organization=self.organization, team=self.team, integration=self.integration
265+
)
266+
.exclude(pk=self.pk)
267+
.exists()
268+
):
269+
raise self.DuplicateDirectPagingError
270+
271+
return super().save(*args, **kwargs)
272+
254273
def change_team(self, team_id, user):
255274
if team_id == self.team_id:
256275
raise TeamCanNotBeChangedError("Integration is already in this team")

engine/apps/api/serializers/alert_receive_channel.py

+21-6
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,24 @@ def create(self, validated_data):
121121
if _integration.slug == integration:
122122
is_able_to_autoresolve = _integration.is_able_to_autoresolve
123123

124-
instance = AlertReceiveChannel.create(
125-
**validated_data,
126-
organization=organization,
127-
author=self.context["request"].user,
128-
allow_source_based_resolving=is_able_to_autoresolve,
129-
)
124+
try:
125+
instance = AlertReceiveChannel.create(
126+
**validated_data,
127+
organization=organization,
128+
author=self.context["request"].user,
129+
allow_source_based_resolving=is_able_to_autoresolve,
130+
)
131+
except AlertReceiveChannel.DuplicateDirectPagingError:
132+
raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL)
130133

131134
return instance
132135

136+
def update(self, *args, **kwargs):
137+
try:
138+
return super().update(*args, **kwargs)
139+
except AlertReceiveChannel.DuplicateDirectPagingError:
140+
raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL)
141+
133142
def get_instructions(self, obj):
134143
if obj.integration in [AlertReceiveChannel.INTEGRATION_MAINTENANCE]:
135144
return ""
@@ -146,6 +155,12 @@ def get_default_channel_filter(self, obj):
146155
if filter.is_default:
147156
return filter.public_primary_key
148157

158+
@staticmethod
159+
def validate_integration(integration):
160+
if integration is None or integration not in AlertReceiveChannel.WEB_INTEGRATION_CHOICES:
161+
raise BadRequest(detail="invalid integration")
162+
return integration
163+
149164
def validate_verbal_name(self, verbal_name):
150165
organization = self.context["request"].auth.organization
151166
if verbal_name is None or (self.instance and verbal_name == self.instance.verbal_name):

engine/apps/api/tests/test_alert_receive_channel.py

+89
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,27 @@ def test_create_invalid_alert_receive_channel(alert_receive_channel_internal_api
102102
assert response.status_code == status.HTTP_400_BAD_REQUEST
103103

104104

105+
@pytest.mark.django_db
106+
def test_create_invalid_alert_receive_channel_type(alert_receive_channel_internal_api_setup, make_user_auth_headers):
107+
user, token, _ = alert_receive_channel_internal_api_setup
108+
109+
client = APIClient()
110+
url = reverse("api-internal:alert_receive_channel-list")
111+
112+
response_1 = client.post(
113+
url,
114+
data={"integration": "random123", "verbal_name": "Random 123"},
115+
format="json",
116+
**make_user_auth_headers(user, token),
117+
)
118+
response_2 = client.post(
119+
url, data={"verbal_name": "Random 123"}, format="json", **make_user_auth_headers(user, token)
120+
)
121+
122+
assert response_1.status_code == status.HTTP_400_BAD_REQUEST
123+
assert response_2.status_code == status.HTTP_400_BAD_REQUEST
124+
125+
105126
@pytest.mark.django_db
106127
def test_update_alert_receive_channel(alert_receive_channel_internal_api_setup, make_user_auth_headers):
107128
user, token, alert_receive_channel = alert_receive_channel_internal_api_setup
@@ -693,6 +714,74 @@ def test_get_alert_receive_channels_direct_paging_present_for_filters(
693714
assert response.json()["results"][0]["value"] == alert_receive_channel.public_primary_key
694715

695716

717+
@pytest.mark.django_db
718+
def test_create_alert_receive_channels_direct_paging(
719+
make_organization_and_user_with_plugin_token, make_team, make_alert_receive_channel, make_user_auth_headers
720+
):
721+
organization, user, token = make_organization_and_user_with_plugin_token()
722+
team = make_team(organization)
723+
724+
client = APIClient()
725+
url = reverse("api-internal:alert_receive_channel-list")
726+
727+
response_1 = client.post(
728+
url, data={"integration": "direct_paging"}, format="json", **make_user_auth_headers(user, token)
729+
)
730+
response_2 = client.post(
731+
url, data={"integration": "direct_paging"}, format="json", **make_user_auth_headers(user, token)
732+
)
733+
734+
response_3 = client.post(
735+
url,
736+
data={"integration": "direct_paging", "team": team.public_primary_key},
737+
format="json",
738+
**make_user_auth_headers(user, token),
739+
)
740+
response_4 = client.post(
741+
url,
742+
data={"integration": "direct_paging", "team": team.public_primary_key},
743+
format="json",
744+
**make_user_auth_headers(user, token),
745+
)
746+
747+
# Check direct paging integration for "No team" is created
748+
assert response_1.status_code == status.HTTP_201_CREATED
749+
# Check direct paging integration is not created, as it already exists for "No team"
750+
assert response_2.status_code == status.HTTP_400_BAD_REQUEST
751+
752+
# Check direct paging integration for team is created
753+
assert response_3.status_code == status.HTTP_201_CREATED
754+
# Check direct paging integration is not created, as it already exists for team
755+
assert response_4.status_code == status.HTTP_400_BAD_REQUEST
756+
assert response_4.json()["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL
757+
758+
759+
@pytest.mark.django_db
760+
def test_update_alert_receive_channels_direct_paging(
761+
make_organization_and_user_with_plugin_token, make_team, make_alert_receive_channel, make_user_auth_headers
762+
):
763+
organization, user, token = make_organization_and_user_with_plugin_token()
764+
team = make_team(organization)
765+
integration = make_alert_receive_channel(
766+
organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=None
767+
)
768+
make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=team)
769+
770+
client = APIClient()
771+
url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": integration.public_primary_key})
772+
773+
# Move direct paging integration from "No team" to team
774+
response = client.put(
775+
url,
776+
data={"integration": "direct_paging", "team": team.public_primary_key},
777+
format="json",
778+
**make_user_auth_headers(user, token),
779+
)
780+
781+
assert response.status_code == status.HTTP_400_BAD_REQUEST
782+
assert response.json()["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL
783+
784+
696785
@pytest.mark.django_db
697786
def test_start_maintenance_integration(
698787
make_user_auth_headers,

engine/apps/api/views/alert_receive_channel.py

-37
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
)
1919
from apps.api.throttlers import DemoAlertThrottler
2020
from apps.auth_token.auth import PluginAuthentication
21-
from apps.user_management.models.team import Team
2221
from common.api_helpers.exceptions import BadRequest
2322
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, TeamModelMultipleChoiceFilter
2423
from common.api_helpers.mixins import (
@@ -104,42 +103,6 @@ class AlertReceiveChannelView(
104103
"stop_maintenance": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
105104
}
106105

107-
def create(self, request, *args, **kwargs):
108-
organization = request.auth.organization
109-
user = request.user
110-
team_lookup = {}
111-
if "team" in request.data:
112-
team_public_pk = request.data.get("team", None)
113-
if team_public_pk is not None:
114-
try:
115-
team = user.available_teams.get(public_primary_key=team_public_pk)
116-
team_lookup = {"team": team}
117-
except Team.DoesNotExist:
118-
return Response(data="invalid team", status=status.HTTP_400_BAD_REQUEST)
119-
else:
120-
team_lookup = {"team__isnull": True}
121-
122-
if request.data["integration"] is not None:
123-
if request.data["integration"] in AlertReceiveChannel.WEB_INTEGRATION_CHOICES:
124-
# Don't allow multiple Direct Paging integrations
125-
if request.data["integration"] == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING:
126-
try:
127-
AlertReceiveChannel.objects.get(
128-
organization=organization,
129-
integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING,
130-
deleted_at=None,
131-
**team_lookup,
132-
)
133-
return Response(
134-
data="Direct paging integration already exists for this team",
135-
status=status.HTTP_400_BAD_REQUEST,
136-
)
137-
except AlertReceiveChannel.DoesNotExist:
138-
pass
139-
return super().create(request, *args, **kwargs)
140-
141-
return Response(data="invalid integration", status=status.HTTP_400_BAD_REQUEST)
142-
143106
def perform_update(self, serializer):
144107
prev_state = serializer.instance.insight_logs_serialized
145108
serializer.save()

engine/apps/public_api/serializers/integrations.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,14 @@ def create(self, validated_data):
125125
if connection_error:
126126
raise serializers.ValidationError(connection_error)
127127
with transaction.atomic():
128-
instance = AlertReceiveChannel.create(
129-
**validated_data,
130-
author=self.context["request"].user,
131-
organization=organization,
132-
)
128+
try:
129+
instance = AlertReceiveChannel.create(
130+
**validated_data,
131+
author=self.context["request"].user,
132+
organization=organization,
133+
)
134+
except AlertReceiveChannel.DuplicateDirectPagingError:
135+
raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL)
133136
if default_route_data:
134137
serializer = DefaultChannelFilterSerializer(
135138
instance.default_channel_filter, default_route_data, context=self.context
@@ -138,6 +141,12 @@ def create(self, validated_data):
138141
serializer.save()
139142
return instance
140143

144+
def update(self, *args, **kwargs):
145+
try:
146+
return super().update(*args, **kwargs)
147+
except AlertReceiveChannel.DuplicateDirectPagingError:
148+
raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL)
149+
141150
def validate(self, attrs):
142151
organization = self.context["request"].auth.organization
143152
verbal_name = attrs.get("verbal_name", None)

engine/apps/public_api/tests/test_integrations.py

+54
Original file line numberDiff line numberDiff line change
@@ -817,3 +817,57 @@ def test_update_integration_default_route(
817817

818818
assert response.status_code == status.HTTP_200_OK
819819
assert response.data["default_route"]["escalation_chain_id"] == escalation_chain.public_primary_key
820+
821+
822+
@pytest.mark.django_db
823+
def test_create_integrations_direct_paging(
824+
make_organization_and_user_with_token, make_team, make_alert_receive_channel, make_user_auth_headers
825+
):
826+
organization, _, token = make_organization_and_user_with_token()
827+
team = make_team(organization)
828+
829+
client = APIClient()
830+
url = reverse("api-public:integrations-list")
831+
832+
response_1 = client.post(url, data={"type": "direct_paging"}, format="json", HTTP_AUTHORIZATION=token)
833+
response_2 = client.post(url, data={"type": "direct_paging"}, format="json", HTTP_AUTHORIZATION=token)
834+
835+
response_3 = client.post(
836+
url, data={"type": "direct_paging", "team_id": team.public_primary_key}, format="json", HTTP_AUTHORIZATION=token
837+
)
838+
response_4 = client.post(
839+
url, data={"type": "direct_paging", "team_id": team.public_primary_key}, format="json", HTTP_AUTHORIZATION=token
840+
)
841+
842+
# Check direct paging integration for "No team" is created
843+
assert response_1.status_code == status.HTTP_201_CREATED
844+
# Check direct paging integration is not created, as it already exists for "No team"
845+
assert response_2.status_code == status.HTTP_400_BAD_REQUEST
846+
847+
# Check direct paging integration for team is created
848+
assert response_3.status_code == status.HTTP_201_CREATED
849+
# Check direct paging integration is not created, as it already exists for team
850+
assert response_4.status_code == status.HTTP_400_BAD_REQUEST
851+
assert response_4.data["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL
852+
853+
854+
@pytest.mark.django_db
855+
def test_update_integrations_direct_paging(
856+
make_organization_and_user_with_token, make_team, make_alert_receive_channel, make_user_auth_headers
857+
):
858+
organization, _, token = make_organization_and_user_with_token()
859+
team = make_team(organization)
860+
861+
integration = make_alert_receive_channel(
862+
organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=None
863+
)
864+
make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=team)
865+
866+
client = APIClient()
867+
url = reverse("api-public:integrations-detail", args=[integration.public_primary_key])
868+
869+
# Move direct paging integration from "No team" to team
870+
response = client.put(url, data={"team_id": team.public_primary_key}, format="json", HTTP_AUTHORIZATION=token)
871+
872+
assert response.status_code == status.HTTP_400_BAD_REQUEST
873+
assert response.data["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL

0 commit comments

Comments
 (0)