Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions caldav/compatibility_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,18 @@ class FeatureSet:
"sync-token.delete": {
"description": "Server correctly handles sync-collection reports after objects have been deleted from the calendar (solved in Nextcloud in https://github.com/nextcloud/server/pull/44130)"
},
"scheduling": {
"description": "Server supports CalDAV Scheduling (RFC6638). Detected via the presence of 'calendar-auto-schedule' in the DAV response header.",
"links": ["https://datatracker.ietf.org/doc/html/rfc6638"],
},
"scheduling.mailbox": {
"description": "Server provides schedule-inbox and schedule-outbox collections for the principal (RFC6638 sections 2.2-2.3). When unsupported, calls to schedule_inbox() or schedule_outbox() raise NotFoundError.",
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.2"],
},
"scheduling.calendar-user-address-set": {
"description": "Server provides the calendar-user-address-set property on the principal (RFC6638 section 2.4.1), used to identify a user's email/URI for scheduling purposes. When unsupported, calendar_user_address_set() raises NotFoundError.",
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.4.1"],
},
'freebusy-query': {'description': "freebusy queries come in two flavors, one query can be done towards a CalDAV server as defined in RFC4791, another query can be done through the scheduling framework, RFC 6638. Only RFC4791 is tested for as today"},
"freebusy-query.rfc4791": {
"description": "Server supports free/busy-query REPORT as specified in RFC4791 section 7.10. The REPORT allows clients to query for free/busy time information for a time range. Servers without this support will typically return an error (often 500 Internal Server Error or 501 Not Implemented). Note: RFC6638 defines a different freebusy mechanism for scheduling",
Expand Down Expand Up @@ -707,15 +719,6 @@ def dotted_feature_set_list(self, compact=False):
## * Perhaps some more readable format should be considered (yaml?).
## * Consider how to get this into the documentation
incompatibility_description = {
'no_scheduling':
"""RFC6833 is not supported""",

'no_scheduling_mailbox':
"""Parts of RFC6833 is supported, but not the existence of inbox/mailbox""",

'no_scheduling_calendar_user_address_set':
"""Parts of RFC6833 is supported, but not getting the calendar users addresses""",

'no_default_calendar':
"""The given user starts without an assigned default calendar """
"""(or without pre-defined calendars at all)""",
Expand Down Expand Up @@ -837,14 +840,12 @@ def dotted_feature_set_list(self, compact=False):
"search.text.category.substring": {"support": "unsupported"},
'principal-search': {'support': 'unsupported'},
'freebusy-query.rfc4791': {'support': 'ungraceful', 'behaviour': '500 internal server error'},
"scheduling": {"support": "unsupported"},
"old_flags": [
## https://github.com/jelmer/xandikos/issues/8
'date_todo_search_ignores_duration',
'vtodo_datesearch_nostart_future_tasks_delivered',

## scheduling is not supported
"no_scheduling",

## The test with an rrule and an overridden event passes as
## long as it's with timestamps. With dates, xandikos gets
## into troubles. I've chosen to edit the test to use timestamp
Expand Down Expand Up @@ -872,13 +873,11 @@ def dotted_feature_set_list(self, compact=False):
## this only applies for very simple installations
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},

"scheduling": {"support": "unsupported"},
"old_flags": [
## https://github.com/jelmer/xandikos/issues/8
'date_todo_search_ignores_duration',
'vtodo_datesearch_nostart_future_tasks_delivered',

## scheduling is not supported
"no_scheduling",
]
}

Expand All @@ -895,11 +894,11 @@ def dotted_feature_set_list(self, compact=False):
## this only applies for very simple installations
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},
## freebusy is not supported yet, but on the long-term road map
"scheduling": {"support": "unsupported"},
'old_flags': [
## calendar listings and calendar creation works a bit
## "weird" on radicale

'no_scheduling',
'no_search_openended',

#'text_search_is_exact_match_sometimes',
Expand Down Expand Up @@ -1224,9 +1223,9 @@ def dotted_feature_set_list(self, compact=False):
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
'principal-search': {'support': 'ungraceful'},
'freebusy-query.rfc4791': {'support': 'ungraceful'},
"scheduling": {"support": "unsupported"},
'old_flags': [
'non_existing_raises_other', ## AuthorizationError instead of NotFoundError
'no_scheduling',
'no_supported_components_support',
'no_relships',
],
Expand Down Expand Up @@ -1266,8 +1265,8 @@ def dotted_feature_set_list(self, compact=False):
'search.combined-is-logical-and': {'support': 'unsupported'},
'sync-token': {'support': 'ungraceful'},
'principal-search': {'support': 'unsupported'},
"scheduling": {"support": "unsupported"},
'old_flags': [
'no_scheduling',
#'no_recurring_todo', ## todo
]
}
Expand Down Expand Up @@ -1401,10 +1400,9 @@ def dotted_feature_set_list(self, compact=False):
'basepath': '/webdav/',
'domain': 'purelymail.com',
},
## Known, work in progress
"scheduling": {"support": "unsupported"},
'old_flags': [
## Known, work in progress
'no_scheduling',

## Known, not a breach of standard
'no_supported_components_support',

Expand Down Expand Up @@ -1448,11 +1446,14 @@ def dotted_feature_set_list(self, compact=False):
## was apparently observed working for a while, possibly due to the master/more_checks split-brain git branching incident in the server-checker project.
## unsupported in be26d42b1ca3ff3b4fd183761b4a9b024ce12b84 / 537a23b145487006bb987dee5ab9e00cdebb0492 2026-02-19. Supported when testing again short time after. Either I'm confused or it's "fragile".
#'search.time-range.alarm': {'support': 'unsupported'},
## GMX advertises calendar-auto-schedule but inbox/mailbox and
## calendar-user-address-set are not functional (RFC6638 sub-features).
"scheduling": {"support": "full"},
"scheduling.mailbox": {"support": "unsupported"},
"scheduling.calendar-user-address-set": {"support": "unsupported"},
"old_flags": [
"no_scheduling_mailbox",
#"text_search_is_case_insensitive",
"no_search_openended",
"no_scheduling_calendar_user_address_set",
"vtodo-cannot-be-uncompleted",
]
}
Expand Down
61 changes: 54 additions & 7 deletions tests/caldav_test_servers.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ test-servers:
port: ${CYRUS_PORT:-8802}
username: ${CYRUS_USERNAME:-testuser@test.local}
password: ${CYRUS_PASSWORD:-testpassword}
# Cyrus pre-creates user1-user5 (password 'x'), enabling scheduling tests.
scheduling_users:
- url: http://${CYRUS_HOST:-localhost}:${CYRUS_PORT:-8802}/dav/calendars/user/user1
username: user1@test.local
password: x
- url: http://${CYRUS_HOST:-localhost}:${CYRUS_PORT:-8802}/dav/calendars/user/user2
username: user2@test.local
password: x
- url: http://${CYRUS_HOST:-localhost}:${CYRUS_PORT:-8802}/dav/calendars/user/user3
username: user3@test.local
password: x

