Skip to content

Commit 3e182a1

Browse files
committed
feat: add RFC6638 scheduling feature support and test infrastructure
Add scheduling support to FeatureSet compatibility hints: - New 'scheduling' feature and sub-features (scheduling.mailbox, scheduling.calendar-user-address-set) - Migrate legacy no_scheduling old flags to the new feature system - Add scheduling_users defaults for Nextcloud, DAViCal, and Zimbra Add RFC6638 scheduling test infrastructure: - Enable multi-user scheduling tests via docker server setups - Pre-populate scheduling_users in Cyrus and Baikal server classes - Fix test collection and server config merging for scheduling_users
1 parent 3c6d075 commit 3e182a1

File tree

16 files changed

+377
-88
lines changed

16 files changed

+377
-88
lines changed

.github/workflows/tests.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ jobs:
149149
# Create test user
150150
docker exec -e OC_PASS="testpass" ${{ job.services.nextcloud.id }} php occ user:add --password-from-env --display-name="Test User" testuser || echo "User may already exist"
151151
152+
# Create scheduling test users (user1-user3)
153+
for i in 1 2 3; do
154+
docker exec -e OC_PASS="testpass${i}" ${{ job.services.nextcloud.id }} php occ user:add --password-from-env --display-name="User ${i}" "user${i}" || echo "user${i} may already exist"
155+
done
156+
152157
# Enable calendar and contacts apps
153158
docker exec ${{ job.services.nextcloud.id }} php occ app:enable calendar || true
154159
docker exec ${{ job.services.nextcloud.id }} php occ app:enable contacts || true

