mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Add tests for caldav but they still fail
This commit is contained in:
parent
1e248140b7
commit
b26c78a361
2 changed files with 127 additions and 49 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue