oncall-engine/engine/common/tests/test_custom_fields.py
Joey Orlando 4a5c4263e0
feat: convert schedule.channel (char field) to schedule.slack_channel (foreign key) (#5199)
# What this PR does

`OnCallSchedule` equivalent of
https://github.com/grafana/oncall/pull/5191.

**NOTE**: merge after https://github.com/grafana/oncall/pull/5224 (so
that I can use some of the new serializer fields defined in there)

### Migration
```bash
Running migrations:                                                                                                                                                                                                │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Starting migration to populate slack_channel field.                                                                │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Total schedules to process: 1                                                                                      │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Schedule 26 updated with SlackChannel 2 (slack_id: C043LL6RTS7).                                                   │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Bulk updated 1 OnCallSchedules with their Slack channel.                                                           │
│ source=engine:app google_trace_id=none logger=apps.schedules.migrations.0019_auto_20241021_1735 Finished migration. Total schedules processed: 1. Schedules updated: 1. Missing SlackChannels: 0.                  │
│   Applying schedules.0019_auto_20241021_1735... OK
```

### Tested Public API
```txt
POST {{oncall_host}}/api/v1/schedules/
Authorization: {{oncall_api_key}}
Content-Type: application/json

{
    "name": "Demo testy testy2",
    "type": "web",
    "time_zone": "America/Los_Angeles",
    "slack": {
        "channel_id": "C05PPLYN1U1"
    }
}

HTTP/1.1 201 Created
Content-Type: application/json
Vary: Accept, Origin
Allow: GET, POST, HEAD, OPTIONS
X-Frame-Options: DENY
Content-Length: 198
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{
  "id": "SBBN73UTUTVCE",
  "team_id": null,
  "name": "Demo testy testy2",
  "time_zone": "America/Los_Angeles",
  "on_call_now": [],
  "shifts": [],
  "slack": {
    "channel_id": "C05PPLYN1U1",
    "user_group_id": null
  },
  "type": "web"
}
```

### Tested via UI (eg; internal API)

https://www.loom.com/share/e66bf3468b144dd782da5eb6e0bfd0af

## 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] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
2024-11-04 14:27:21 -05:00

319 lines
12 KiB
Python