caldav/compatibility_hints.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,18 @@ class FeatureSet:
264264
"sync-token.delete": {
265265
"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)"
266266
},
267+
"scheduling": {
268+
"description": "Server supports CalDAV Scheduling (RFC6638). Detected via the presence of 'calendar-auto-schedule' in the DAV response header.",
269+
"links": ["https://datatracker.ietf.org/doc/html/rfc6638"],
270+
},
271+
"scheduling.mailbox": {
272+
"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.",
273+
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.2"],
274+
},
275+
"scheduling.calendar-user-address-set": {
276+
"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.",
277+
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.4.1"],
278+
},
267279
'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"},
268280
"freebusy-query.rfc4791": {
269281
"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",
@@ -710,15 +722,6 @@ def dotted_feature_set_list(self, compact=False):
710722
## * Perhaps some more readable format should be considered (yaml?).
711723
## * Consider how to get this into the documentation
712724
incompatibility_description = {
713-
'no_scheduling':
714-
"""RFC6833 is not supported""",
715-
716-
'no_scheduling_mailbox':
717-
"""Parts of RFC6833 is supported, but not the existence of inbox/mailbox""",
718-
719-
'no_scheduling_calendar_user_address_set':
720-
"""Parts of RFC6833 is supported, but not getting the calendar users addresses""",
721-
722725
'no_default_calendar':
723726
"""The given user starts without an assigned default calendar """
724727
"""(or without pre-defined calendars at all)""",
@@ -837,14 +840,12 @@ def dotted_feature_set_list(self, compact=False):
837840
"search.text.category.substring": {"support": "unsupported"},
838841
'principal-search': {'support': 'unsupported'},
839842
'freebusy-query.rfc4791': {'support': 'ungraceful', 'behaviour': '500 internal server error'},
843+
"scheduling": {"support": "unsupported"},
840844
"old_flags": [
841845
## https://github.com/jelmer/xandikos/issues/8
842846
'date_todo_search_ignores_duration',
843847
'vtodo_datesearch_nostart_future_tasks_delivered',
844848

845-
## scheduling is not supported
846-
"no_scheduling",
847-
848849
## The test with an rrule and an overridden event passes as
849850
## long as it's with timestamps. With dates, xandikos gets
850851
## into troubles. I've chosen to edit the test to use timestamp
@@ -872,13 +873,11 @@ def dotted_feature_set_list(self, compact=False):
872873
## this only applies for very simple installations
873874
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},
874875

876+
"scheduling": {"support": "unsupported"},
875877
"old_flags": [
876878
## https://github.com/jelmer/xandikos/issues/8
877879
'date_todo_search_ignores_duration',
878880
'vtodo_datesearch_nostart_future_tasks_delivered',
879-
880-
## scheduling is not supported
881-
"no_scheduling",
882881
]
883882
}
884883

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

902-
'no_scheduling',
903902
'no_search_openended',
904903

905904
#'text_search_is_exact_match_sometimes',
@@ -1224,9 +1223,10 @@ def dotted_feature_set_list(self, compact=False):
12241223
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
12251224
'principal-search': {'support': 'ungraceful'},
12261225
'freebusy-query.rfc4791': {'support': 'ungraceful'},
1226+
"scheduling": {"support": "unsupported"},
12271227
'old_flags': [
12281228
'non_existing_raises_other', ## AuthorizationError instead of NotFoundError
1229-
'no_scheduling',
1229+
'no_supported_components_support',
12301230
'no_relships',
12311231
],
12321232
'test-calendar': {'cleanup-regime': 'wipe-calendar'},
@@ -1266,8 +1266,8 @@ def dotted_feature_set_list(self, compact=False):
12661266
'search.combined-is-logical-and': {'support': 'unsupported'},
12671267
'sync-token': {'support': 'ungraceful'},
12681268
'principal-search': {'support': 'unsupported'},
1269+
"scheduling": {"support": "unsupported"},
12691270
'old_flags': [
1270-
'no_scheduling',
12711271
#'no_recurring_todo', ## todo
12721272
]
12731273
}
@@ -1401,9 +1401,10 @@ def dotted_feature_set_list(self, compact=False):
14011401
'basepath': '/webdav/',
14021402
'domain': 'purelymail.com',
14031403
},
1404+
## Known, work in progress
1405+
"scheduling": {"support": "unsupported"},
14041406
'old_flags': [
1405-
## Known, work in progress
1406-
'no_scheduling',
1407+
'no_supported_components_support',
14071408
],
14081409
## Known, not a breach of standard
14091410
"get-supported-components": {"support": "unsupported"},
@@ -1445,11 +1446,14 @@ def dotted_feature_set_list(self, compact=False):
14451446
## was apparently observed working for a while, possibly due to the master/more_checks split-brain git branching incident in the server-checker project.
14461447
## unsupported in be26d42b1ca3ff3b4fd183761b4a9b024ce12b84 / 537a23b145487006bb987dee5ab9e00cdebb0492 2026-02-19. Supported when testing again short time after. Either I'm confused or it's "fragile".
14471448
#'search.time-range.alarm': {'support': 'unsupported'},
1449+
## GMX advertises calendar-auto-schedule but inbox/mailbox and
1450+
## calendar-user-address-set are not functional (RFC6638 sub-features).
1451+
"scheduling": {"support": "full"},
1452+
"scheduling.mailbox": {"support": "unsupported"},
1453+
"scheduling.calendar-user-address-set": {"support": "unsupported"},
14481454
"old_flags": [
1449-
"no_scheduling_mailbox",
14501455
#"text_search_is_case_insensitive",
14511456
"no_search_openended",
1452-
"no_scheduling_calendar_user_address_set",
14531457
"vtodo-cannot-be-uncompleted",
14541458
]
14551459
}

tests/caldav_test_servers.yaml.example

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ test-servers:
5252
port: ${NEXTCLOUD_PORT:-8801}
5353
username: ${NEXTCLOUD_USERNAME:-testuser}
5454
password: ${NEXTCLOUD_PASSWORD:-testpass}
55+
# setup_nextcloud.sh creates user1-user3 (passwords testpass1-3) for scheduling tests.
5556