sogo:
type: docker
Expand Down Expand Up @@ -150,13 +161,49 @@ test-servers:
# RFC6638 scheduling test users (optional)
# =========================================================================
#
# For testing calendar scheduling (meeting invites, etc.), define
# multiple users that can send invites to each other:

# Preferred: add scheduling_users inside a server block (as shown for Cyrus
# above). The registry merges it into the already-registered server, and
# pytest generates a TestSchedulingForServer<Name> class automatically.
#
# Baikal (user1-user3 in pre-seeded db.sqlite, passwords testpass1-3):
#
# baikal:
# scheduling_users:
# - url: http://localhost:8800/dav.php/
# username: user1
# password: testpass1
# - url: http://localhost:8800/dav.php/
# username: user2
# password: testpass2
# - url: http://localhost:8800/dav.php/
# username: user3
# password: testpass3
#
# SOGo (user1-user3 from init-sogo-users.sql, passwords testpass1-3):
#
# sogo:
# scheduling_users:
# - url: http://localhost:8803/SOGo/dav/user1
# username: user1
# password: testpass1
# - url: http://localhost:8803/SOGo/dav/user2
# username: user2
# password: testpass2
# - url: http://localhost:8803/SOGo/dav/user3
# username: user3
# password: testpass3
#
# Legacy: top-level rfc6638_users creates a single TestScheduling class
# that is not tied to any specific server in the test run. Prefer the
# per-server scheduling_users approach above.
#
# rfc6638_users:
# - url: https://caldav.example.com/dav/user1/
# - url: http://localhost:8802/dav/calendars/user/user1
# username: user1
# password: pass1
# - url: https://caldav.example.com/dav/user2/
# password: x
# - url: http://localhost:8802/dav/calendars/user/user2
# username: user2
# password: pass2
# password: x
# - url: http://localhost:8802/dav/calendars/user/user3
# username: user3
# password: x
Binary file modified tests/docker-test-servers/baikal/Specific/db/db.sqlite
Binary file not shown.
50 changes: 49 additions & 1 deletion tests/docker-test-servers/baikal/create_baikal_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,49 @@ def create_baikal_db(db_path: Path, username: str = "testuser", password: str =
print(f" Digest A1: {ha1}")


def add_baikal_user(db_path: Path, username: str, password: str) -> None:
"""Add an additional user to an existing Baikal SQLite database."""
realm = "BaikalDAV"
ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest()
principal_uri = f"principals/{username}"

conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()

cursor.execute(
"INSERT OR REPLACE INTO users (username, digesta1) VALUES (?, ?)", (username, ha1)
)

cursor.execute(
"INSERT OR IGNORE INTO principals (uri, email, displayname) VALUES (?, ?, ?)",
(principal_uri, f"{username}@baikal.test", f"Test User ({username})"),
)

cursor.execute(
"INSERT INTO calendars (synctoken, components) VALUES (?, ?)",
(1, "VEVENT,VTODO,VJOURNAL"),
)
calendar_id = cursor.lastrowid

cursor.execute(
"""INSERT INTO calendarinstances
(calendarid, principaluri, access, displayname, uri, calendarorder, calendarcolor)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(calendar_id, principal_uri, 1, "Default Calendar", "default", 0, "#3a87ad"),
)

cursor.execute(
"""INSERT INTO addressbooks
(principaluri, displayname, uri, synctoken)
VALUES (?, ?, ?, ?)""",
(principal_uri, "Default Address Book", "default", 1),
)

conn.commit()
conn.close()
print(f"✓ Added user '{username}' to Baikal database")


def create_baikal_config(config_path: Path) -> None:
"""Create Baikal config.php file."""

Expand Down Expand Up @@ -358,10 +401,14 @@ def create_baikal_yaml(yaml_path: Path) -> None:
if __name__ == "__main__":
script_dir = Path(__file__).parent

# Create database
# Create database with primary test user
db_path = script_dir / "Specific" / "db" / "db.sqlite"
create_baikal_db(db_path, username="testuser", password="testpass")

# Add extra users for RFC6638 scheduling tests (need at least 3)
for i in range(1, 4):
add_baikal_user(db_path, username=f"user{i}", password=f"testpass{i}")

# Create legacy PHP config files (for older Baikal versions)
config_path = script_dir / "Specific" / "config.php"
create_baikal_config(config_path)
Expand All @@ -380,4 +427,5 @@ def create_baikal_yaml(yaml_path: Path) -> None:
print("\nCredentials:")
print(" Admin: admin / admin")
print(" User: testuser / testpass")
print(" RFC6638 users: user1/testpass1, user2/testpass2, user3/testpass3")
print(" CalDAV URL: http://localhost:8800/dav.php/")
13 changes: 13 additions & 0 deletions tests/docker-test-servers/sogo/init-sogo-users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,16 @@ CREATE TABLE IF NOT EXISTS sogo_users (
INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
VALUES ('testuser', 'testuser', MD5('testpass'), 'Test User', 'testuser@example.com')
ON DUPLICATE KEY UPDATE c_password=MD5('testpass');

-- Additional users for RFC6638 scheduling tests (need at least 3 users)
INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
VALUES ('user1', 'user1', MD5('testpass1'), 'Test User 1', 'user1@example.com')
ON DUPLICATE KEY UPDATE c_password=MD5('testpass1');

INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
VALUES ('user2', 'user2', MD5('testpass2'), 'Test User 2', 'user2@example.com')
ON DUPLICATE KEY UPDATE c_password=MD5('testpass2');

INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
VALUES ('user3', 'user3', MD5('testpass3'), 'Test User 3', 'user3@example.com')
ON DUPLICATE KEY UPDATE c_password=MD5('testpass3');
49 changes: 31 additions & 18 deletions tests/test_caldav.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,15 +670,11 @@ def test_multi_server_meta_section(self) -> None:
assert len(clients) == 2


@pytest.mark.skipif(
not rfc6638_users, reason="need rfc6638_users to be set in order to run this test"
)
@pytest.mark.skipif(
len(rfc6638_users) < 3,
reason="need at least three users in rfc6638_users to be set in order to run this test",
)
class TestScheduling:
"""Testing support of RFC6638.
class _TestSchedulingBase:
"""
Base class for RFC6638 scheduling tests. Not collected directly by
pytest (no ``Test`` prefix); concrete subclasses supply ``_users``.

TODO: work in progress. Stalled a bit due to lack of proper testing accounts. I haven't managed to get this test to pass at any systems yet, but I believe the problem is not on the library side.
* icloud: cannot really test much with only one test account
available. I did some testing forth and back with emails sent
Expand All @@ -701,6 +697,9 @@ class TestScheduling:
RFC6638.
"""

## Subclasses set this to the list of user connection dicts to use.
_users: list[dict] = []

def _getCalendar(self, i):
calendar_id = "schedulingnosetestcalendar%i" % i
calendar_name = "caldav scheduling test %i" % i
Expand All @@ -713,7 +712,7 @@ def _getCalendar(self, i):
def setup_method(self):
self.clients = []
self.principals = []
for foo in rfc6638_users:
for foo in self._users:
c = client(**foo)
if not c.check_scheduling_support():
continue ## ignoring user because server does not support scheduling.
Expand Down Expand Up @@ -790,6 +789,15 @@ def testInviteAndRespond(self):
## inbox/outbox?


## Legacy: run TestScheduling against the top-level rfc6638_users config.
if rfc6638_users:
TestScheduling = type(
"TestScheduling",
(_TestSchedulingBase,),
{"_users": rfc6638_users},
)


def _delay_decorator(f, t=20):
def foo(*a, **kwa):
time.sleep(t)
Expand Down Expand Up @@ -1093,20 +1101,15 @@ def testSupport(self):
self.skip_on_compatibility_flag("dav_not_supported")
assert self.caldav.check_dav_support()
assert self.caldav.check_cdav_support()
if self.check_compatibility_flag("no_scheduling"):
assert not self.caldav.check_scheduling_support()
else:
assert self.caldav.check_scheduling_support()
assert self.caldav.check_scheduling_support() == self.is_supported("scheduling")

def testSchedulingInfo(self):
self.skip_on_compatibility_flag("no_scheduling")
self.skip_on_compatibility_flag("no_scheduling_calendar_user_address_set")
self.skip_unless_support("scheduling.calendar-user-address-set")
calendar_user_address_set = self.principal.calendar_user_address_set()
me_a_participant = self.principal.get_vcal_address()

def testSchedulingMailboxes(self):
self.skip_on_compatibility_flag("no_scheduling")
self.skip_on_compatibility_flag("no_scheduling_mailbox")
self.skip_unless_support("scheduling.mailbox")
inbox = self.principal.schedule_inbox()
outbox = self.principal.schedule_outbox()

Expand Down Expand Up @@ -3679,3 +3682,13 @@ def testWithEnvironment(self):
(RepeatedFunctionalTestsBaseClass,),
{"server_params": _caldav_server},
)

# If the server has scheduling_users configured, also generate a
# TestSchedulingForServer* class so scheduling tests run per-server.
if "scheduling_users" in _caldav_server:
_sched_classname = "TestSchedulingForServer" + _servername
vars()[_sched_classname] = type(
_sched_classname,
(_TestSchedulingBase,),
{"_users": _caldav_server["scheduling_users"]},
)
Loading
Loading