Skip to content

Commit 2fd759e

Browse files
committed
work in progress
1 parent 30f7eab commit 2fd759e

3 files changed

Lines changed: 63 additions & 33 deletions

File tree

caldav/compatibility_hints.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class FeatureSet:
2222
support -> "supported" (default), "unsupported", "fragile", "broken", "ungraceful"
2323
"""
2424
FEATURES = {
25+
"get-current-user-principal": {
26+
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the standard",
27+
"features": {
28+
"with-calendars": "Principal has one or more calendars. Some servers and providers comes with a pre-defined calendar for each user, for other servers a calendar has to be explicitly created (supported means there exists a calendar - it may be because the calendar was already provisioned together with the principal, or it may be because a calendar was created manually, the checks can't see the difference)"
29+
}
30+
},
2531
"rate-limit": {
2632
"type": "client-feature",
2733
"description": "client (or test code) must not send requests too fast",
@@ -40,20 +46,28 @@ class FeatureSet:
4046
"type": "tests-behaviour",
4147
"description": "Deleting a calendar does not delete the objects, or perhaps create/delete of calendars does not work at all. For each test run, every calendar resource object should be deleted for every test run",
4248
},
49+
"create-calendar": {
50+
"description": "RFC4791 says that \"support for MKCALENDAR on the server is only RECOMMENDED and not REQUIRED because some calendar stores only support one calendar per user (or principal), and those are typically pre-created for each account\". Hence a conformant server may opt to not support creating calendars, this is often seen for cloud services (some services allows extra calendars to be made, but not through the CalDAV protocol). (RFC4791 also says that the server MAY support MKCOL in section 8.5.2. I do read it as MKCOL may be used for creating calendars - which is weird, since section 8.5.2 is titled \"external attachments\". We should consider testing this as well)",
51+
"features": {
52+
"auto": {
53+
"description": "Accessing a calendar which does not exist automatically creates it",
54+
}
55+
}
56+
},
4357
"delete-calendar": {
4458
"description": "RFC4791 says nothing about deletion of calendars, so the server implementation is free to choose weather this should be supported or not. Section 3.2.3.2 in RFC 6638 says that if a calendar is deleted, all the calendarobjectresources on the calendar should also be deleted - but it's a bit unclear if this only applies to scheduling objects or not. Some calendar servers moves the object to a trashcan rather than deleting it"
4559
},
4660
"recurrences": {
4761
"description": "Support for recurring events and tasks",
4862
"features": {
49-
"save_load": {
63+
"save-load": {
5064
"description": "Calendar server should accept ojects with RRULE given, as well as objects with recurrence sets, and should be able to deliver back the same data",
5165
"features": {
5266
"event": {"description": "works with events"},
5367
"todo": {"description": "works with tasks"},
5468
}
5569
},
56-
"search_includes_implicit_recurrences": {
70+
"search-includes-implicit-recurrences": {
5771
"description": "RFC says that the server MUST expand recurring components to determine whether any recurrence instances overlap the specified time range. Considered supported i.e. if a search for 2005 yields a yearly event happening first time in 2004.",
5872
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-7.4"],
5973
"features": {
@@ -64,11 +78,11 @@ class FeatureSet:
6478
"todo": {"description": "works with tasks"},
6579
}
6680
},
67-
"expanded_search": {
81+
"expanded-search": {
6882
"description": "According to RFC 4791, the server MUST expand recurrence objects if asked for it - but many server doesn't do that. It doesn't matter much by now, as the client library can do the expandation. Some servers don't do expand at all, others deliver broken data, typically missing RECURRENCE-ID",
6983
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5"],
7084
"features": {
71-
"recurrence_exception_handling": {
85+
"recurrence-exception-handling": {
7286
"description": "Server expand should work correctly also if a recurrence set with exceptions is given"
7387
},
7488
},
@@ -77,7 +91,7 @@ class FeatureSet:
7791
},
7892
}
7993

80-
def __init__(self, feature_set_dict):
94+
def __init__(self, feature_set_dict=None):
8195
"""
8296
TODO: describe the feature_set better.
8397
@@ -86,16 +100,16 @@ def __init__(self, feature_set_dict):
86100
Shortcuts accepted in the dict, like:
87101
88102
{
89-
"recurrences.search_includes_implicit_recurrences.infinite_scope":
103+
"recurrences.search-includes-implicit-recurrences.infinite-scope":
90104
"unsupported" }
91105
92106
is equivalent with
93107
94108
{
95109
"recurrences": {
96110
"features": {
97-
"search_includes_inplicit_recurrences": {
98-
"infinite_scope":
111+
"search-includes-inplicit-recurrences": {
112+
"infinite-scope":
99113
"support": "unsupported" }}}}
100114
101115
(TODO: is this sane? Am I reinventing a configuration language?)
@@ -104,7 +118,8 @@ def __init__(self, feature_set_dict):
104118
## (anyways, that is an internal design decision that may be
105119
## changed ... but we need test code in place)
106120
self._server_features = {}
107-
self.copyFeatureSet(feature_set_dict)
121+
if feature_set_dict:
122+
self.copyFeatureSet(feature_set_dict)
108123

109124
def _copyFeature(self, def_node, server_node, value):
110125
if isinstance(value, str) and not 'support' in server_node:
@@ -168,6 +183,7 @@ def check_support(self, feature, return_type=bool):
168183
## TODO: consider feature_info['type'], be smarter about this
169184
return support.get('support', 'full') == 'full' and not support.get('enable') and not support.get('behaviour')
170185

186+
## TODO: could be a class method?
171187
def find_feature(self, feature: str) -> dict:
172188
"""
173189
Feature should be a string like feature.subfeature.subsubfeature.
@@ -184,6 +200,22 @@ def find_feature(self, feature: str) -> dict:
184200
node = feature.get('features', {})
185201
return feature
186202

203+
def dotted_feature_set_list(self):
204+
return self._dotted_feature_set_list('', self._server_features)
205+
206+
def _dotted_feature_set_list(self, feature_path, feature_node):
207+
ret = {}
208+
if feature_path:
209+
feature_path = f"{feature_path}."
210+
for x in feature_node:
211+
feature = feature_node[x].copy()
212+
full = f"{feature_path}{x}"
213+
ret[full] = feature
214+
if 'features' in feature:
215+
subnode = feature.pop('features')
216+
ret.update(self._dotted_feature_set_list(full, subnode))
217+
return ret
218+
187219
#### OLD STYLE
188220

189221
## The lists below are specifying what tests should be skipped or
@@ -218,8 +250,6 @@ def find_feature(self, feature: str) -> dict:
218250
'no_alarmsearch':
219251
"""Searching for alarms may yield too few or too many or even a 500 internal server error""",
220252

221-
222-
223253
'no_scheduling':
224254
"""RFC6833 is not supported""",
225255

@@ -431,8 +461,8 @@ def find_feature(self, feature: str) -> dict:
431461
}
432462

433463
xandikos = {
434-
"recurrences.expanded_search": {'support': 'ungraceful'},
435-
"recurrences.search_includes_implicit_recurrences": {'support': 'unsupported'},
464+
"recurrences.expanded-search": {'support': 'ungraceful'},
465+
"recurrences.search-includes-implicit-recurrences": {'support': 'unsupported'},
436466
"old_flags": [
437467
## https://github.com/jelmer/xandikos/issues/8
438468
'date_todo_search_ignores_duration',
@@ -463,7 +493,7 @@ def find_feature(self, feature: str) -> dict:
463493
## TODO - there has been quite some development in radicale recently, so this list
464494
## should probably be gone through
465495
radicale = {
466-
"recurrences.expanded_search": {'support': 'ungraceful'},
496+
"recurrences.expanded-search": {'support': 'ungraceful'},
467497
'old_flags': [
468498
## calendar listings and calendar creation works a bit
469499
## "weird" on radicale
@@ -491,9 +521,6 @@ def find_feature(self, feature: str) -> dict:
491521
## ZIMBRA IS THE MOST SILLY, AND THERE ARE REGRESSIONS FOR EVERY RELEASE!
492522
## AAARGH!
493523
zimbra = [
494-
## no idea why this breaks
495-
"non_existing_calendar_found",
496-
497524
## apparently, zimbra has no journal support
498525
'no_journal',
499526

@@ -548,7 +575,6 @@ def find_feature(self, feature: str) -> dict:
548575
## (TODO: do some research on this)
549576
'sync_breaks_on_delete',
550577
'no_recurring_todo',
551-
'non_existing_calendar_found',
552578
'combined_search_not_working',
553579
'text_search_is_exact_match_sometimes',
554580

@@ -684,9 +710,6 @@ def find_feature(self, feature: str) -> dict:
684710
## Known, work in progress
685711
'no_scheduling',
686712

687-
## Not a breach of standard
688-
'non_existing_calendar_found',
689-
690713
## Known, not a breach of standard
691714
'no_supported_components_support',
692715

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ test = [
5656
"tzlocal",
5757
"xandikos",
5858
"radicale",
59-
"pyfakefs"
59+
"caldav_server_tester"
6060
]
6161
[tool.setuptools_scm]
6262
write_to = "caldav/_version.py"

tests/test_caldav.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from caldav.objects import FreeBusy
6363
from caldav.objects import Principal
6464
from caldav.objects import Todo
65+
from caldav_server_tester import ServerQuirkChecker
6566

6667
log = logging.getLogger("caldav")
6768

@@ -848,6 +849,12 @@ def _fixCalendar(self, **kwargs):
848849

849850
return ret
850851

852+
def testCheckCompatibility(self):
853+
checker = ServerQuirkChecker(self.caldav)
854+
## TODO
855+
#checker.check_all()
856+
checker.check_one('CheckMakeDeleteCalendar')
857+
851858
def testSupport(self):
852859
"""
853860
Test the check_*_support methods
@@ -1203,7 +1210,7 @@ def testObjectBySyncToken(self):
12031210
objcnt += len(c.events())
12041211
obj = c.save_event(ev1)
12051212
objcnt += 1
1206-
if self.check_support("recurrences.save_load"):
1213+
if self.check_support("recurrences.save-load"):
12071214
c.save_event(evr)
12081215
objcnt += 1
12091216
if not self.check_compatibility_flag("no_todo"):
@@ -1332,7 +1339,7 @@ def testSync(self):
13321339
objcnt += len(c.events())
13331340
obj = c.save_event(ev1)
13341341
objcnt += 1
1335-
if self.check_support("recurrences.save_load"):
1342+
if self.check_support("recurrences.save-load"):
13361343
c.save_event(evr)
13371344
objcnt += 1
13381345
if not self.check_compatibility_flag("no_todo"):
@@ -2427,7 +2434,7 @@ def testTodoDatesearch(self):
24272434
# Not all servers perform according to (my interpretation of) the RFC.
24282435
foo = 5
24292436
if not self.check_support(
2430-
"recurrences.search_includes_implicit_recurrences.todo"
2437+
"recurrences.search-includes-implicit-recurrences.todo"
24312438
):
24322439
foo -= 1 ## t6 will not be returned
24332440
if self.check_compatibility_flag(
@@ -2442,13 +2449,13 @@ def testTodoDatesearch(self):
24422449
assert len(todos2) == foo
24432450

24442451
## verify that "expand" works
2445-
if self.check_support("recurrences.search_includes_implicit_recurrences.todo"):
2452+
if self.check_support("recurrences.search-includes-implicit-recurrences.todo"):
24462453
## todo1 and todo2 should be the same (todo1 using legacy method)
24472454
## todo1 and todo2 tries doing server side expand, with fallback
24482455
## to client side expand
24492456
assert len([x for x in todos1 if "DTSTART:20020415T1330" in x.data]) == 1
24502457
assert len([x for x in todos2 if "DTSTART:20020415T1330" in x.data]) == 1
2451-
if self.check_support("recurrences.expanded_search"):
2458+
if self.check_support("recurrences.expanded-search"):
24522459
assert (
24532460
len([x for x in todos4 if "DTSTART:20020415T1330" in x.data]) == 1
24542461
)
@@ -2476,7 +2483,7 @@ def testTodoDatesearch(self):
24762483
urls_found = [x.url for x in todos1]
24772484
urls_found2 = [x.url for x in todos2]
24782485
assert urls_found == urls_found2
2479-
if self.check_support("recurrences.search_includes_implicit_recurrences"):
2486+
if self.check_support("recurrences.search-includes-implicit-recurrences"):
24802487
urls_found.remove(t6.url)
24812488
if not self.check_compatibility_flag(
24822489
"vtodo_datesearch_nodtstart_task_is_skipped"
@@ -2968,7 +2975,7 @@ def testRecurringDateSearch(self):
29682975
event?
29692976
"""
29702977
self.skip_on_compatibility_flag("read_only")
2971-
self.skip_unless_support("recurrences.search_includes_implicit_recurrences")
2978+
self.skip_unless_support("recurrences.search-includes-implicit-recurrences")
29722979
self.skip_on_compatibility_flag("no_search")
29732980
c = self._fixCalendar()
29742981

@@ -3019,7 +3026,7 @@ def testRecurringDateSearch(self):
30193026
## due to expandation, the DTSTART should be in 2008
30203027
assert r1[0].data.count("DTSTART;VALUE=DATE:2008") == 1
30213028
assert r2[0].data.count("DTSTART;VALUE=DATE:2008") == 1
3022-
if self.check_support("recurrences.expanded_search"):
3029+
if self.check_support("recurrences.expanded-search"):
30233030
assert r4[0].data.count("DTSTART;VALUE=DATE:2008") == 1
30243031

30253032
## With expand=True and searching over two recurrences ...
@@ -3084,15 +3091,15 @@ def testRecurringDateWithExceptionSearch(self):
30843091
)
30853092

30863093
assert len(r) == 2
3087-
if self.check_support("recurrences.expanded_search"):
3094+
if self.check_support("recurrences.expanded-search"):
30883095
assert len(rs) == 2
30893096

30903097
assert "RRULE" not in r[0].data
30913098
assert "RRULE" not in r[1].data
30923099

30933100
asserts_on_results = [r]
30943101
if self.check_support(
3095-
"recurrences.expanded_search.recurrence_exception_handling"
3102+
"recurrences.expanded-search.recurrence-exception-handling"
30963103
):
30973104
asserts_on_results.append(rs)
30983105

@@ -3121,7 +3128,7 @@ def testEditSingleRecurrence(self):
31213128
Only the recurrence should be edited, not the rest of the
31223129
event.
31233130
"""
3124-
self.skip_unless_support("recurrences.search_includes_implicit_recurrences")
3131+
self.skip_unless_support("recurrences.search-includes-implicit-recurrences")
31253132
cal = self._fixCalendar()
31263133

31273134
## Create a daily recurring event

0 commit comments

Comments
 (0)