Add tests for caldav but they still fail

This commit is contained in:
Markus Unterwaditzer 2014-02-26 18:52:39 +01:00
parent 1e248140b7
commit b26c78a361
2 changed files with 127 additions and 49 deletions

View file

@ -15,16 +15,14 @@ from lxml import etree
import requests import requests
import datetime import datetime
CALDAV_LIST_TEMPLATE =
CALDAV_GET_MULTI_TEMPLATE = ''''''
CALDAV_DT_FORMAT = '%Y%m%dT%H%M%SZ' CALDAV_DT_FORMAT = '%Y%m%dT%H%M%SZ'
class CaldavStorage(Storage): class CaldavStorage(Storage):
'''hrefs are full URLs to items''' '''hrefs are full URLs to items'''
def __init__(self, url, username, password, start_date=None, end_date=None, _session = None
verify=True, auth='basic', useragent='vdirsyncer', **kwargs): def __init__(self, url, username='', password='', start_date=None,
end_date=None, verify=True, auth='basic',
useragent='vdirsyncer', _request_func=None, **kwargs):
''' '''
:param url: Direct URL for the CalDAV collection. No autodiscovery. :param url: Direct URL for the CalDAV collection. No autodiscovery.
:param username: Username for authentication. :param username: Username for authentication.
@ -34,10 +32,12 @@ class CaldavStorage(Storage):
:param verify: Verify SSL certificate, default True. :param verify: Verify SSL certificate, default True.
:param auth: Authentication method, from {'basic', 'digest'}, default 'basic'. :param auth: Authentication method, from {'basic', 'digest'}, default 'basic'.
:param useragent: Default 'vdirsyncer'. :param useragent: Default 'vdirsyncer'.
:param _request_func: Function to use for network calls. Same API as
requests.request. Useful for tests.
''' '''
super(CaldavStorage, self).__init__(**kwargs) super(CaldavStorage, self).__init__(**kwargs)
self._request = _request_func or self._request
self.session = requests.session()
self._settings = {'verify': verify} self._settings = {'verify': verify}
if auth == 'basic': if auth == 'basic':
self._settings['auth'] = (username, password) self._settings['auth'] = (username, password)
@ -48,17 +48,16 @@ class CaldavStorage(Storage):
raise ValueError('Unknown authentication method: {}'.format(auth)) raise ValueError('Unknown authentication method: {}'.format(auth))
self.useragent = useragent self.useragent = useragent
self.url = url self.url = url.rstrip('/') + '/'
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
headers = self._default_headers() headers = self._default_headers()
headers['Depth'] = 1 headers['Depth'] = 1
response = self.session.request( response = self._request(
'OPTIONS', 'OPTIONS',
self.url, '',
headers=headers, headers=headers
**self._settings
) )
response.raise_for_status() response.raise_for_status()
if 'calendar-access' not in response.headers['DAV']: if 'calendar-access' not in response.headers['DAV']:
@ -70,6 +69,18 @@ class CaldavStorage(Storage):
'Content-Type': 'application/xml; charset=UTF-8' 'Content-Type': 'application/xml; charset=UTF-8'
} }
def _simplify_href(self, href):
if href.startswith(self.url):
return href[len(self.url):]
return href
def _request(self, method, item, data=None, headers=None):
if self._session is None:
self._session = requests.session()
assert '/' not in item
path = self.url + item
return self._session.request(method, url, data=data, headers=headers, **self._settings)
def list(self): def list(self):
data = '''<?xml version="1.0" encoding="utf-8" ?> data = '''<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
@ -95,18 +106,17 @@ class CaldavStorage(Storage):
else: else:
data = data.format(caldavfilter='') data = data.format(caldavfilter='')
response = self.session.request( response = self._request(
'REPORT', 'REPORT',
self.url, '',
data=data, data=data,
headers=self._default_headers(), headers=self._default_headers()
**self._settings
) )
response.raise_for_status() response.raise_for_status()
root = etree.XML(response.content) root = etree.XML(response.content)
for element in root.iter('{DAV:}response'): for element in root.iter('{DAV:}response'):
etag = element.find('{DAV:}propstat').find('{DAV:}prop').find('{DAV:}getetag').text etag = element.find('{DAV:}propstat').find('{DAV:}prop').find('{DAV:}getetag').text
href = element.find('{DAV:}href').text href = self._simplify_href(element.find('{DAV:}href').text)
yield href, etag yield href, etag
def get_multi(self, hrefs): def get_multi(self, hrefs):
@ -121,23 +131,25 @@ class CaldavStorage(Storage):
</D:prop> </D:prop>
{hrefs} {hrefs}
</C:calendar-multiget>''' </C:calendar-multiget>'''
hrefs = '\n'.join('<D:href>{}</D:href>'.format(href=href) for href in hrefs) href_xml = []
data = data.format(hrefs=hrefs) for href in hrefs:
response = self.session.request( assert '/' not in href
href_xml.append('<D:href>{}</D:href>'.format(self.url + href))
data = data.format(hrefs='\n'.join(href_xml))
response = self._request(
'REPORT', 'REPORT',
self.url, '',
data=data, data=data,
headers=self._default_headers(), headers=self._default_headers()
**self._settings
) )
if response != 404: # nonexisting hrefs will be handled separately status_code = response.status_code
response.raise_for_status() response.raise_for_status()
c = response.x.get_data()
root = etree.XML(response.content) root = etree.XML(response.content)
finished_hrefs = set()
rv = [] rv = []
hrefs_left = set(hrefs)
for element in root.iter('{DAV:}response'): for element in root.iter('{DAV:}response'):
try: href = self._simplify_href(element.find('{DAV:}href').text)
href = element.find('{DAV:}href').text
obj = element \ obj = element \
.find('{DAV:}propstat') \ .find('{DAV:}propstat') \
.find('{DAV:}prop') \ .find('{DAV:}prop') \
@ -146,11 +158,9 @@ class CaldavStorage(Storage):
.find('{DAV:}propstat') \ .find('{DAV:}propstat') \
.find('{DAV:}prop') \ .find('{DAV:}prop') \
.find('{DAV:}getetag').text .find('{DAV:}getetag').text
except AttributeError:
continue
rv.append((href, Item(obj), etag)) rv.append((href, Item(obj), etag))
finished_hrefs.add(href) hrefs_left.remove(href)
for href in set(hrefs) - finished_hrefs: for href in hrefs_left:
raise exceptions.NotFoundError(href) raise exceptions.NotFoundError(href)
return rv return rv
@ -168,18 +178,20 @@ class CaldavStorage(Storage):
return True return True
def upload(self, obj): def upload(self, obj):
href = self.url + self._get_href(obj.uid) href = self._get_href(obj.uid)
headers = self._default_headers() headers = self._default_headers()
headers.update({ headers.update({
'Content-Type': 'text/calendar', 'Content-Type': 'text/calendar',
'If-None-Match': '*' 'If-None-Match': '*'
}) })
response = requests.put( response = self._request(
'PUT',
href, href,
data=obj.raw data=obj.raw,
headers=headers, headers=headers
**self._settings
) )
if response.status_code != 201:
raise exceptions.StorageError('Unexpected response with content {}'.format(repr(response.content)))
response.raise_for_status() response.raise_for_status()
if not response.headers.get('etag', None): if not response.headers.get('etag', None):
@ -193,11 +205,11 @@ class CaldavStorage(Storage):
'Content-Type': 'text/calendar', 'Content-Type': 'text/calendar',
'If-Match': etag 'If-Match': etag
}) })
response = requests.put( response = self._request(
remotepath, 'PUT',
href,
data=obj.raw, data=obj.raw,
headers=headers, headers=headers
**self._settings
) )
response.raise_for_status() response.raise_for_status()
@ -205,3 +217,16 @@ class CaldavStorage(Storage):
obj2, etag = self.get(href) obj2, etag = self.get(href)
assert obj2.raw == obj.raw assert obj2.raw == obj.raw
return href, etag return href, etag
def delete(self, href, etag):
headers = self._default_headers()
headers.update({
'If-Match': etag
})
response = self._request(
'DELETE',
href,
headers=headers
)
response.raise_for_status()

