caldav: add rrule/until convertion to date (#88393)
gitea/passerelle/pipeline/head This commit looks good Details

This commit is contained in:
Yann Weber 2024-03-20 18:03:39 +01:00
parent 1e13344dc0
commit fd7a2d487d
2 changed files with 117 additions and 17 deletions

View File

@ -55,7 +55,7 @@ EVENT_SCHEMA_PART = {
'CATEGORY': {'type': 'string'},
'TRANSP': {
'type': 'boolean',
'description': 'Transparent if true else opaque (RFC 5545 3.8.2.7)',
'description': _('Transparent if true else opaque (RFC 5545 3.8.2.7)'),
},
'RRULE': {
'description': _('Recurrence rule (RFC 5545 3.8.5.3)'),
@ -84,6 +84,10 @@ EVENT_SCHEMA_PART = {
'type': 'integer',
'minimum': 1,
},
'UNTIL': {
'type': 'string',
'description': _('Date or date and time indicating the end of recurrence'),
},
},
},
},
@ -201,6 +205,7 @@ class CalDAV(BaseResource):
vevent = vevent[0]
# vevent.update(post_data) do not convert values as expected
for k, v in post_data.items():
vevent.pop(k)
vevent.add(k, v)
try:
# do not use ical.save(no_create=True) : no_create fails on some calDAV
@ -300,15 +305,33 @@ class CalDAV(BaseResource):
if 'CATEGORY' in data:
data['CATEGORIES'] = [data.pop('CATEGORY')]
for dt_field in ('DTSTART', 'DTEND'):
value = data[dt_field]
if 'RRULE' in data and 'UNTIL' in data['RRULE']:
try:
data[dt_field] = parse_date(value) or parse_datetime(value)
data['RRULE']['UNTIL'] = self._parse_date_or_datetime(data['RRULE']['UNTIL'])
except ValueError:
data[dt_field] = None
if not data[dt_field]:
raise APIError(
_('Unable to convert field %s=%r: not a valid date nor date-time') % (dt_field, value),
_('Unable to convert field %s=%r: not a valid date nor date-time')
% ('RRULE/UNTIL', data['RRULE']['UNTIL']),
http_status=400,
)
for dt_field in ('DTSTART', 'DTEND'):
if dt_field not in data:
continue
try:
data[dt_field] = self._parse_date_or_datetime(data[dt_field])
except ValueError:
raise APIError(
_('Unable to convert field %s=%r: not a valid date nor date-time')
% (dt_field, data[dt_field]),
http_status=400,
)
def _parse_date_or_datetime(self, value):
try:
ret = parse_date(value) or parse_datetime(value)
except ValueError:
ret = None
if not ret:
raise ValueError('Invalid value')
return ret

View File

@ -41,6 +41,42 @@ PROPFIND_REPLY = '''<?xml version="1.0" encoding="utf-8"?>
</D:multistatus>
'''
FAKE_ICS = '''BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//EGroupware//NONSGML EGroupware Calendar 17.1.003//FR
BEGIN:VTIMEZONE
TZID:Europe/Paris
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:20240227
DTEND;VALUE=DATE:20240228
X-MICROSOFT-CDO-ALLDAYEVENT:TRUE
SUMMARY:Yann Weber off
RRULE:FREQ=WEEKLY;BYDAY=TU
TRANSP:TRANSPARENT
CATEGORIES:Journées off
UID:01234567-abcd
STATUS:CONFIRMED
CREATED:20240320T095450Z
LAST-MODIFIED:20240320T095450Z
DTSTAMP:20240320T095513Z
END:VEVENT
END:VCALENDAR
'''
SOME_EVENTS = [
{'DTSTART': '2020-02-20T20:02', 'DTEND': '2020-02-22T22:43', 'SUMMARY': 'Test'},
@ -308,18 +344,24 @@ def test_caldav_event_create_ok_net_err(app, caldav_conn, event):
@pytest.mark.parametrize(
'dtstart,dtend',
'dtstart,dtend,until',
[
('2020-13-13', '2021-01-01'),
('2020-01-01T01:02:03', '2020-01-01T02:64:02'),
('23-01-12', '2023-01-13'),
('13:30', '2023-01-01T14:00'),
('2020-13-13', '2021-01-01', '2021-12-01'),
('2020-01-01T01:02:03', '2020-01-01T02:64:02', '2021-12-01'),
('23-01-12', '2023-01-13', '2021-12-01'),
('13:30', '2023-01-01T14:00', '2021-12-01'),
('2023-10-02', '2023-10-03', '2024-13-12'),
],
)
def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend):
def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend, until):
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
username = 'toto'
event = {'DTSTART': dtstart, 'DTEND': dtend, 'SUMMARY': 'hello'}
event = {
'DTSTART': dtstart,
'DTEND': dtend,
'SUMMARY': 'hello',
'RRULE': {'FREQ': 'WEEKLY', 'BYDAY': 'FR', 'UNTIL': until},
}
qs_params = {'username': username}
resp = app_post(app, url, qs_params, event, status=400)
@ -331,6 +373,9 @@ def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend):
@pytest.mark.parametrize('transp', [True, False])
@responses.activate
def test_caldav_event_update_ok(app, transp, caldav_conn):
"""This test mock de caldav lib in order to check if the
icalendar lib raises error on event modifications
"""
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
username = 'toto'
evt_id = '01234567-abcd'
@ -342,7 +387,7 @@ def test_caldav_event_update_ok(app, transp, caldav_conn):
'DTEND': '2020-03-30',
'SUMMARY': 'foobar',
'TRANSP': transp,
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR']},
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR'], 'UNTIL': '2020-10-01'},
}
qs_params = {'username': username, 'event_id': evt_id}
@ -365,13 +410,45 @@ def test_caldav_event_update_ok(app, transp, caldav_conn):
elif k == 'RRULE':
assert isinstance(evt[k], icalendar.vRecur)
for rk, rv in event['RRULE'].items():
assert evt[k][rk] == rv
if rk == 'UNTIL':
assert evt[k][rk] == datetime.date.fromisoformat(rv)
else:
assert evt[k][rk] == rv
elif k in ('DTSTART', 'DTEND'):
assert evt.decoded(k) == datetime.date.fromisoformat(v)
else:
assert str(evt[k]) == v
@responses.activate
def test_caldav_event_update_ok_nomock(app, caldav_conn):
"""This test let the caldav lib think it is able to retrieve
an event in order to modify it.
This can trigger errors from icalendar lib when caldav
attempt to save the event
"""
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
username = 'toto'
evt_id = '01234567-abcd'
evt_url = DAV_URL + get_event_path(caldav_conn, username, evt_id)
event = {
'DTSTART': '2020-02-20',
'DTEND': '2020-03-30',
'SUMMARY': 'foobar',
'TRANSP': True,
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR'], 'UNTIL': '2020-10-01'},
}
qs_params = {'username': username, 'event_id': evt_id}
responses.add(responses.GET, evt_url, body=FAKE_ICS)
responses.add(responses.PUT, evt_url, status=201)
resp = app_post(app, url, qs_params, event)
assert resp.json['err'] == 0
assert resp.json['data']['event_id'] == evt_id
@responses.activate
def test_caldav_event_update_notfound(app, caldav_conn):
url = tests.utils.generic_endpoint_url('caldav', 'event/update')