oncall-engine/engine/apps/schedules/tests/test_ical_utils.py
Matias Bordese 5341a7ea5b
Revert "Refactoring schedule final events for reusability" (#2642)
Reverts grafana/oncall#2625

Found a small issue, reverting for now until the problem is fixed not to
block other changes
2023-07-25 17:37:33 -03:00

397 lines
13 KiB
Python

import datetime
import textwrap
from uuid import uuid4
import icalendar
import pytest
import pytz
from django.utils import timezone
from apps.api.permissions import LegacyAccessControlRole
from apps.schedules.ical_utils import (
get_icalendar_tz_or_utc,
is_icals_equal,
list_of_oncall_shifts_from_ical,
list_users_to_notify_from_ical,
parse_event_uid,
users_in_ical,
)
from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleWeb
def test_get_icalendar_tz_or_utc():
ical_data = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-TIMEZONE:Europe/London
BEGIN:VTIMEZONE
TZID:America/Argentina/Buenos_Aires
X-LIC-LOCATION:America/Argentina/Buenos_Aires
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0300
TZNAME:-03
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
"""
)
ical = icalendar.Calendar.from_ical(ical_data)
tz = get_icalendar_tz_or_utc(ical)
assert tz == pytz.timezone("Europe/London")
def test_get_icalendar_tz_or_utc_fallback():
ical_data = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:America/Argentina/Buenos_Aires
X-LIC-LOCATION:America/Argentina/Buenos_Aires
BEGIN:STANDARD
TZOFFSETFROM:-0300
TZOFFSETTO:-0300
TZNAME:-03
DTSTART:19700101T000000
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
"""
)
ical = icalendar.Calendar.from_ical(ical_data)
tz = get_icalendar_tz_or_utc(ical)
assert tz == pytz.timezone("UTC")
@pytest.mark.django_db
def test_users_in_ical_email_case_insensitive(make_organization_and_user, make_user_for_organization):
organization, user = make_organization_and_user()
user = make_user_for_organization(organization, username="foo", email="TestingUser@test.com")
usernames = ["testinguser@test.com"]
result = users_in_ical(usernames, organization)
assert set(result) == {user}
@pytest.mark.django_db
@pytest.mark.parametrize("include_viewers", [True, False])
def test_users_in_ical_viewers_inclusion(make_organization_and_user, make_user_for_organization, include_viewers):
organization, user = make_organization_and_user()
viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER)
usernames = [user.username, viewer.username]
result = users_in_ical(usernames, organization, include_viewers=include_viewers)
if include_viewers:
assert set(result) == {user, viewer}
else:
assert set(result) == {user}
@pytest.mark.django_db
@pytest.mark.parametrize("include_viewers", [True, False])
def test_list_users_to_notify_from_ical_viewers_inclusion(
make_organization_and_user, make_user_for_organization, make_schedule, make_on_call_shift, include_viewers
):
organization, user = make_organization_and_user()
viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER)
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
date = timezone.now().replace(microsecond=0)
data = {
"priority_level": 1,
"start": date,
"rotation_start": date,
"duration": timezone.timedelta(seconds=10800),
}
on_call_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_SINGLE_EVENT, **data
)
on_call_shift.users.add(user)
on_call_shift.users.add(viewer)
schedule.custom_on_call_shifts.add(on_call_shift)
# get users on-call
date = date + timezone.timedelta(minutes=5)
users_on_call = list_users_to_notify_from_ical(schedule, date, include_viewers=include_viewers)
if include_viewers:
assert len(users_on_call) == 2
assert set(users_on_call) == {user, viewer}
else:
assert len(users_on_call) == 1
assert set(users_on_call) == {user}
@pytest.mark.django_db
def test_list_users_to_notify_from_ical_until_terminated_event(
make_organization_and_user, make_user_for_organization, make_schedule, make_on_call_shift
):
organization, user = make_organization_and_user()
other_user = make_user_for_organization(organization)
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
date = timezone.now().replace(microsecond=0)
data = {
"start": date,
"duration": timezone.timedelta(hours=4),
"rotation_start": date + timezone.timedelta(days=3),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
"by_day": ["SU"],
"interval": 1,
"until": date + timezone.timedelta(hours=8),
"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], [other_user]])
# get users on-call
date = date + timezone.timedelta(minutes=5)
# this should not raise despite the shift configuration (until < rotation start)
users_on_call = list_users_to_notify_from_ical(schedule, date)
assert users_on_call == []
@pytest.mark.django_db
def test_shifts_dict_all_day_middle_event(make_organization, make_schedule, get_ical):
calendar = get_ical("calendar_with_all_day_event.ics")
organization = make_organization()
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
schedule.cached_ical_file_primary = calendar.to_ical()
day_to_check_iso = "2021-01-27T15:27:14.448059+00:00"
parsed_iso_day_to_check = datetime.datetime.fromisoformat(day_to_check_iso).replace(tzinfo=pytz.UTC)
requested_date = (parsed_iso_day_to_check - timezone.timedelta(days=1)).date()
shifts = list_of_oncall_shifts_from_ical(schedule, requested_date, days=3, with_empty_shifts=True)
assert len(shifts) == 5
for s in shifts:
start = s["start"].date() if isinstance(s["start"], datetime.datetime) else s["start"]
end = s["end"].date() if isinstance(s["end"], datetime.datetime) else s["end"]
# event started in the given period, or ended in that period, or is happening during the period
assert (
requested_date <= start <= requested_date + timezone.timedelta(days=3)
or requested_date <= end <= requested_date + timezone.timedelta(days=3)
or start <= requested_date <= end
)
@pytest.mark.django_db
def test_shifts_dict_from_cached_final(
make_organization,
make_user_for_organization,
make_schedule,
make_on_call_shift,
):
organization = make_organization()
u1 = make_user_for_organization(organization)
yesterday = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) - timezone.timedelta(days=1)
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
data = {
"start": yesterday + timezone.timedelta(hours=10),
"rotation_start": yesterday + timezone.timedelta(hours=10),
"duration": timezone.timedelta(hours=2),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
"schedule": schedule,
}
on_call_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
)
on_call_shift.add_rolling_users([[u1]])
override_data = {
"start": yesterday + timezone.timedelta(hours=12),
"rotation_start": yesterday + timezone.timedelta(hours=12),
"duration": timezone.timedelta(hours=1),
"schedule": schedule,
}
override = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_OVERRIDE, **override_data
)
override.add_rolling_users([[u1]])
schedule.refresh_ical_file()
schedule.refresh_ical_final_schedule()
shifts = [
(s["calendar_type"], s["start"], list(s["users"]))
for s in list_of_oncall_shifts_from_ical(schedule, yesterday, days=1, from_cached_final=True)
]
expected_events = [
(OnCallSchedule.PRIMARY, on_call_shift.start, [u1]),
(OnCallSchedule.OVERRIDES, override.start, [u1]),
]
assert shifts == expected_events
def test_parse_event_uid_v1():
uuid = uuid4()
event_uid = f"amixr-{uuid}-U1-E2-S1"
pk, source = parse_event_uid(event_uid)
assert pk is None
assert source == "api"
def test_parse_event_uid_v2():
uuid = uuid4()
pk_value = "OABCDEF12345"
event_uid = f"oncall-{uuid}-PK{pk_value}-U3-E1-S2"
pk, source = parse_event_uid(event_uid)
assert pk == pk_value
assert source == "slack"
def test_parse_event_uid_fallback():
# use ical existing UID for imported events
event_uid = "someid@google.com"
pk, source = parse_event_uid(event_uid)
assert pk == event_uid
assert source is None
def test_is_icals_equal_compare_events():
with_vtimezone = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-TIMEZONE:Europe/Amsterdam
BEGIN:VTIMEZONE
TZID:Europe/Amsterdam
X-LIC-LOCATION:Europe/Amsterdam
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20230515
DTEND;VALUE=DATE:20230522
DTSTAMP:20230503T152557Z
UID:something@google.com
RECURRENCE-ID;VALUE=DATE:20230501
CREATED:20230403T073117Z
LAST-MODIFIED:20230424T123617Z
SEQUENCE:2
STATUS:CONFIRMED
SUMMARY:some@user.com
END:VEVENT
END:VCALENDAR
"""
)
without_vtimezone = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-TIMEZONE:Europe/Amsterdam
BEGIN:VEVENT
DTSTART;VALUE=DATE:20230515
DTEND;VALUE=DATE:20230522
DTSTAMP:20230503T162103Z
UID:something@google.com
RECURRENCE-ID;VALUE=DATE:20230501
CREATED:20230403T073117Z
LAST-MODIFIED:20230424T123617Z
SEQUENCE:2
STATUS:CONFIRMED
SUMMARY:some@user.com
END:VEVENT
END:VCALENDAR
"""
)
assert is_icals_equal(with_vtimezone, without_vtimezone)
def test_is_icals_equal_compare_events_not_equal():
with_vtimezone = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-TIMEZONE:Europe/Amsterdam
BEGIN:VTIMEZONE
TZID:Europe/Amsterdam
X-LIC-LOCATION:Europe/Amsterdam
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20230515
DTEND;VALUE=DATE:20230522
DTSTAMP:20230503T152557Z
UID:something@google.com
RECURRENCE-ID;VALUE=DATE:20230501
CREATED:20230403T073117Z
LAST-MODIFIED:20230424T123617Z
SEQUENCE:2
STATUS:CONFIRMED
SUMMARY:some@user.com
END:VEVENT
END:VCALENDAR
"""
)
without_vtimezone = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-TIMEZONE:Europe/Amsterdam
BEGIN:VEVENT
DTSTART;VALUE=DATE:20230515
DTEND;VALUE=DATE:20230522
DTSTAMP:20230503T162103Z
UID:something@google.com
RECURRENCE-ID;VALUE=DATE:20230501
CREATED:20230403T073117Z
LAST-MODIFIED:20230424T123617Z
SEQUENCE:3
STATUS:CONFIRMED
SUMMARY:some@user.com
END:VEVENT
END:VCALENDAR
"""
)
assert not is_icals_equal(with_vtimezone, without_vtimezone)