View file

@ -15,6 +15,7 @@ import shutil
from vdirsyncer.storage.base import Item from vdirsyncer.storage.base import Item
from vdirsyncer.storage.filesystem import FilesystemStorage from vdirsyncer.storage.filesystem import FilesystemStorage
from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.storage.memory import MemoryStorage
from vdirsyncer.storage.caldav import CaldavStorage
import vdirsyncer.exceptions as exceptions import vdirsyncer.exceptions as exceptions
class StorageTests(object): class StorageTests(object):
@ -80,3 +81,55 @@ class FilesystemStorageTests(TestCase, StorageTests):
class MemoryStorageTests(TestCase, StorageTests): class MemoryStorageTests(TestCase, StorageTests):
def _get_storage(self, **kwargs): def _get_storage(self, **kwargs):
return MemoryStorage(**kwargs) return MemoryStorage(**kwargs)
class CaldavStorageTests(TestCase, StorageTests):
tmpdir = None
old_radicale_config_key = None
def _get_storage(self, **kwargs):
self.tmpdir = tempfile.mkdtemp()
os.environ['RADICALE_CONFIG'] = ''
import radicale.config as radicale_config
radicale_config.set('storage', 'type', 'filesystem')
radicale_config.set('storage', 'filesystem_folder', self.tmpdir)
radicale_config.set('rights', 'type', 'None')
from radicale import Application
app = Application()
import radicale.log
radicale.log.start()
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse as WerkzeugResponse
class Response(object):
'''Fake API of requests module'''
def __init__(self, x):
self.x = x
self.status_code = x.status_code
self.content = x.get_data(as_text=False)
self.headers = x.headers
def raise_for_status(self):
'''copied from requests itself'''
if 400 <= self.status_code < 600:
from requests.exceptions import HTTPError
raise HTTPError(str(self.status_code))
c = Client(app, WerkzeugResponse)
server = 'http://127.0.0.1'
calendar_path = '/bob/test.ics/'
full_url = server + calendar_path
def x(method, item, data=None, headers=None):
assert '/' not in item
url = calendar_path + item
r = c.open(path=url, method=method, data=data, headers=headers)
r = Response(r)
return r
return CaldavStorage(full_url, _request_func=x)
def tearDown(self):
self.app = None
if self.tmpdir is not None:
shutil.rmtree(self.tmpdir)
self.tmpdir = None