import datetime
from zoneinfo import ZoneInfo
import pytest
import pytz
from rest_framework import serializers
import common.api_helpers.custom_fields as cf
from common.api_helpers.exceptions import BadRequest
class TestTimeZoneField:
@pytest.mark.parametrize("tz", pytz.all_timezones)
def test_valid_timezones(self, tz):
class MySerializer(serializers.Serializer):
tz = cf.TimeZoneField()
try:
serializer = MySerializer(data={"tz": tz})
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["tz"] == tz
except Exception:
pytest.fail()
def test_invalid_timezone(self):
class MySerializer(serializers.Serializer):
tz = cf.TimeZoneField()
with pytest.raises(serializers.ValidationError, match="Invalid timezone"):
serializer = MySerializer(data={"tz": "potato"})
serializer.is_valid(raise_exception=True)
def test_it_works_with_allow_null(self):
class MySerializer(serializers.Serializer):
tz = cf.TimeZoneField(allow_null=True)
try:
serializer = MySerializer(data={"tz": None})
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["tz"] is None
serializer = MySerializer(data={"tz": "UTC"})
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["tz"] == "UTC"
except Exception:
pytest.fail()
def test_it_works_with_required(self):
class MySerializer(serializers.Serializer):
tz = cf.TimeZoneField(required=True)
with pytest.raises(serializers.ValidationError, match="This field is required"):
serializer = MySerializer(data={})
serializer.is_valid(raise_exception=True)
try:
serializer = MySerializer(data={"tz": "UTC"})
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["tz"] == "UTC"
except Exception:
pytest.fail()
class TestTimeZoneAwareDatetimeField:
@pytest.mark.parametrize(
"test_case,expected_persisted_value",
[
# UTC format
("2023-07-20T12:00:00Z", datetime.datetime(2023, 7, 20, 12, 0, 0, tzinfo=ZoneInfo("UTC"))),
# UTC format w/ microseconds
("2023-07-20T12:00:00.245652Z", datetime.datetime(2023, 7, 20, 12, 0, 0, 245652, tzinfo=ZoneInfo("UTC"))),
# UTC offset w/ colons + no microseconds
("2023-07-20T12:00:00+07:00", datetime.datetime(2023, 7, 20, 5, 0, 0, tzinfo=ZoneInfo("UTC"))),
# UTC offset w/ colons + microseconds
(
"2023-07-20T12:00:00.245652+07:00",
datetime.datetime(2023, 7, 20, 5, 0, 0, 245652, tzinfo=ZoneInfo("UTC")),
),
# UTC offset w/ no colons + no microseconds
("2023-07-20T12:00:00+0700", datetime.datetime(2023, 7, 20, 5, 0, 0, tzinfo=ZoneInfo("UTC"))),
# UTC offset w/ no colons + microseconds
(
"2023-07-20T12:00:00.245652+0700",
datetime.datetime(2023, 7, 20, 5, 0, 0, 245652, tzinfo=ZoneInfo("UTC")),
),
("2023-07-20 12:00:00", None),
("20230720T120000Z", None),
],
)
def test_various_datetimes(self, test_case, expected_persisted_value):
class MySerializer(serializers.Serializer):
dt = cf.TimeZoneAwareDatetimeField()
serializer = MySerializer(data={"dt": test_case})
if expected_persisted_value:
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["dt"] == expected_persisted_value
else:
with pytest.raises(serializers.ValidationError):
serializer.is_valid(raise_exception=True)
class TestSlackChannelsFilteredByOrganizationSlackWorkspaceField:
class MockRequest:
def __init__(self, user) -> None:
self.user = user
class MySerializer(serializers.Serializer):
slack_channel_id = cf.SlackChannelsFilteredByOrganizationSlackWorkspaceField()
@pytest.mark.django_db
def test_org_does_not_have_slack_connected(
self,
make_organization,
make_user_for_organization,
):
organization = make_organization()
user = make_user_for_organization(organization)
serializer = self.MySerializer(
data={"slack_channel_id": "abcd"},
context={"request": self.MockRequest(user)},
)
with pytest.raises(BadRequest) as excinfo:
serializer.is_valid(raise_exception=True)
assert excinfo.value.detail == "Slack isn't connected to this workspace"
assert excinfo.value.status_code == 400
@pytest.mark.django_db
def test_org_channel_doesnt_belong_to_org(
self,
make_organization,
make_user_for_organization,
make_slack_team_identity,
make_slack_channel,
):
slack_channel1_id = "FOO"
slack_channel2_id = "BAR"
slack_team_identity1 = make_slack_team_identity()
make_slack_channel(slack_team_identity1, slack_id=slack_channel1_id)
slack_team_identity2 = make_slack_team_identity()
make_slack_channel(slack_team_identity2, slack_id=slack_channel2_id)
organization = make_organization(slack_team_identity=slack_team_identity1)
user = make_user_for_organization(organization)
serializer = self.MySerializer(
data={"slack_channel_id": slack_channel2_id},
context={"request": self.MockRequest(user)},
)
with pytest.raises(serializers.ValidationError) as excinfo:
serializer.is_valid(raise_exception=True)
assert excinfo.value.detail == {"slack_channel_id": ["Slack channel does not exist"]}
@pytest.mark.django_db
def test_invalid_slack_channel(
self,
make_organization,
make_user_for_organization,
make_slack_team_identity,
make_slack_channel,
):
slack_channel_id = "FOO"
slack_team_identity = make_slack_team_identity()
make_slack_channel(slack_team_identity, slack_id=slack_channel_id)
organization = make_organization(slack_team_identity=slack_team_identity)
user = make_user_for_organization(organization)
serializer = self.MySerializer(
data={"slack_channel_id": 1},
context={"request": self.MockRequest(user)},
)
with pytest.raises(serializers.ValidationError) as excinfo:
serializer.is_valid(raise_exception=True)
assert excinfo.value.detail == {"slack_channel_id": ["Invalid Slack channel"]}
@pytest.mark.django_db
def test_valid(
self,
make_organization,
make_user_for_organization,
make_slack_team_identity,
make_slack_channel,
):
slack_channel_id = "FOO"
slack_team_identity = make_slack_team_identity()
slack_channel = make_slack_channel(slack_team_identity, slack_id=slack_channel_id)
organization = make_organization(slack_team_identity=slack_team_identity)
user = make_user_for_organization(organization)
context = {"request": self.MockRequest(user)}
serializer = self.MySerializer(data={"slack_channel_id": slack_channel_id}, context=context)
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["slack_channel_id"] == slack_channel
# case insensitive
serializer = self.MySerializer(data={"slack_channel_id": slack_channel_id.lower()}, context=context)
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["slack_channel_id"] == slack_channel
class TestSlackUserGroupsFilteredByOrganizationSlackWorkspaceField:
class MockRequest:
def __init__(self, user) -> None:
self.user = user
class MySerializer(serializers.Serializer):
slack_user_group_id = cf.SlackUserGroupsFilteredByOrganizationSlackWorkspaceField()
@pytest.mark.django_db
def test_org_does_not_have_slack_connected(
self,
make_organization,
make_user_for_organization,
):
organization = make_organization()
user = make_user_for_organization(organization)
serializer = self.MySerializer(
data={"slack_user_group_id": "abcd"},
context={"request": self.MockRequest(user)},
)
with pytest.raises(BadRequest) as excinfo:
serializer.is_valid(raise_exception=True)
assert excinfo.value.detail == "Slack isn't connected to this workspace"
assert excinfo.value.status_code == 400
@pytest.mark.django_db
def test_org_user_group_doesnt_belong_to_org(
self,
make_organization,
make_user_for_organization,
make_slack_team_identity,
make_slack_user_group,
):
slack_user_group1_id = "FOO"
slack_user_group2_id = "BAR"
slack_team_identity1 = make_slack_team_identity()
make_slack_user_group(slack_team_identity1, slack_id=slack_user_group1_id)
slack_team_identity2 = make_slack_team_identity()
make_slack_user_group(slack_team_identity2, slack_id=slack_user_group2_id)
organization = make_organization(slack_team_identity=slack_team_identity1)
user = make_user_for_organization(organization)
serializer = self.MySerializer(
data={"slack_user_group_id": slack_user_group2_id},
context={"request": self.MockRequest(user)},
)
with pytest.raises(serializers.ValidationError) as excinfo:
serializer.is_valid(raise_exception=True)
assert excinfo.value.detail == {"slack_user_group_id": ["Slack user group does not exist"]}
@pytest.mark.django_db
def test_invalid_slack_user_group(
self,
make_organization,
make_user_for_organization,
make_slack_team_identity,
make_slack_user_group,
):
slack_user_group_id = "FOO"
slack_team_identity = make_slack_team_identity()
make_slack_user_group(slack_team_identity, slack_id=slack_user_group_id)
organization = make_organization(slack_team_identity=slack_team_identity)
user = make_user_for_organization(organization)
serializer = self.MySerializer(
data={"slack_user_group_id": 1},
context={"request": self.MockRequest(user)},
)
with pytest.raises(serializers.ValidationError) as excinfo:
serializer.is_valid(raise_exception=True)
assert excinfo.value.detail == {"slack_user_group_id": ["Invalid Slack user group"]}
@pytest.mark.django_db
def test_valid(
self,
make_organization,
make_user_for_organization,
make_slack_team_identity,
make_slack_user_group,
):
slack_user_group_id = "FOO"
slack_team_identity = make_slack_team_identity()
slack_user_group = make_slack_user_group(slack_team_identity, slack_id=slack_user_group_id)
organization = make_organization(slack_team_identity=slack_team_identity)
user = make_user_for_organization(organization)
context = {"request": self.MockRequest(user)}
serializer = self.MySerializer(data={"slack_user_group_id": slack_user_group_id}, context=context)
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["slack_user_group_id"] == slack_user_group
# case insensitive
serializer = self.MySerializer(data={"slack_user_group_id": slack_user_group_id.lower()}, context=context)
serializer.is_valid(raise_exception=True)
assert serializer.validated_data["slack_user_group_id"] == slack_user_group