5657
cyrus:
5758
type: docker
@@ -60,6 +61,17 @@ test-servers:
6061
port: ${CYRUS_PORT:-8802}
6162
username: ${CYRUS_USERNAME:-testuser@test.local}
6263
password: ${CYRUS_PASSWORD:-testpassword}
64+
# Cyrus pre-creates user1-user5 (password 'x'), enabling scheduling tests.
65+
scheduling_users:
66+
- url: http://${CYRUS_HOST:-localhost}:${CYRUS_PORT:-8802}/dav/calendars/user/user1
67+
username: user1@test.local
68+
password: x
69+
- url: http://${CYRUS_HOST:-localhost}:${CYRUS_PORT:-8802}/dav/calendars/user/user2
70+
username: user2@test.local
71+
password: x
72+
- url: http://${CYRUS_HOST:-localhost}:${CYRUS_PORT:-8802}/dav/calendars/user/user3
73+
username: user3@test.local
74+
password: x
6375

6476
sogo:
6577
type: docker
@@ -84,6 +96,7 @@ test-servers:
8496
port: ${DAVICAL_PORT:-8805}
8597
username: ${DAVICAL_USERNAME:-testuser}
8698
password: ${DAVICAL_PASSWORD:-testpass}
99+
# setup_davical.sh creates user1-user3 (passwords testpass1-3) for scheduling tests.
87100

88101
davis:
89102
type: docker
@@ -108,6 +121,7 @@ test-servers:
108121
port: ${ZIMBRA_PORT:-8808}
109122
username: ${ZIMBRA_USERNAME:-testuser@zimbra.io}
110123
password: ${ZIMBRA_PASSWORD:-testpass}
124+
# start.sh creates testuser/testuser2/testuser3@zimbra.io (password testpass) for scheduling tests.
111125

112126
stalwart:
113127
type: docker
@@ -150,13 +164,49 @@ test-servers:
150164
# RFC6638 scheduling test users (optional)
151165
# =========================================================================
152166
#
153-
# For testing calendar scheduling (meeting invites, etc.), define
154-
# multiple users that can send invites to each other:
155-
167+
# Preferred: add scheduling_users inside a server block (as shown for Cyrus
168+
# above). The registry merges it into the already-registered server, and
169+
# pytest generates a TestSchedulingForServer<Name> class automatically.
170+
#
171+
# Baikal (user1-user3 in pre-seeded db.sqlite, passwords testpass1-3):
172+
#
173+
# baikal:
174+
# scheduling_users:
175+
# - url: http://localhost:8800/dav.php/
176+
# username: user1
177+
# password: testpass1
178+
# - url: http://localhost:8800/dav.php/
179+
# username: user2
180+
# password: testpass2
181+
# - url: http://localhost:8800/dav.php/
182+
# username: user3
183+
# password: testpass3
184+
#
185+
# SOGo (user1-user3 from init-sogo-users.sql, passwords testpass1-3):
186+
#
187+
# sogo:
188+
# scheduling_users:
189+
# - url: http://localhost:8803/SOGo/dav/user1
190+
# username: user1
191+
# password: testpass1
192+
# - url: http://localhost:8803/SOGo/dav/user2
193+
# username: user2
194+
# password: testpass2
195+
# - url: http://localhost:8803/SOGo/dav/user3
196+
# username: user3
197+
# password: testpass3
198+
#
199+
# Legacy: top-level rfc6638_users creates a single TestScheduling class
200+
# that is not tied to any specific server in the test run. Prefer the
201+
# per-server scheduling_users approach above.
202+
#
156203
# rfc6638_users:
157-
# - url: https://caldav.example.com/dav/user1/
204+
# - url: http://localhost:8802/dav/calendars/user/user1
158205
# username: user1
159-
# password: pass1
160-
# - url: https://caldav.example.com/dav/user2/
206+
# password: x
207+
# - url: http://localhost:8802/dav/calendars/user/user2
161208
# username: user2
162-
# password: pass2
209+
# password: x
210+
# - url: http://localhost:8802/dav/calendars/user/user3
211+
# username: user3
212+
# password: x
0 Bytes
Binary file not shown.

