mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-05 10:45:51 +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 datetime
|
||||
|
||||
CALDAV_LIST_TEMPLATE =
|
||||
|
||||
CALDAV_GET_MULTI_TEMPLATE = ''''''
|
||||
|
||||
CALDAV_DT_FORMAT = '%Y%m%dT%H%M%SZ'
|
||||
|
||||
class CaldavStorage(Storage):
|
||||
'''hrefs are full URLs to items'''
|
||||
def __init__(self, url, username, password, start_date=None, end_date=None,
|
||||
verify=True, auth='basic', useragent='vdirsyncer', **kwargs):
|
||||
_session = None
|
||||
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 username: Username for authentication.
|
||||
|
|
@ -34,10 +32,12 @@ class CaldavStorage(Storage):
|
|||
:param verify: Verify SSL certificate, default True.
|
||||
:param auth: Authentication method, from {'basic', 'digest'}, default 'basic'.
|
||||
: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)
|
||||
self._request = _request_func or self._request
|
||||
|
||||
self.session = requests.session()
|
||||
self._settings = {'verify': verify}
|
||||
if auth == 'basic':
|
||||
self._settings['auth'] = (username, password)
|
||||
|
|
@ -48,17 +48,16 @@ class CaldavStorage(Storage):
|
|||
raise ValueError('Unknown authentication method: {}'.format(auth))
|
||||
|
||||
self.useragent = useragent
|
||||
self.url = url
|
||||
self.url = url.rstrip('/') + '/'
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
|
||||
headers = self._default_headers()
|
||||
headers['Depth'] = 1
|
||||
response = self.session.request(
|
||||
response = self._request(
|
||||
'OPTIONS',
|
||||
self.url,
|
||||
headers=headers,
|
||||
**self._settings
|
||||
'',
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
if 'calendar-access' not in response.headers['DAV']:
|
||||
|
|
@ -70,6 +69,18 @@ class CaldavStorage(Storage):
|
|||
'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):
|
||||
data = '''<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
|
|
@ -95,18 +106,17 @@ class CaldavStorage(Storage):
|
|||
else:
|
||||
data = data.format(caldavfilter='')
|
||||
|
||||
response = self.session.request(
|
||||
response = self._request(
|
||||
'REPORT',
|
||||
self.url,
|
||||
'',
|
||||
data=data,
|
||||
headers=self._default_headers(),
|
||||
**self._settings
|
||||
headers=self._default_headers()
|
||||
)
|
||||
response.raise_for_status()
|
||||
root = etree.XML(response.content)
|
||||
for element in root.iter('{DAV:}response'):
|
||||
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
|
||||
|
||||
def get_multi(self, hrefs):
|
||||
|
|
@ -121,36 +131,36 @@ class CaldavStorage(Storage):
|
|||
</D:prop>
|
||||
{hrefs}
|
||||
</C:calendar-multiget>'''
|
||||
hrefs = '\n'.join('<D:href>{}</D:href>'.format(href=href) for href in hrefs)
|
||||
data = data.format(hrefs=hrefs)
|
||||
response = self.session.request(
|
||||
href_xml = []
|
||||
for href in hrefs:
|
||||
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',
|
||||
self.url,
|
||||
'',
|
||||
data=data,
|
||||
headers=self._default_headers(),
|
||||
**self._settings
|
||||
headers=self._default_headers()
|
||||
)
|
||||
if response != 404: # nonexisting hrefs will be handled separately
|
||||
response.raise_for_status()
|
||||
status_code = response.status_code
|
||||
response.raise_for_status()
|
||||
c = response.x.get_data()
|
||||
root = etree.XML(response.content)
|
||||
finished_hrefs = set()
|
||||
rv = []
|
||||
hrefs_left = set(hrefs)
|
||||
for element in root.iter('{DAV:}response'):
|
||||
try:
|
||||
href = element.find('{DAV:}href').text
|
||||
obj = element \
|
||||
.find('{DAV:}propstat') \
|
||||
.find('{DAV:}prop') \
|
||||
.find('{urn:ietf:params:xml:ns:caldav}calendar-data').text
|
||||
etag = element \
|
||||
.find('{DAV:}propstat') \
|
||||
.find('{DAV:}prop') \
|
||||
.find('{DAV:}getetag').text
|
||||
except AttributeError:
|
||||
continue
|
||||
href = self._simplify_href(element.find('{DAV:}href').text)
|
||||
obj = element \
|
||||
.find('{DAV:}propstat') \
|
||||
.find('{DAV:}prop') \
|
||||
.find('{urn:ietf:params:xml:ns:caldav}calendar-data').text
|
||||
etag = element \
|
||||
.find('{DAV:}propstat') \
|
||||
.find('{DAV:}prop') \
|
||||
.find('{DAV:}getetag').text
|
||||
rv.append((href, Item(obj), etag))
|
||||
finished_hrefs.add(href)
|
||||
for href in set(hrefs) - finished_hrefs:
|
||||
hrefs_left.remove(href)
|
||||
for href in hrefs_left:
|
||||
raise exceptions.NotFoundError(href)
|
||||
return rv
|
||||
|
||||
|
|
@ -168,18 +178,20 @@ class CaldavStorage(Storage):
|
|||
return True
|
||||
|
||||
def upload(self, obj):
|
||||
href = self.url + self._get_href(obj.uid)
|
||||
href = self._get_href(obj.uid)
|
||||
headers = self._default_headers()
|
||||
headers.update({
|
||||
'Content-Type': 'text/calendar',
|
||||
'If-None-Match': '*'
|
||||
})
|
||||
response = requests.put(
|
||||
response = self._request(
|
||||
'PUT',
|
||||
href,
|
||||
data=obj.raw
|
||||
headers=headers,
|
||||
**self._settings
|
||||
data=obj.raw,
|
||||
headers=headers
|
||||
)
|
||||
if response.status_code != 201:
|
||||
raise exceptions.StorageError('Unexpected response with content {}'.format(repr(response.content)))
|
||||
response.raise_for_status()
|
||||
|
||||
if not response.headers.get('etag', None):
|
||||
|
|
@ -193,11 +205,11 @@ class CaldavStorage(Storage):
|
|||
'Content-Type': 'text/calendar',
|
||||
'If-Match': etag
|
||||
})
|
||||
response = requests.put(
|
||||
remotepath,
|
||||
response = self._request(
|
||||
'PUT',
|
||||
href,
|
||||
data=obj.raw,
|
||||
headers=headers,
|
||||
**self._settings
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
|
|
@ -205,3 +217,16 @@ class CaldavStorage(Storage):
|
|||
obj2, etag = self.get(href)
|
||||
assert obj2.raw == obj.raw
|
||||
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.filesystem import FilesystemStorage
|
||||
from vdirsyncer.storage.memory import MemoryStorage
|
||||
from vdirsyncer.storage.caldav import CaldavStorage
|
||||
import vdirsyncer.exceptions as exceptions
|
||||
|
||||
class StorageTests(object):
|
||||
|
|
@ -80,3 +81,55 @@ class FilesystemStorageTests(TestCase, StorageTests):
|
|||
class MemoryStorageTests(TestCase, StorageTests):
|
||||
def _get_storage(self, **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