From 0cbde7f57be7311b755e72b8cca778fc8d847db1 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 20 Mar 2026 20:38:10 +0100 Subject: [PATCH 1/7] test: enable RFC6638 scheduling tests via multi-user docker setups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix config_loader._load_config_file to preserve top-level keys like rfc6638_users when unwrapping the test-servers dict — previously they were silently dropped, so TestScheduling was always skipped. - Fix registry.load_from_config to skip non-dict entries (e.g. the rfc6638_users list) instead of raising ValueError. - Add two new tests in test_config_loader.py covering these fixes. - Add user1/testpass1, user2/testpass2, user3/testpass3 to the Baikal pre-seeded SQLite DB (create_baikal_db.py + regenerated db.sqlite), alongside the existing testuser account. - Add user1-user3 to init-sogo-users.sql for SOGo. - Add CI step that writes tests/caldav_test_servers.yaml with rfc6638_users pointing to Cyrus user1-user3 (password 'x'), which are pre-created by the ghcr.io/cyrusimap/cyrus-docker-test-server image and are expected to support calendar-auto-schedule. - Update example config files with server-specific rfc6638_users snippets for Cyrus, Baikal and SOGo. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/tests.yaml | 20 ++++++++ tests/caldav_test_servers.yaml.example | 43 +++++++++++++--- .../baikal/Specific/db/db.sqlite | Bin 110592 -> 110592 bytes .../baikal/create_baikal_db.py | 48 +++++++++++++++++- .../sogo/init-sogo-users.sql | 13 +++++ tests/test_servers.yaml.example | 31 ++++++++--- tests/test_servers/config_loader.py | 4 ++ tests/test_servers/registry.py | 6 +-- tests/test_servers/test_config_loader.py | 36 +++++++++++++ 9 files changed, 182 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 48555ffc..2be349cd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -276,6 +276,26 @@ jobs: echo "✗ Error: Bedework CalDAV access failed" exit 1 fi + - name: Configure RFC6638 scheduling test users + run: | + # Write test config with rfc6638_users for scheduling tests. + # Cyrus pre-creates user1-user5 (password 'x') with scheduling support. + # Baikal users user1-user3 are in the pre-seeded db.sqlite. + # Two separate config sections for each server; CI uses Cyrus. + cat > tests/caldav_test_servers.yaml << 'EOF' + # RFC6638 scheduling test users - written by CI + # Cyrus pre-creates user1-user5 with password 'x' and supports scheduling + rfc6638_users: + - url: http://localhost:8802/dav/calendars/user/user1 + username: user1 + password: x + - url: http://localhost:8802/dav/calendars/user/user2 + username: user2 + password: x + - url: http://localhost:8802/dav/calendars/user/user3 + username: user3 + password: x + EOF - run: tox -e py env: NEXTCLOUD_URL: http://localhost:8801 diff --git a/tests/caldav_test_servers.yaml.example b/tests/caldav_test_servers.yaml.example index dabef575..504152c0 100644 --- a/tests/caldav_test_servers.yaml.example +++ b/tests/caldav_test_servers.yaml.example @@ -150,13 +150,42 @@ test-servers: # RFC6638 scheduling test users (optional) # ========================================================================= # -# For testing calendar scheduling (meeting invites, etc.), define -# multiple users that can send invites to each other: - +# For testing calendar scheduling (meeting invites, etc.), define at least +# three users on the same CalDAV server that can send invites to each other. +# This section lives at the TOP LEVEL (not under test-servers). +# +# Cyrus (pre-creates user1-user5 with password 'x'): +# rfc6638_users: +# - url: http://localhost:8802/dav/calendars/user/user1 +# username: user1 +# password: x +# - url: http://localhost:8802/dav/calendars/user/user2 +# username: user2 +# password: x +# - url: http://localhost:8802/dav/calendars/user/user3 +# username: user3 +# password: x +# +# Baikal (user1-user3 are in the pre-seeded db.sqlite, passwords testpass1-3): +# rfc6638_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): # rfc6638_users: -# - url: https://caldav.example.com/dav/user1/ +# - url: http://localhost:8803/SOGo/dav/user1 # username: user1 -# password: pass1 -# - url: https://caldav.example.com/dav/user2/ +# password: testpass1 +# - url: http://localhost:8803/SOGo/dav/user2 # username: user2 -# password: pass2 +# password: testpass2 +# - url: http://localhost:8803/SOGo/dav/user3 +# username: user3 +# password: testpass3 diff --git a/tests/docker-test-servers/baikal/Specific/db/db.sqlite b/tests/docker-test-servers/baikal/Specific/db/db.sqlite index f8af389afd02d0cf54f841286baee06013064d65..781d544497ea8b4d97c163716b98227b321aa18a 100644 GIT binary patch delta 1157 zcmaKrO-LI-6vtn~0!d=**=s0g;M{~&k5StjttxoIWHqPfFJM?mN(ijKZD6G zLXcvnN7JL@Q+?^FiIIuE^xcUE_s56Ey71YOe5O=B$VFO7+X;eASb%}HlS=8ht#^xxD}Ks(5S-!4d4h7g!VREyJB4k<{i|iR*5v4ZH59TI3K> z^zu%m^UW#|PaGon3xD7@9B#Bn_uxu^NTQv@Jur3;Stdk5$|sFzYedsTPzQo7cn)LG zQ7K{QHc1Oo`}@6ZaJO}(8j(W1t{F42AIEAU`_HkMDf@A>#!;6GxMpl+D8vC8 z1wY{{tcy0igW*oBHqC_iI2rbFQaOm-{(ROX@<`l1iC|aEdjTdZr6DNB#EOURxdrir z#vD_(6NY9QG#lqU9?zQ8PG#b$WRe*%vGR44IjJnC7By|%$m))nG`L2YmEc;ISzJ$A zG4Tu3QEKT6%Q%*8CG8aBPEt1$lxI>IO-`6AyYD3?65B&!aK})EJu$lPa81D> X$<2$LS`43BRKA3JlsUUFS0nxbx@}NP delta 228 zcmW;Ev1-C#6o%pRn`0nY_$U@jP)WhP{Ip{ipUO}4TmUL}K zvp5uLG6>q)L2&Wl_&$g4j52eSnO;?uC$-07nQmVXL3<8UAt5B;<;^$3#Cd26YzP~H zUleTR2OWFbWzmQbiH7i_;gfVu$fZ~C^KnCZOV~>Pfv?%R_m|Gqbk3}Bq@eqTiRTuX zEwVl}>krmi7BL}~#TRC=qgnzx!cJffqhg#tEVSZhIi!SC4nLTs@9IuqPuL6m 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.""" @@ -358,10 +399,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) @@ -380,4 +425,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/") diff --git a/tests/docker-test-servers/sogo/init-sogo-users.sql b/tests/docker-test-servers/sogo/init-sogo-users.sql index 227dfeb7..dc8f68c1 100644 --- a/tests/docker-test-servers/sogo/init-sogo-users.sql +++ b/tests/docker-test-servers/sogo/init-sogo-users.sql @@ -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'); diff --git a/tests/test_servers.yaml.example b/tests/test_servers.yaml.example index f28008f7..c07af076 100644 --- a/tests/test_servers.yaml.example +++ b/tests/test_servers.yaml.example @@ -109,13 +109,30 @@ test-servers: # RFC6638 scheduling test users (optional) # ========================================================================= # -# For testing calendar scheduling (meeting invites, etc.), define -# multiple users that can send invites to each other: - +# For testing calendar scheduling (meeting invites, etc.), define at least +# three users on the same CalDAV server that can send invites to each other. +# This section lives at the TOP LEVEL (not under test-servers). +# +# Cyrus (pre-creates user1-user5 with password 'x'): +# rfc6638_users: +# - url: http://localhost:8802/dav/calendars/user/user1 +# username: user1 +# password: x +# - url: http://localhost:8802/dav/calendars/user/user2 +# username: user2 +# password: x +# - url: http://localhost:8802/dav/calendars/user/user3 +# username: user3 +# password: x +# +# Baikal (user1-user3 are in the pre-seeded db.sqlite, passwords testpass1-3): # rfc6638_users: -# - url: https://caldav.example.com/dav/user1/ +# - url: http://localhost:8800/dav.php/ # username: user1 -# password: pass1 -# - url: https://caldav.example.com/dav/user2/ +# password: testpass1 +# - url: http://localhost:8800/dav.php/ # username: user2 -# password: pass2 +# password: testpass2 +# - url: http://localhost:8800/dav.php/ +# username: user3 +# password: testpass3 diff --git a/tests/test_servers/config_loader.py b/tests/test_servers/config_loader.py index 982e8158..c7467e46 100644 --- a/tests/test_servers/config_loader.py +++ b/tests/test_servers/config_loader.py @@ -91,7 +91,11 @@ def _load_config_file(path: str) -> dict[str, dict[str, Any]]: # Unwrap the "test-servers" key if present (the example YAML # uses this as a top-level namespace). Also support configs # where server dicts are at the top level directly. + # Preserve top-level non-server keys (e.g. rfc6638_users) by merging + # them back into the servers dict after unwrapping. if "test-servers" in cfg: + top_level_extras = {k: v for k, v in cfg.items() if k != "test-servers"} cfg = cfg["test-servers"] + cfg.update(top_level_extras) return cfg diff --git a/tests/test_servers/registry.py b/tests/test_servers/registry.py index ed794a23..1dc7e481 100644 --- a/tests/test_servers/registry.py +++ b/tests/test_servers/registry.py @@ -183,10 +183,8 @@ def load_from_config(self, config: dict) -> None: for name, server_config in config.items(): if not isinstance(server_config, dict): - raise ValueError( - f"Server '{name}': configuration must be a dict, " - f"got {type(server_config).__name__}" - ) + # Skip non-server entries (e.g. rfc6638_users is a list, not a server) + continue if not server_config.get("enabled", True): continue diff --git a/tests/test_servers/test_config_loader.py b/tests/test_servers/test_config_loader.py index 1e51ed09..97230ba9 100644 --- a/tests/test_servers/test_config_loader.py +++ b/tests/test_servers/test_config_loader.py @@ -88,3 +88,39 @@ def test_empty_yaml_raises_error(self, tmp_path: Path) -> None: with pytest.raises(ConfigParseError) as exc_info: load_test_server_config(str(config_file)) assert "could not be parsed" in str(exc_info.value) + + def test_rfc6638_users_preserved_alongside_test_servers(self, tmp_path: Path) -> None: + """rfc6638_users at top level is preserved when test-servers is unwrapped.""" + config_file = tmp_path / "test_servers.yaml" + config_file.write_text(""" +test-servers: + radicale: + type: embedded + enabled: true + +rfc6638_users: + - url: http://localhost:8802/dav/calendars/user/user1 + username: user1 + password: x + - url: http://localhost:8802/dav/calendars/user/user2 + username: user2 + password: x +""") + cfg = load_test_server_config(str(config_file)) + assert "radicale" in cfg + assert "rfc6638_users" in cfg + assert len(cfg["rfc6638_users"]) == 2 + assert cfg["rfc6638_users"][0]["username"] == "user1" + + def test_rfc6638_users_only_config(self, tmp_path: Path) -> None: + """Config with only rfc6638_users (no test-servers) works correctly.""" + config_file = tmp_path / "test_servers.yaml" + config_file.write_text(""" +rfc6638_users: + - url: http://localhost:8802/dav/calendars/user/user1 + username: user1 + password: x +""") + cfg = load_test_server_config(str(config_file)) + assert "rfc6638_users" in cfg + assert cfg["rfc6638_users"][0]["url"] == "http://localhost:8802/dav/calendars/user/user1" From e90f9db81efbd0bd190ac953859b84791788e1d1 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 20 Mar 2026 23:01:12 +0100 Subject: [PATCH 2/7] feat: add 'scheduling' feature to FeatureSet.FEATURES (RFC6638) Adds a new 'scheduling' feature entry to the compatibility hints, corresponding to the legacy 'no_scheduling' flag, to enable structured tracking of RFC6638 CalDAV Scheduling support. Co-Authored-By: Claude Sonnet 4.6 --- caldav/compatibility_hints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index a37e483d..030ea789 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -261,6 +261,10 @@ 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 the schedule-outbox and/or schedule-inbox in DAV headers. Corresponds to the legacy 'no_scheduling' flag.", + "links": ["https://datatracker.ietf.org/doc/html/rfc6638"], + }, '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", From 10eaee1f1b2abca1f24d5c9b3229f58652aa63bf Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Fri, 20 Mar 2026 23:50:52 +0100 Subject: [PATCH 3/7] feat: migrate no_scheduling legacy flags to new 'scheduling' feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old_flags no_scheduling / no_scheduling_mailbox / no_scheduling_calendar_user_address_set with the new 'scheduling' feature entry in the compatibility matrix, and adapt the test code to use skip_unless_support("scheduling") / is_supported("scheduling"). Servers with no_scheduling → "scheduling": {"support": "unsupported"}: xandikos_v0_2_12, xandikos, radicale, robur, posteo, purelymail Servers with partial scheduling (mailbox/address-set missing) → noted with a TODO for future sub-features: gmx Co-Authored-By: Claude Sonnet 4.6 --- caldav/compatibility_hints.py | 34 +++++++++++----------------------- tests/test_caldav.py | 13 +++++-------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 030ea789..940b9a84 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -711,15 +711,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)""", @@ -841,14 +832,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 @@ -876,13 +865,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", ] } @@ -899,11 +886,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', @@ -1228,9 +1215,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', ], @@ -1270,8 +1257,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 ] } @@ -1405,10 +1392,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', @@ -1452,11 +1438,13 @@ 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). + ## TODO: add scheduling.mailbox and scheduling.calendar-user-address-set sub-features + "scheduling": {"support": "full"}, "old_flags": [ - "no_scheduling_mailbox", #"text_search_is_case_insensitive", "no_search_openended", - "no_scheduling_calendar_user_address_set", "vtodo-cannot-be-uncompleted", ] } diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 07359431..47577e9e 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -1093,20 +1093,17 @@ 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") + ## TODO: add scheduling.calendar-user-address-set sub-feature check 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") + ## TODO: add scheduling.mailbox sub-feature check inbox = self.principal.schedule_inbox() outbox = self.principal.schedule_outbox() From 11abd7706e4df2e1ac97d7478ef89b49aad39120 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 21 Mar 2026 01:19:43 +0100 Subject: [PATCH 4/7] feat: add scheduling.mailbox and scheduling.calendar-user-address-set sub-features - Add scheduling.mailbox and scheduling.calendar-user-address-set to FeatureSet.FEATURES (RFC6638 sections 2.2-2.4.1) - Set both to unsupported in the gmx server entry (scheduling is advertised but sub-features are non-functional) - Adapt testSchedulingInfo/testSchedulingMailboxes to use the new sub-feature flags instead of the top-level scheduling flag - Refactor TestScheduling into TestSchedulingBase with a configurable _users list; legacy TestScheduling created from rfc6638_users - Generate TestSchedulingForServer* classes for servers with scheduling_users configured in their server entry - Pass scheduling_users through in TestServer.get_server_params() - Document and enable scheduling_users for Cyrus in the example config (Cyrus pre-creates user1-user5 with password 'x') Co-Authored-By: Claude Sonnet 4.6 --- caldav/compatibility_hints.py | 13 ++++++-- tests/caldav_test_servers.yaml.example | 11 +++++++ tests/test_caldav.py | 44 ++++++++++++++++++-------- tests/test_servers/base.py | 3 ++ 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 940b9a84..0ff07720 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -262,9 +262,17 @@ class FeatureSet: "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 the schedule-outbox and/or schedule-inbox in DAV headers. Corresponds to the legacy 'no_scheduling' flag.", + "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", @@ -1440,8 +1448,9 @@ def dotted_feature_set_list(self, compact=False): #'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). - ## TODO: add scheduling.mailbox and scheduling.calendar-user-address-set sub-features "scheduling": {"support": "full"}, + "scheduling.mailbox": {"support": "unsupported"}, + "scheduling.calendar-user-address-set": {"support": "unsupported"}, "old_flags": [ #"text_search_is_case_insensitive", "no_search_openended", diff --git a/tests/caldav_test_servers.yaml.example b/tests/caldav_test_servers.yaml.example index 504152c0..85d62e9a 100644 --- a/tests/caldav_test_servers.yaml.example +++ b/tests/caldav_test_servers.yaml.example @@ -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 diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 47577e9e..9bb1f306 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -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 @@ -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 @@ -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. @@ -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) @@ -1096,14 +1104,12 @@ def testSupport(self): assert self.caldav.check_scheduling_support() == self.is_supported("scheduling") def testSchedulingInfo(self): - self.skip_unless_support("scheduling") - ## TODO: add scheduling.calendar-user-address-set sub-feature check + 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_unless_support("scheduling") - ## TODO: add scheduling.mailbox sub-feature check + self.skip_unless_support("scheduling.mailbox") inbox = self.principal.schedule_inbox() outbox = self.principal.schedule_outbox() @@ -3676,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"]}, + ) diff --git a/tests/test_servers/base.py b/tests/test_servers/base.py index 5c1edbde..4a61327b 100644 --- a/tests/test_servers/base.py +++ b/tests/test_servers/base.py @@ -201,6 +201,9 @@ def get_server_params(self) -> dict[str, Any]: # Pass through SSL verification setting if configured if "ssl_verify_cert" in self.config: params["ssl_verify_cert"] = self.config["ssl_verify_cert"] + # Pass through scheduling_users if configured (for TestScheduling generation) + if "scheduling_users" in self.config: + params["scheduling_users"] = self.config["scheduling_users"] # Check if server is already running (either started by us or externally) already_running = self._started or self.is_accessible() if already_running: From 8ac202dcfb3823b567b6ca7b297a6609858ddf7a Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 21 Mar 2026 01:38:07 +0100 Subject: [PATCH 5/7] test: fix RFC6638 scheduling test collection and server config merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename TestSchedulingBase → _TestSchedulingBase so pytest no longer collects the base class directly (it has _users=[] → noisy skip). The docstring already said "no Test prefix" but the code disagreed. - Teach load_from_config to merge scheduling_users into an already- registered server (case-insensitive name match) instead of registering a duplicate. This allows a caldav_test_servers.yaml entry like: cyrus: scheduling_users: [...] to augment the auto-discovered CyrusTestServer without doubling up. - If a config entry contains only test-metadata keys (scheduling_users, type, enabled) and no matching server is already registered, skip it silently — it is intended to augment a running server, not create a new one (avoids spurious test runs when e.g. Cyrus isn't started locally but the CI-oriented config file is present). - Update CI step to write per-server scheduling_users for both Cyrus and Baikal (instead of the previous global rfc6638_users approach). - Update caldav_test_servers.yaml.example with Baikal and SOGo scheduling_users examples and explain when to use the legacy rfc6638_users fallback. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/tests.yaml | 38 ++++++++++------ tests/caldav_test_servers.yaml.example | 63 ++++++++++++++------------ tests/test_caldav.py | 6 +-- tests/test_servers/registry.py | 26 +++++++++++ 4 files changed, 88 insertions(+), 45 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2be349cd..39fbd42a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -278,23 +278,33 @@ jobs: fi - name: Configure RFC6638 scheduling test users run: | - # Write test config with rfc6638_users for scheduling tests. + # Write test config with scheduling_users for each server that supports it. # Cyrus pre-creates user1-user5 (password 'x') with scheduling support. # Baikal users user1-user3 are in the pre-seeded db.sqlite. - # Two separate config sections for each server; CI uses Cyrus. + # These configs are merged into auto-discovered servers by the registry. cat > tests/caldav_test_servers.yaml << 'EOF' - # RFC6638 scheduling test users - written by CI - # Cyrus pre-creates user1-user5 with password 'x' and supports scheduling - rfc6638_users: - - url: http://localhost:8802/dav/calendars/user/user1 - username: user1 - password: x - - url: http://localhost:8802/dav/calendars/user/user2 - username: user2 - password: x - - url: http://localhost:8802/dav/calendars/user/user3 - username: user3 - password: x + cyrus: + scheduling_users: + - url: http://localhost:8802/dav/calendars/user/user1 + username: user1 + password: x + - url: http://localhost:8802/dav/calendars/user/user2 + username: user2 + password: x + - url: http://localhost:8802/dav/calendars/user/user3 + username: user3 + password: x + 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 EOF - run: tox -e py env: diff --git a/tests/caldav_test_servers.yaml.example b/tests/caldav_test_servers.yaml.example index 85d62e9a..70a68cf5 100644 --- a/tests/caldav_test_servers.yaml.example +++ b/tests/caldav_test_servers.yaml.example @@ -161,11 +161,42 @@ test-servers: # RFC6638 scheduling test users (optional) # ========================================================================= # -# For testing calendar scheduling (meeting invites, etc.), define at least -# three users on the same CalDAV server that can send invites to each other. -# This section lives at the TOP LEVEL (not under test-servers). +# 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 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. # -# Cyrus (pre-creates user1-user5 with password 'x'): # rfc6638_users: # - url: http://localhost:8802/dav/calendars/user/user1 # username: user1 @@ -176,27 +207,3 @@ test-servers: # - url: http://localhost:8802/dav/calendars/user/user3 # username: user3 # password: x -# -# Baikal (user1-user3 are in the pre-seeded db.sqlite, passwords testpass1-3): -# rfc6638_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): -# rfc6638_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 diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 9bb1f306..94e8516e 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -670,7 +670,7 @@ def test_multi_server_meta_section(self) -> None: assert len(clients) == 2 -class TestSchedulingBase: +class _TestSchedulingBase: """ Base class for RFC6638 scheduling tests. Not collected directly by pytest (no ``Test`` prefix); concrete subclasses supply ``_users``. @@ -793,7 +793,7 @@ def testInviteAndRespond(self): if rfc6638_users: TestScheduling = type( "TestScheduling", - (TestSchedulingBase,), + (_TestSchedulingBase,), {"_users": rfc6638_users}, ) @@ -3689,6 +3689,6 @@ def testWithEnvironment(self): _sched_classname = "TestSchedulingForServer" + _servername vars()[_sched_classname] = type( _sched_classname, - (TestSchedulingBase,), + (_TestSchedulingBase,), {"_users": _caldav_server["scheduling_users"]}, ) diff --git a/tests/test_servers/registry.py b/tests/test_servers/registry.py index 1dc7e481..b36cd8fb 100644 --- a/tests/test_servers/registry.py +++ b/tests/test_servers/registry.py @@ -189,6 +189,32 @@ def load_from_config(self, config: dict) -> None: if not server_config.get("enabled", True): continue + # Keys that only carry test-specific metadata, not connection config. + _TEST_ONLY_KEYS = frozenset({"scheduling_users"}) + _META_KEYS = frozenset({"type", "enabled", "name"}) + + # If an auto-discovered server with the same name (case-insensitive) is + # already registered, merge extra test-only fields (like scheduling_users) + # into it instead of registering a duplicate. + existing_key = next( + (k for k in self._servers if k.lower() == name.lower()), None + ) + if existing_key is not None: + if "scheduling_users" in server_config: + self._servers[existing_key].config["scheduling_users"] = server_config[ + "scheduling_users" + ] + continue + + # If the config only contains test-only metadata (no real connection + # params) and there is no existing server to merge into, skip: the + # entry is intended only to augment a running server, not to register + # a new one (e.g. a config written for CI that references Cyrus + # scheduling users but Cyrus isn't started locally). + non_connection_keys = {k for k in server_config if k not in _META_KEYS | _TEST_ONLY_KEYS} + if not non_connection_keys: + continue + server_type = server_config.get("type", name) server_class = get_server_class(server_type) From ff0c679ed677d2d00ac6b45f4b2d712aefb967fe Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 21 Mar 2026 01:45:32 +0100 Subject: [PATCH 6/7] test: pre-populate scheduling_users in Cyrus and Baikal server classes Rather than requiring a caldav_test_servers.yaml file to be written by CI (or manually by the user), bake the scheduling_users directly into CyrusTestServer and BaikalTestServer.__init__. - Cyrus: user1-user3 / password 'x' (pre-created by the docker image) - Baikal: user1-user3 / testpass1-3 (pre-seeded in the committed db.sqlite) The per-server TestSchedulingForServer{Cyrus,Baikal} classes are now generated automatically whenever those servers are running, with no additional configuration required. Also drop the now-unnecessary CI step that wrote caldav_test_servers.yaml. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/tests.yaml | 30 ------------------------------ tests/test_servers/docker.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 39fbd42a..48555ffc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -276,36 +276,6 @@ jobs: echo "✗ Error: Bedework CalDAV access failed" exit 1 fi - - name: Configure RFC6638 scheduling test users - run: | - # Write test config with scheduling_users for each server that supports it. - # Cyrus pre-creates user1-user5 (password 'x') with scheduling support. - # Baikal users user1-user3 are in the pre-seeded db.sqlite. - # These configs are merged into auto-discovered servers by the registry. - cat > tests/caldav_test_servers.yaml << 'EOF' - cyrus: - scheduling_users: - - url: http://localhost:8802/dav/calendars/user/user1 - username: user1 - password: x - - url: http://localhost:8802/dav/calendars/user/user2 - username: user2 - password: x - - url: http://localhost:8802/dav/calendars/user/user3 - username: user3 - password: x - 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 - EOF - run: tox -e py env: NEXTCLOUD_URL: http://localhost:8801 diff --git a/tests/test_servers/docker.py b/tests/test_servers/docker.py index 7c221cd8..18c4c50d 100644 --- a/tests/test_servers/docker.py +++ b/tests/test_servers/docker.py @@ -30,13 +30,22 @@ class BaikalTestServer(DockerTestServer): def __init__(self, config: dict[str, Any] | None = None) -> None: config = config or {} - config.setdefault("host", os.environ.get("BAIKAL_HOST", "localhost")) - config.setdefault("port", int(os.environ.get("BAIKAL_PORT", "8800"))) + host = config.get("host") or os.environ.get("BAIKAL_HOST", "localhost") + port = int(config.get("port") or os.environ.get("BAIKAL_PORT", "8800")) + config.setdefault("host", host) + config.setdefault("port", port) config.setdefault("username", os.environ.get("BAIKAL_USERNAME", "testuser")) config.setdefault("password", os.environ.get("BAIKAL_PASSWORD", "testpass")) # Set up Baikal-specific compatibility hints if "features" not in config: config["features"] = compatibility_hints.baikal.copy() + # user1-user3 are pre-seeded in the committed db.sqlite for scheduling tests + if "scheduling_users" not in config: + base = f"http://{host}:{port}/dav.php" + config["scheduling_users"] = [ + {"url": base, "username": f"user{i}", "password": f"testpass{i}"} + for i in range(1, 4) + ] super().__init__(config) def _default_port(self) -> int: @@ -88,14 +97,18 @@ class CyrusTestServer(DockerTestServer): Cyrus IMAP server with CalDAV support in Docker. Cyrus is a mail server that also supports CalDAV/CardDAV. + The ghcr.io/cyrusimap/cyrus-docker-test-server image pre-creates + user1-user5 (password 'x') with CalDAV scheduling support. """ name = "Cyrus" def __init__(self, config: dict[str, Any] | None = None) -> None: config = config or {} - config.setdefault("host", os.environ.get("CYRUS_HOST", "localhost")) - config.setdefault("port", int(os.environ.get("CYRUS_PORT", "8802"))) + host = config.get("host") or os.environ.get("CYRUS_HOST", "localhost") + port = int(config.get("port") or os.environ.get("CYRUS_PORT", "8802")) + config.setdefault("host", host) + config.setdefault("port", port) config.setdefault("username", os.environ.get("CYRUS_USERNAME", "user1")) config.setdefault( "password", os.environ.get("CYRUS_PASSWORD", "any-password-seems-to-work") @@ -103,6 +116,16 @@ def __init__(self, config: dict[str, Any] | None = None) -> None: # Set up Cyrus-specific compatibility hints if "features" not in config: config["features"] = compatibility_hints.cyrus.copy() + # The docker image pre-creates user1-user5 with password 'x' + if "scheduling_users" not in config: + config["scheduling_users"] = [ + { + "url": f"http://{host}:{port}/dav/calendars/user/user{i}", + "username": f"user{i}", + "password": "x", + } + for i in range(1, 4) + ] super().__init__(config) def _default_port(self) -> int: From e257d0a1ea05a52d5ba5bf040523c67e9eb15a72 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 21 Mar 2026 01:46:30 +0100 Subject: [PATCH 7/7] style: fix --- tests/docker-test-servers/baikal/create_baikal_db.py | 4 +++- tests/test_servers/registry.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/docker-test-servers/baikal/create_baikal_db.py b/tests/docker-test-servers/baikal/create_baikal_db.py index 7205c59b..bdf83dd1 100755 --- a/tests/docker-test-servers/baikal/create_baikal_db.py +++ b/tests/docker-test-servers/baikal/create_baikal_db.py @@ -241,7 +241,9 @@ def add_baikal_user(db_path: Path, username: str, password: str) -> None: 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 REPLACE INTO users (username, digesta1) VALUES (?, ?)", (username, ha1) + ) cursor.execute( "INSERT OR IGNORE INTO principals (uri, email, displayname) VALUES (?, ?, ?)", diff --git a/tests/test_servers/registry.py b/tests/test_servers/registry.py index b36cd8fb..384b340f 100644 --- a/tests/test_servers/registry.py +++ b/tests/test_servers/registry.py @@ -196,9 +196,7 @@ def load_from_config(self, config: dict) -> None: # If an auto-discovered server with the same name (case-insensitive) is # already registered, merge extra test-only fields (like scheduling_users) # into it instead of registering a duplicate. - existing_key = next( - (k for k in self._servers if k.lower() == name.lower()), None - ) + existing_key = next((k for k in self._servers if k.lower() == name.lower()), None) if existing_key is not None: if "scheduling_users" in server_config: self._servers[existing_key].config["scheduling_users"] = server_config[ @@ -211,7 +209,9 @@ def load_from_config(self, config: dict) -> None: # entry is intended only to augment a running server, not to register # a new one (e.g. a config written for CI that references Cyrus # scheduling users but Cyrus isn't started locally). - non_connection_keys = {k for k in server_config if k not in _META_KEYS | _TEST_ONLY_KEYS} + non_connection_keys = { + k for k in server_config if k not in _META_KEYS | _TEST_ONLY_KEYS + } if not non_connection_keys: continue