439 lines
15 KiB
Python
439 lines
15 KiB
Python
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'
|