caldav: add event creation/update/deletion (#87227)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
5f86102711
commit
665c16bca2
|
@ -16,6 +16,7 @@ Architecture: all
|
|||
Depends: ghostscript,
|
||||
pdftk,
|
||||
poppler-utils,
|
||||
python3-caldav,
|
||||
python3-cmislib,
|
||||
python3-cryptography,
|
||||
python3-dateutil,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 3.2.18 on 2024-02-20 15:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('base', '0030_resourcelog_base_resour_appname_298cbc_idx'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CalDAV',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('title', models.CharField(max_length=50, verbose_name='Title')),
|
||||
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
(
|
||||
'dav_url',
|
||||
models.URLField(
|
||||
help_text='DAV root URL (such as https://test.egw/groupdav.php/)',
|
||||
verbose_name='DAV root URL',
|
||||
),
|
||||
),
|
||||
('dav_login', models.CharField(max_length=128, verbose_name='DAV username')),
|
||||
('dav_password', models.CharField(max_length=512, verbose_name='DAV password')),
|
||||
(
|
||||
'users',
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name='_caldav_caldav_users_+',
|
||||
related_query_name='+',
|
||||
to='base.ApiUser',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'CalDAV',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,305 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2024 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import functools
|
||||
import urllib.parse
|
||||
|
||||
import caldav
|
||||
import requests
|
||||
from django.db import models
|
||||
from django.utils.dateparse import parse_date, parse_datetime
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passerelle.base.models import BaseResource
|
||||
from passerelle.utils.api import endpoint
|
||||
from passerelle.utils.conversion import exception_to_text
|
||||
from passerelle.utils.jsonresponse import APIError
|
||||
|
||||
EVENT_SCHEMA_PART = {
|
||||
'type': 'object',
|
||||
'description': _('Ical event properties ( VEVENT RFC 5545 3.6.1 )'),
|
||||
'required': ['DTSTART', 'DTEND'],
|
||||
'properties': {
|
||||
'DTSTART': {
|
||||
'type': 'string',
|
||||
'description': _('Event start (included) ISO-8601 date-time or date (for allday event)'),
|
||||
},
|
||||
'DTEND': {
|
||||
'type': 'string',
|
||||
'description': _('Event end (excluded) ISO-8601 date-time or date (for allday event)'),
|
||||
},
|
||||
'SUMMARY': {
|
||||
'type': 'string',
|
||||
'description': 'RFC 5545 3.8.1.12',
|
||||
},
|
||||
'DESCRIPTION': {
|
||||
'type': 'string',
|
||||
'description': 'RFC 5545 3.8.2.5',
|
||||
},
|
||||
'LOCATION': {
|
||||
'type': 'string',
|
||||
'description': 'RFC 5545 3.8.1.7',
|
||||
},
|
||||
'TRANSP': {
|
||||
'type': 'boolean',
|
||||
'description': 'Transparent if true else opaque (RFC 5545 3.8.2.7)',
|
||||
},
|
||||
'RRULE': {
|
||||
'description': _('Recurrence rule (RFC 5545 3.8.5.3)'),
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'FREQ': {
|
||||
'type': 'string',
|
||||
'enum': ['WEEKLY', 'MONTHLY', 'YEARLY'],
|
||||
},
|
||||
'BYDAY': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'enum': ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'],
|
||||
},
|
||||
},
|
||||
'BYMONTH': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'integer',
|
||||
'minimum': 1,
|
||||
'maximum': 12,
|
||||
},
|
||||
},
|
||||
'COUNT': {
|
||||
'type': 'integer',
|
||||
'minimum': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
USERNAME_PARAM = {
|
||||
'description': _('The calendar\'s owner username'),
|
||||
'type': 'string',
|
||||
}
|
||||
|
||||
EVENT_UID_PARAM = {
|
||||
'description': _('An event UID'),
|
||||
'type': 'string',
|
||||
}
|
||||
|
||||
|
||||
# Action's request body schema
|
||||
EVENT_SCHEMA = {
|
||||
'$schema': 'http://json-schema.org/draft-04/schema#',
|
||||
'title': _('Event description schema'),
|
||||
'unflatten': True,
|
||||
**EVENT_SCHEMA_PART,
|
||||
}
|
||||
|
||||
|
||||
class CalDAV(BaseResource):
|
||||
dav_url = models.URLField(
|
||||
blank=False,
|
||||
verbose_name=_('DAV root URL'),
|
||||
help_text=_('DAV root URL (such as https://test.egw/groupdav.php/)'),
|
||||
)
|
||||
dav_login = models.CharField(max_length=128, verbose_name=_('DAV username'), blank=False)
|
||||
dav_password = models.CharField(max_length=512, verbose_name=_('DAV password'), blank=False)
|
||||
|
||||
category = _('Misc')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('CalDAV')
|
||||
|
||||
@functools.cached_property
|
||||
def dav_client(self):
|
||||
'''Instanciate a caldav.DAVCLient and return the instance'''
|
||||
return caldav.DAVClient(self.dav_url, username=self.dav_login, password=self.dav_password)
|
||||
|
||||
def check_status(self):
|
||||
'''Attempt a propfind on DAV root URL'''
|
||||
try:
|
||||
rep = self.dav_client.propfind()
|
||||
rep.find_objects_and_props()
|
||||
except caldav.lib.error.AuthorizationError:
|
||||
raise Exception(_('Not authorized: bad login/password ?'))
|
||||
|
||||
@endpoint(
|
||||
name='event',
|
||||
pattern='^create$',
|
||||
example_pattern='create',
|
||||
methods=['post'],
|
||||
post={'request_body': {'schema': {'application/json': EVENT_SCHEMA}}},
|
||||
parameters={
|
||||
'username': USERNAME_PARAM,
|
||||
},
|
||||
)
|
||||
def create_event(self, request, username, post_data):
|
||||
'''Event creation endpoint'''
|
||||
cal = self.get_calendar(username)
|
||||
self._process_event_properties(post_data)
|
||||
|
||||
try:
|
||||
evt = cal.save_event(**post_data)
|
||||
except requests.exceptions.RequestException as expt:
|
||||
raise APIError(
|
||||
_('Error sending creation request to caldav server'),
|
||||
data={
|
||||
'expt_class': str(type(expt)),
|
||||
'expt': str(expt),
|
||||
'username': username,
|
||||
},
|
||||
)
|
||||
return {'data': {'event_id': evt.id}}
|
||||
|
||||
# Patch do not support request_body validation, using post instead
|
||||
@endpoint(
|
||||
name='event',
|
||||
pattern='^update$',
|
||||
example_pattern='update',
|
||||
methods=['post'],
|
||||
post={'request_body': {'schema': {'application/json': EVENT_SCHEMA}}},
|
||||
parameters={
|
||||
'username': USERNAME_PARAM,
|
||||
'event_id': EVENT_UID_PARAM,
|
||||
},
|
||||
)
|
||||
def update_event(self, request, username, event_id, post_data):
|
||||
'''Event update endpoint'''
|
||||
self._process_event_properties(post_data)
|
||||
ical = self.get_event(username, event_id)
|
||||
|
||||
vevent = ical.icalendar_instance.walk('VEVENT')
|
||||
if not len(vevent) == 1:
|
||||
raise APIError(
|
||||
_('Given event (user:%r uid:%r) do not contains VEVENT component') % (username, event_id),
|
||||
data={
|
||||
'username': username,
|
||||
'event_uid': event_id,
|
||||
'VEVENT': str(vevent),
|
||||
},
|
||||
)
|
||||
vevent = vevent[0]
|
||||
# vevent.update(post_data) do not convert values as expected
|
||||
for k, v in post_data.items():
|
||||
vevent[k] = v
|
||||
try:
|
||||
# do not use ical.save(no_create=True) : no_create fails on some calDAV
|
||||
ical.save()
|
||||
except requests.exceptions.RequestException as expt:
|
||||
raise APIError(
|
||||
_('Error sending update request to caldav server'),
|
||||
data={
|
||||
'expt_class': str(type(expt)),
|
||||
'expt': str(expt),
|
||||
'username': username,
|
||||
'event_id': event_id,
|
||||
},
|
||||
)
|
||||
return {'data': {'event_id': ical.id}}
|
||||
|
||||
@endpoint(
|
||||
name='event',
|
||||
pattern='^delete$',
|
||||
example_pattern='delete',
|
||||
methods=['delete'],
|
||||
parameters={
|
||||
'username': USERNAME_PARAM,
|
||||
'event_id': EVENT_UID_PARAM,
|
||||
},
|
||||
)
|
||||
def delete_event(self, request, username, event_id):
|
||||
ical = self.get_event(username, event_id)
|
||||
try:
|
||||
ical.delete()
|
||||
except requests.exceptions.RequestException as expt:
|
||||
raise APIError(
|
||||
_('Error sending deletion request to caldav server'),
|
||||
data={
|
||||
'expt_class': str(type(expt)),
|
||||
'expt': str(expt),
|
||||
'username': username,
|
||||
'event_id': event_id,
|
||||
},
|
||||
)
|
||||
return {}
|
||||
|
||||
def get_event(self, username, event_uid):
|
||||
'''Fetch an event given a username and an event_uid
|
||||
Arguments:
|
||||
- username: Calendar owner's username
|
||||
- event_uid: The event's UID
|
||||
|
||||
Returns an caldav.Event instance
|
||||
'''
|
||||
event_path = '%s/calendar/%s.ics' % (urllib.parse.quote(username), urllib.parse.quote(str(event_uid)))
|
||||
cal = self.get_calendar(username)
|
||||
try:
|
||||
ical = cal.event_by_url(event_path)
|
||||
except caldav.lib.error.DAVError as expt:
|
||||
raise APIError(
|
||||
_('Unable to get event %r in calendar owned by %r') % (event_uid, username),
|
||||
data={
|
||||
'expt': exception_to_text(expt),
|
||||
'expt_cls': str(type(expt)),
|
||||
'username': username,
|
||||
'event_uid': event_uid,
|
||||
},
|
||||
)
|
||||
except requests.exceptions.RequestException as expt:
|
||||
raise APIError(
|
||||
_('Unable to communicate with caldav server while fetching event'),
|
||||
data={
|
||||
'expt': exception_to_text(expt),
|
||||
'expt_class': str(type(expt)),
|
||||
'username': username,
|
||||
'event_uid': event_uid,
|
||||
},
|
||||
)
|
||||
return ical
|
||||
|
||||
def get_calendar(self, username):
|
||||
'''Given a username returns the associated calendar set
|
||||
Arguments:
|
||||
- username: Calendar owner's username
|
||||
|
||||
Returns A caldav.Calendar instance
|
||||
Note: do not raise any caldav exception before a method trying to make
|
||||
a request is called
|
||||
'''
|
||||
path = '%s/calendar' % urllib.parse.quote(username)
|
||||
calendar = caldav.Calendar(client=self.dav_client, url=path)
|
||||
return calendar
|
||||
|
||||
def _process_event_properties(self, data):
|
||||
'''Handles verification & convertion of event properties
|
||||
@note Modify given data dict inplace
|
||||
'''
|
||||
if 'TRANSP' in data:
|
||||
data['TRANSP'] = 'TRANSPARENT' if data['TRANSP'] else 'OPAQUE'
|
||||
|
||||
for dt_field in ('DTSTART', 'DTEND'):
|
||||
value = data[dt_field]
|
||||
try:
|
||||
data[dt_field] = parse_date(value) or parse_datetime(value)
|
||||
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),
|
||||
http_status=400,
|
||||
)
|
|
@ -143,6 +143,7 @@ INSTALLED_APPS = (
|
|||
'passerelle.apps.base_adresse',
|
||||
'passerelle.apps.bbb',
|
||||
'passerelle.apps.bdp',
|
||||
'passerelle.apps.caldav',
|
||||
'passerelle.apps.cartads_cs',
|
||||
'passerelle.apps.choosit',
|
||||
'passerelle.apps.cityweb',
|
||||
|
|
1
setup.py
1
setup.py
|
@ -138,6 +138,7 @@ setup(
|
|||
scripts=['manage.py'],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
'caldav',
|
||||
'django >= 3.2, <3.3',
|
||||
'django-model-utils<4.3',
|
||||
'requests',
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
import datetime
|
||||
import urllib
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import caldav
|
||||
import icalendar
|
||||
import pytest
|
||||
import responses
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
|
||||
|
||||
import tests.utils
|
||||
from passerelle.apps.caldav.models import CalDAV
|
||||
from passerelle.base.models import AccessRight, ApiUser
|
||||
|
||||
DAV_URL = 'http://test.caldav.notld/somedav/'
|
||||
EVENT_PATH_FMT = '%(username)s/calendar/%(uid)s.ics'
|
||||
|
||||
PROPFIND_REPLY = '''<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:multistatus xmlns:D="DAV:">
|
||||
<D:response xmlns:ns0="urn:uuid:02481234-abcd-4321-abcd-00aa00bb00cc/" xmlns:ns2="urn:ietf:params:xml:ns:carddav">
|
||||
<D:href>/somedave/</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:displayname>Test Caldav</D:displayname>
|
||||
<D:owner/>
|
||||
<D:resourcetype><D:collection /></D:resourcetype>
|
||||
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
||||
<D:current-user-principal><D:href>/somedav/principals/users/apiaccess/</D:href></D:current-user-principal>
|
||||
<D:principal-collection-set><D:href>/somedav/principals/</D:href></D:principal-collection-set>
|
||||
<D:getetag>"none"</D:getetag>
|
||||
<D:getcontentlength/>
|
||||
<D:getlastmodified/>
|
||||
<ns2:addressbook-home-set><D:href>/somedav/apiaccess/</D:href></ns2:addressbook-home-set>
|
||||
<ns2:principal-address/>
|
||||
<ns2:directory-gateway><D:href>/somedav/addressbook/</D:href></ns2:directory-gateway>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>
|
||||
'''
|
||||
|
||||
|
||||
SOME_EVENTS = [
|
||||
{'DTSTART': '2020-02-20T20:02', 'DTEND': '2020-02-22T22:43', 'SUMMARY': 'Test'},
|
||||
{
|
||||
'DTSTART': '2020-02-20',
|
||||
'DTEND': '2020-02-22',
|
||||
'SUMMARY': 'Test',
|
||||
'RRULE': {'FREQ': 'MONTHLY', 'BYDAY': ['FR', 'MO']},
|
||||
},
|
||||
{
|
||||
'DTSTART': '2020-02-20',
|
||||
'DTEND': '2020-02-22',
|
||||
'SUMMARY': 'Test',
|
||||
'LOCATION': 'Foo bar',
|
||||
'TRANSP': True,
|
||||
'DESCRIPTION': 'test',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def caldav_conn(db):
|
||||
user = ApiUser.objects.create(username='all', keytype='', key='')
|
||||
cdav = CalDAV.objects.create(dav_url=DAV_URL, slug='test', dav_login='foo', dav_password='hackmeplz')
|
||||
content_type = ContentType.objects.get_for_model(cdav)
|
||||
AccessRight.objects.create(
|
||||
codename='can_access', apiuser=user, resource_type=content_type, resource_pk=cdav.pk
|
||||
)
|
||||
return cdav
|
||||
|
||||
|
||||
def get_fake_event(caldav_conn):
|
||||
vevent = icalendar.Event()
|
||||
for k, v in {
|
||||
'DTSTART': datetime.date(2023, 1, 1),
|
||||
'DTEND': datetime.date(2023, 2, 1),
|
||||
'SUMMARY': 'Test',
|
||||
'UID': '12345678-abcd',
|
||||
}.items():
|
||||
vevent.add(k, v)
|
||||
ical = icalendar.Calendar()
|
||||
ical.add_component(vevent)
|
||||
|
||||
event = caldav.Event(client=caldav_conn.dav_client, data=ical)
|
||||
event.url = caldav.lib.url.URL(DAV_URL + 'foo/calendar/12345678-abcd.ics')
|
||||
calendar = caldav.Calendar(client=caldav_conn.dav_client)
|
||||
calendar.url = caldav.lib.url.URL(DAV_URL + 'foo/calendar')
|
||||
event.parent = calendar
|
||||
return event
|
||||
|
||||
|
||||
def get_calendar_mock(event_path, event_uid):
|
||||
'''Return a mock configured to be usable as a Calendar instance in
|
||||
the caldav connector
|
||||
'''
|
||||
event_url = DAV_URL + event_path
|
||||
url = caldav.lib.url.URL(event_url)
|
||||
evt_conf = {'url': url, 'id': event_uid}
|
||||
mock_event = Mock(**evt_conf)
|
||||
|
||||
ical = icalendar.Calendar()
|
||||
event = icalendar.Event()
|
||||
|
||||
conf = {
|
||||
'url': DAV_URL,
|
||||
'id': '1337',
|
||||
'props': {
|
||||
'dtstart': datetime.date.fromisoformat('2023-02-02'),
|
||||
'dtend': datetime.date.fromisoformat('2023-02-28'),
|
||||
'summary': 'test',
|
||||
},
|
||||
}
|
||||
|
||||
for k, v in conf.items():
|
||||
event.add(k, v)
|
||||
ical.add_component(event)
|
||||
|
||||
get_evt_conf = {
|
||||
'url': url,
|
||||
'id': event_uid,
|
||||
'save': Mock(),
|
||||
'delete': Mock(),
|
||||
'icalendar_instance': ical,
|
||||
}
|
||||
mock_get_event = Mock(**get_evt_conf)
|
||||
cal_conf = {'save_event.return_value': mock_event, 'event_by_url.return_value': mock_get_event}
|
||||
return Mock(**cal_conf)
|
||||
|
||||
|
||||
def qs_url(url, params):
|
||||
'''Append query string to URL'''
|
||||
return url + '?' + urllib.parse.urlencode(params)
|
||||
|
||||
|
||||
def app_post(app, url, params, data, status=None):
|
||||
'''Allows to post data on an URL with query string params and JSON data'''
|
||||
return app.post_json(qs_url(url, params), params=data, status=status)
|
||||
|
||||
|
||||
def get_event_path(caldav_conn, username, uid):
|
||||
return EVENT_PATH_FMT % {
|
||||
'username': urllib.parse.quote(username),
|
||||
'uid': urllib.parse.quote(str(uid)),
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Tests
|
||||
#
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_check_status_ok(app, caldav_conn):
|
||||
responses.add('PROPFIND', DAV_URL, headers={'Content-Type': 'text/xml'}, body=PROPFIND_REPLY)
|
||||
caldav_conn.check_status()
|
||||
assert len(responses.calls) == 1
|
||||
assert responses.calls[0].request.method == 'PROPFIND'
|
||||
assert responses.calls[0].request.url == DAV_URL
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_check_status_login_failed(app, caldav_conn):
|
||||
responses.add(
|
||||
'PROPFIND',
|
||||
DAV_URL,
|
||||
headers={'Content-Type': 'text/xml'},
|
||||
body=PROPFIND_REPLY,
|
||||
status=401,
|
||||
)
|
||||
with pytest.raises(Exception) as expt:
|
||||
caldav_conn.check_status()
|
||||
assert str(expt) == 'Not authorized: bad login/password ?'
|
||||
assert len(responses.calls) == 1
|
||||
assert responses.calls[0].request.method == 'PROPFIND'
|
||||
assert responses.calls[0].request.url == DAV_URL
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_check_status_fails(app, caldav_conn):
|
||||
responses.add('PROPFIND', DAV_URL, status=500)
|
||||
with pytest.raises(Exception):
|
||||
caldav_conn.check_status()
|
||||
assert len(responses.calls) == 1
|
||||
assert responses.calls[0].request.method == 'PROPFIND'
|
||||
assert responses.calls[0].request.url == DAV_URL
|
||||
|
||||
|
||||
@pytest.mark.parametrize('requests_expt', (ConnectTimeout, ReadTimeout, ConnectionError))
|
||||
@responses.activate
|
||||
def test_caldav_check_status_timeout(app, caldav_conn, requests_expt):
|
||||
propfind_mock = responses.add( # pylint: disable=assignment-from-none
|
||||
'PROPFIND', DAV_URL, body=requests_expt('Do not work as expected')
|
||||
)
|
||||
get_mock = responses.add( # pylint: disable=assignment-from-none
|
||||
'GET', DAV_URL, body=requests_expt('Do not work as expected')
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
caldav_conn.check_status()
|
||||
assert len(responses.calls) == 2
|
||||
assert len(propfind_mock.calls) == 1
|
||||
assert len(get_mock.calls) == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_create_allday_ok(app, caldav_conn):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
|
||||
username = 'foo'
|
||||
evt_id = 42
|
||||
evt_path = get_event_path(caldav_conn, username, evt_id)
|
||||
cal_mock = get_calendar_mock(evt_path, evt_id)
|
||||
|
||||
event = {'DTSTART': '2020-02-20', 'DTEND': '2020-03-30', 'SUMMARY': 'foobar'}
|
||||
qs_params = {'username': username}
|
||||
|
||||
with patch('caldav.Calendar', return_value=cal_mock):
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['event_id'] == evt_id
|
||||
|
||||
event_conv = {
|
||||
k: v if k not in ('DTSTART', 'DTEND') else datetime.date.fromisoformat(v)
|
||||
for k, v in event.items()
|
||||
}
|
||||
cal_mock.save_event.assert_called_once_with(**event_conv)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'event',
|
||||
SOME_EVENTS,
|
||||
)
|
||||
@responses.activate
|
||||
def test_caldav_event_create_ok(app, caldav_conn, event):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
|
||||
username = 'foo'
|
||||
evt_id = '12345678-abcd'
|
||||
evt_path = get_event_path(caldav_conn, username, evt_id)
|
||||
cal_mock = get_calendar_mock(evt_path, evt_id)
|
||||
|
||||
qs_params = {'username': username}
|
||||
|
||||
with patch('caldav.Calendar', return_value=cal_mock):
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['event_id'] == evt_id
|
||||
|
||||
save_args = event.copy()
|
||||
caldav_conn._process_event_properties(save_args)
|
||||
cal_mock.save_event.assert_called_once_with(**save_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'event',
|
||||
SOME_EVENTS,
|
||||
)
|
||||
@responses.activate
|
||||
def test_caldav_event_create_ok_nomock(app, caldav_conn, event):
|
||||
'''Testing that caldav is able to create the event instance before sending it'''
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
|
||||
username = 'toto'
|
||||
qs_params = {'username': username}
|
||||
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] != 0
|
||||
assert resp.json['err_desc'] == 'Error sending creation request to caldav server'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'event',
|
||||
SOME_EVENTS,
|
||||
)
|
||||
@responses.activate
|
||||
def test_caldav_event_create_ok_net_err(app, caldav_conn, event):
|
||||
with patch('passerelle.apps.caldav.models.CalDAV.get_event', return_value=get_fake_event(caldav_conn)):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
|
||||
username = 'toto'
|
||||
qs_params = {'username': username}
|
||||
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] != 0
|
||||
assert resp.json['err_desc'] == 'Error sending creation request to caldav server'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dtstart,dtend',
|
||||
[
|
||||
('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'),
|
||||
],
|
||||
)
|
||||
def test_caldav_event_create_bad_dates(app, caldav_conn, dtstart, dtend):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/create')
|
||||
username = 'toto'
|
||||
event = {'DTSTART': dtstart, 'DTEND': dtend, 'SUMMARY': 'hello'}
|
||||
qs_params = {'username': username}
|
||||
|
||||
resp = app_post(app, url, qs_params, event, status=400)
|
||||
|
||||
assert resp.status_code == 400
|
||||
assert resp.json['err'] != 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('transp', [True, False])
|
||||
@responses.activate
|
||||
def test_caldav_event_update_ok(app, transp, caldav_conn):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
|
||||
username = 'toto'
|
||||
evt_id = '01234567-abcd'
|
||||
evt_path = get_event_path(caldav_conn, username, evt_id)
|
||||
cal_mock = get_calendar_mock(evt_path, evt_id)
|
||||
|
||||
event = {'DTSTART': '2020-02-20', 'DTEND': '2020-03-30', 'SUMMARY': 'foobar', 'TRANSP': transp}
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
|
||||
with patch('caldav.Calendar', return_value=cal_mock):
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data']['event_id'] == evt_id
|
||||
|
||||
cal_mock.event_by_url.assert_called_once_with(evt_path)
|
||||
cal_mock.event_by_url().save.assert_called_once_with()
|
||||
|
||||
ical = cal_mock.event_by_url().icalendar_instance
|
||||
evt = ical.walk('VEVENT')[0]
|
||||
for k, v in event.items():
|
||||
if k == 'TRANSP':
|
||||
if v:
|
||||
assert str(evt[k]) == 'TRANSPARENT'
|
||||
else:
|
||||
assert str(evt[k]) == 'OPAQUE'
|
||||
else:
|
||||
assert str(evt[k]) == v
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_update_notfound(app, caldav_conn):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
|
||||
username = 'foo-user'
|
||||
evt_id = '42'
|
||||
evt_path = get_event_path(caldav_conn, username, evt_id)
|
||||
cal_mock = get_calendar_mock(evt_path, evt_id)
|
||||
|
||||
cal_mock.event_by_url().icalendar_instance = icalendar.Calendar() # empty evt
|
||||
|
||||
event = {'DTSTART': '2020-02-20', 'DTEND': '2020-03-30', 'SUMMARY': 'foobar'}
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
|
||||
with patch('caldav.Calendar', return_value=cal_mock):
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'Given event (user:%r uid:%r) do not contains VEVENT component' % (
|
||||
username,
|
||||
evt_id,
|
||||
)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_update_bad_evt(app, caldav_conn):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
|
||||
|
||||
username = 'toto'
|
||||
evt_id = '012345678'
|
||||
evt_path = get_event_path(caldav_conn, username, evt_id)
|
||||
cal_mock = get_calendar_mock(evt_path, evt_id)
|
||||
|
||||
cal_mock.event_by_url.side_effect = caldav.lib.error.NotFoundError('NotFoundError at \'404 Not Found')
|
||||
|
||||
event = {'DTSTART': '2020-02-20', 'DTEND': '2020-03-30', 'SUMMARY': 'foobar'}
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
with patch('caldav.Calendar', return_value=cal_mock):
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] == 1
|
||||
|
||||
cal_mock.event_by_url.assert_called_once_with(evt_path)
|
||||
assert cal_mock.event_by_url.save.call_count == 0
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_update_net_err(app, caldav_conn):
|
||||
with patch('passerelle.apps.caldav.models.CalDAV.get_event', return_value=get_fake_event(caldav_conn)):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/update')
|
||||
username = 'toto'
|
||||
evt_id = '012345678'
|
||||
event = {'DTSTART': '2020-02-20', 'DTEND': '2020-03-30', 'SUMMARY': 'foobar'}
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
resp = app_post(app, url, qs_params, event)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'Error sending update request to caldav server'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_delete_ok(app, caldav_conn):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/delete')
|
||||
|
||||
username = 'toto'
|
||||
evt_id = '012345678-dcba'
|
||||
evt_path = get_event_path(caldav_conn, username, evt_id)
|
||||
cal_mock = get_calendar_mock(evt_path, evt_id)
|
||||
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
|
||||
with patch('caldav.Calendar', return_value=cal_mock):
|
||||
resp = app.delete(qs_url(url, qs_params))
|
||||
assert resp.json['err'] == 0
|
||||
|
||||
cal_mock.event_by_url.assert_called_once_with(evt_path)
|
||||
cal_mock.event_by_url().delete.assert_called_once_with()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_delete_net_err(app, caldav_conn):
|
||||
with patch('passerelle.apps.caldav.models.CalDAV.get_event', return_value=get_fake_event(caldav_conn)):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/delete')
|
||||
|
||||
username = 'toto'
|
||||
evt_id = '012345678-dcba'
|
||||
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
resp = app.delete(qs_url(url, qs_params))
|
||||
assert resp.json['err'] != 0
|
||||
assert resp.json['err_desc'] == 'Error sending deletion request to caldav server'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_caldav_event_delete_get_event_fail(app, caldav_conn):
|
||||
url = tests.utils.generic_endpoint_url('caldav', 'event/delete')
|
||||
|
||||
username = 'toto'
|
||||
evt_id = '012345678-dcba'
|
||||
|
||||
qs_params = {'username': username, 'event_id': evt_id}
|
||||
resp = app.delete(qs_url(url, qs_params))
|
||||
assert resp.json['err'] != 0
|
||||
assert resp.json['err_desc'] == 'Unable to communicate with caldav server while fetching event'
|
Loading…
Reference in New Issue