tests/docker-test-servers/baikal/create_baikal_db.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,47 @@ def create_baikal_db(db_path: Path, username: str = "testuser", password: str =
232232
print(f" Digest A1: {ha1}")
233233

234234

235+
def add_baikal_user(db_path: Path, username: str, password: str) -> None:
236+
"""Add an additional user to an existing Baikal SQLite database."""
237+
realm = "BaikalDAV"
238+
ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest()
239+
principal_uri = f"principals/{username}"
240+
241+
conn = sqlite3.connect(str(db_path))
242+
cursor = conn.cursor()
243+
244+
cursor.execute("INSERT OR REPLACE INTO users (username, digesta1) VALUES (?, ?)", (username, ha1))
245+
246+
cursor.execute(
247+
"INSERT OR IGNORE INTO principals (uri, email, displayname) VALUES (?, ?, ?)",
248+
(principal_uri, f"{username}@baikal.test", f"Test User ({username})"),
249+
)
250+
251+
cursor.execute(
252+
"INSERT INTO calendars (synctoken, components) VALUES (?, ?)",
253+
(1, "VEVENT,VTODO,VJOURNAL"),
254+
)
255+
calendar_id = cursor.lastrowid
256+
257+
cursor.execute(
258+
"""INSERT INTO calendarinstances
259+
(calendarid, principaluri, access, displayname, uri, calendarorder, calendarcolor)
260+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
261+
(calendar_id, principal_uri, 1, "Default Calendar", "default", 0, "#3a87ad"),
262+
)
263+
264+
cursor.execute(
265+
"""INSERT INTO addressbooks
266+
(principaluri, displayname, uri, synctoken)
267+
VALUES (?, ?, ?, ?)""",
268+
(principal_uri, "Default Address Book", "default", 1),
269+
)
270+
271+
conn.commit()
272+
conn.close()
273+
print(f"✓ Added user '{username}' to Baikal database")
274+
275+
235276
def create_baikal_config(config_path: Path) -> None:
236277
"""Create Baikal config.php file."""
237278

@@ -358,10 +399,14 @@ def create_baikal_yaml(yaml_path: Path) -> None:
358399
if __name__ == "__main__":
359400
script_dir = Path(__file__).parent
360401

361-
# Create database
402+
# Create database with primary test user
362403
db_path = script_dir / "Specific" / "db" / "db.sqlite"
363404
create_baikal_db(db_path, username="testuser", password="testpass")
364405

406+
# Add extra users for RFC6638 scheduling tests (need at least 3)
407+
for i in range(1, 4):
408+
add_baikal_user(db_path, username=f"user{i}", password=f"testpass{i}")
409+
365410
# Create legacy PHP config files (for older Baikal versions)
366411
config_path = script_dir / "Specific" / "config.php"
367412
create_baikal_config(config_path)
@@ -380,4 +425,5 @@ def create_baikal_yaml(yaml_path: Path) -> None:
380425
print("\nCredentials:")
381426
print(" Admin: admin / admin")
382427
print(" User: testuser / testpass")
428+
print(" RFC6638 users: user1/testpass1, user2/testpass2, user3/testpass3")
383429
print(" CalDAV URL: http://localhost:8800/dav.php/")

tests/docker-test-servers/davical/setup_davical.sh

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,32 @@ for i in $(seq 1 $max_attempts); do
3434
sleep 3
3535
done
3636

37-
echo ""
38-
echo "Creating test user..."
39-
# Check if user already exists
40-
EXISTING=$(run_sql "SELECT username FROM usr WHERE username='${TEST_USER}'")
41-
if [ -n "$EXISTING" ]; then
42-
echo "User '${TEST_USER}' already exists, skipping creation"
43-
else
44-
run_sql "INSERT INTO usr (username, password, fullname, email) VALUES ('${TEST_USER}', '**${TEST_PASSWORD}', 'Test User', '${TEST_USER}@example.com')"
45-
echo "User created"
46-
fi
37+
create_user() {
38+
local username="$1"
39+
local password="$2"
40+
local fullname="$3"
41+
EXISTING=$(run_sql "SELECT username FROM usr WHERE username='${username}'")
42+
if [ -n "$EXISTING" ]; then
43+
echo "User '${username}' already exists, skipping creation"
44+
else
45+
run_sql "INSERT INTO usr (username, password, fullname, email) VALUES ('${username}', '**${password}', '${fullname}', '${username}@example.com')"
46+
echo "User '${username}' created"
47+
fi
48+
EXISTING_PRINCIPAL=$(run_sql "SELECT principal_id FROM principal p JOIN usr u ON p.user_no = u.user_no WHERE u.username='${username}'")
49+
if [ -n "$EXISTING_PRINCIPAL" ]; then
50+
echo "Principal for '${username}' already exists, skipping"
51+
else
52+
run_sql "INSERT INTO principal (type_id, user_no, displayname) SELECT 1, user_no, fullname FROM usr WHERE username='${username}'"
53+
echo "Principal for '${username}' created"
54+
fi
55+
}
4756

48-
echo "Creating principal entry..."
49-
EXISTING_PRINCIPAL=$(run_sql "SELECT principal_id FROM principal p JOIN usr u ON p.user_no = u.user_no WHERE u.username='${TEST_USER}'")
50-
if [ -n "$EXISTING_PRINCIPAL" ]; then
51-
echo "Principal already exists, skipping"
52-
else
53-
run_sql "INSERT INTO principal (type_id, user_no, displayname) SELECT 1, user_no, fullname FROM usr WHERE username='${TEST_USER}'"
54-
echo "Principal created"
55-
fi
57+
echo ""
58+
echo "Creating test users..."
59+
create_user "${TEST_USER}" "${TEST_PASSWORD}" "Test User"
60+
create_user "user1" "testpass1" "User One"
61+
create_user "user2" "testpass2" "User Two"
62+
create_user "user3" "testpass3" "User Three"
5663

5764
echo ""
5865
echo "Verifying CalDAV access..."
@@ -79,4 +86,5 @@ echo ""
7986
echo "Credentials:"
8087
echo " Admin: admin / testpass"
8188
echo " Test user: ${TEST_USER} / ${TEST_PASSWORD}"
89+
echo " Scheduling users: user1/testpass1, user2/testpass2, user3/testpass3"
8290
echo " CalDAV URL: http://localhost:8805/caldav.php/${TEST_USER}/"

tests/docker-test-servers/nextcloud/setup_nextcloud.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ echo ""
2727
echo "Disabling password policy for testing..."
2828
docker exec $CONTAINER_NAME php occ app:disable password_policy || true
2929

30-
echo "Creating test user..."
30+
echo "Creating test users..."
3131
# Create test user (ignore error if already exists)
3232
docker exec -e OC_PASS="$TEST_PASSWORD" $CONTAINER_NAME php occ user:add --password-from-env --display-name="Test User" $TEST_USER 2>/dev/null || echo "User may already exist"
33+
# Create scheduling test users
34+
for i in 1 2 3; do
35+
docker exec -e OC_PASS="testpass${i}" $CONTAINER_NAME php occ user:add --password-from-env --display-name="User ${i}" "user${i}" 2>/dev/null || echo "user${i} may already exist"
36+
done
3337

3438
echo "Enabling calendar app..."
3539
docker exec $CONTAINER_NAME php occ app:enable calendar || true
@@ -64,5 +68,6 @@ echo ""
6468
echo "Credentials:"
6569
echo " Admin: admin / admin"
6670
echo " Test user: $TEST_USER / $TEST_PASSWORD"
71+
echo " Scheduling users: user1/testpass1, user2/testpass2, user3/testpass3"
6772
echo " CalDAV URL: http://localhost:8801/remote.php/dav"
6873
echo ""

0 commit comments

Comments
 (0)