Skip to content

Commit 0cbde7f

Browse files
tobixenclaude
andcommitted
test: enable RFC6638 scheduling tests via multi-user docker setups
- 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 <noreply@anthropic.com>
1 parent f1f15c7 commit 0cbde7f

9 files changed

Lines changed: 182 additions & 19 deletions

File tree

.github/workflows/tests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,26 @@ jobs:
276276
echo "✗ Error: Bedework CalDAV access failed"
277277
exit 1
278278
fi
279+
- name: Configure RFC6638 scheduling test users
280+
run: |
281+
# Write test config with rfc6638_users for scheduling tests.
282+
# Cyrus pre-creates user1-user5 (password 'x') with scheduling support.
283+
# Baikal users user1-user3 are in the pre-seeded db.sqlite.
284+
# Two separate config sections for each server; CI uses Cyrus.
285+
cat > tests/caldav_test_servers.yaml << 'EOF'
286+
# RFC6638 scheduling test users - written by CI
287+
# Cyrus pre-creates user1-user5 with password 'x' and supports scheduling
288+
rfc6638_users:
289+
- url: http://localhost:8802/dav/calendars/user/user1
290+
username: user1
291+
password: x
292+
- url: http://localhost:8802/dav/calendars/user/user2
293+
username: user2
294+
password: x
295+
- url: http://localhost:8802/dav/calendars/user/user3
296+
username: user3
297+
password: x
298+
EOF
279299
- run: tox -e py
280300
env:
281301
NEXTCLOUD_URL: http://localhost:8801

tests/caldav_test_servers.yaml.example

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,42 @@ test-servers:
150150
# RFC6638 scheduling test users (optional)
151151
# =========================================================================
152152
#
153-
# For testing calendar scheduling (meeting invites, etc.), define
154-
# multiple users that can send invites to each other:
155-
153+
# For testing calendar scheduling (meeting invites, etc.), define at least
154+
# three users on the same CalDAV server that can send invites to each other.
155+
# This section lives at the TOP LEVEL (not under test-servers).
156+
#
157+
# Cyrus (pre-creates user1-user5 with password 'x'):
158+
# rfc6638_users:
159+
# - url: http://localhost:8802/dav/calendars/user/user1
160+
# username: user1
161+
# password: x
162+
# - url: http://localhost:8802/dav/calendars/user/user2
163+
# username: user2
164+
# password: x
165+
# - url: http://localhost:8802/dav/calendars/user/user3
166+
# username: user3
167+
# password: x
168+
#
169+
# Baikal (user1-user3 are in the pre-seeded db.sqlite, passwords testpass1-3):
170+
# rfc6638_users:
171+
# - url: http://localhost:8800/dav.php/
172+
# username: user1
173+
# password: testpass1
174+
# - url: http://localhost:8800/dav.php/
175+
# username: user2
176+
# password: testpass2
177+
# - url: http://localhost:8800/dav.php/
178+
# username: user3
179+
# password: testpass3
180+
#
181+
# SOGo (user1-user3 from init-sogo-users.sql, passwords testpass1-3):
156182
# rfc6638_users:
157-
# - url: https://caldav.example.com/dav/user1/
183+
# - url: http://localhost:8803/SOGo/dav/user1
158184
# username: user1
159-
# password: pass1
160-
# - url: https://caldav.example.com/dav/user2/
185+
# password: testpass1
186+
# - url: http://localhost:8803/SOGo/dav/user2
161187
# username: user2
162-
# password: pass2
188+
# password: testpass2
189+
# - url: http://localhost:8803/SOGo/dav/user3
190+
# username: user3
191+
# password: testpass3
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/sogo/init-sogo-users.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,16 @@ CREATE TABLE IF NOT EXISTS sogo_users (
1414
INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
1515
VALUES ('testuser', 'testuser', MD5('testpass'), 'Test User', 'testuser@example.com')
1616
ON DUPLICATE KEY UPDATE c_password=MD5('testpass');
17+
18+
-- Additional users for RFC6638 scheduling tests (need at least 3 users)
19+
INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
20+
VALUES ('user1', 'user1', MD5('testpass1'), 'Test User 1', 'user1@example.com')
21+
ON DUPLICATE KEY UPDATE c_password=MD5('testpass1');
22+
23+
INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
24+
VALUES ('user2', 'user2', MD5('testpass2'), 'Test User 2', 'user2@example.com')
25+
ON DUPLICATE KEY UPDATE c_password=MD5('testpass2');
26+
27+
INSERT INTO sogo_users (c_uid, c_name, c_password, c_cn, mail)
28+
VALUES ('user3', 'user3', MD5('testpass3'), 'Test User 3', 'user3@example.com')
29+
ON DUPLICATE KEY UPDATE c_password=MD5('testpass3');

tests/test_servers.yaml.example

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,30 @@ test-servers:
109109
# RFC6638 scheduling test users (optional)
110110
# =========================================================================
111111
#
112-
# For testing calendar scheduling (meeting invites, etc.), define
113-
# multiple users that can send invites to each other:
114-
112+
# For testing calendar scheduling (meeting invites, etc.), define at least
113+
# three users on the same CalDAV server that can send invites to each other.
114+
# This section lives at the TOP LEVEL (not under test-servers).
115+
#
116+
# Cyrus (pre-creates user1-user5 with password 'x'):
117+
# rfc6638_users:
118+
# - url: http://localhost:8802/dav/calendars/user/user1
119+
# username: user1
120+
# password: x
121+
# - url: http://localhost:8802/dav/calendars/user/user2
122+
# username: user2
123+
# password: x
124+
# - url: http://localhost:8802/dav/calendars/user/user3
125+
# username: user3
126+
# password: x
127+
#
128+
# Baikal (user1-user3 are in the pre-seeded db.sqlite, passwords testpass1-3):
115129
# rfc6638_users:
116-
# - url: https://caldav.example.com/dav/user1/
130+
# - url: http://localhost:8800/dav.php/
117131
# username: user1
118-
# password: pass1
119-
# - url: https://caldav.example.com/dav/user2/
132+
# password: testpass1
133+
# - url: http://localhost:8800/dav.php/
120134
# username: user2
121-
# password: pass2
135+
# password: testpass2
136+
# - url: http://localhost:8800/dav.php/
137+
# username: user3
138+
# password: testpass3

tests/test_servers/config_loader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ def _load_config_file(path: str) -> dict[str, dict[str, Any]]:
9191
# Unwrap the "test-servers" key if present (the example YAML
9292
# uses this as a top-level namespace). Also support configs
9393
# where server dicts are at the top level directly.
94+
# Preserve top-level non-server keys (e.g. rfc6638_users) by merging
95+
# them back into the servers dict after unwrapping.
9496
if "test-servers" in cfg:
97+
top_level_extras = {k: v for k, v in cfg.items() if k != "test-servers"}
9598
cfg = cfg["test-servers"]
99+
cfg.update(top_level_extras)
96100

97101
return cfg

tests/test_servers/registry.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,8 @@ def load_from_config(self, config: dict) -> None:
183183

184184
for name, server_config in config.items():
185185
if not isinstance(server_config, dict):
186-
raise ValueError(
187-
f"Server '{name}': configuration must be a dict, "
188-
f"got {type(server_config).__name__}"
189-
)
186+
# Skip non-server entries (e.g. rfc6638_users is a list, not a server)
187+
continue
190188

191189
if not server_config.get("enabled", True):
192190
continue

tests/test_servers/test_config_loader.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,39 @@ def test_empty_yaml_raises_error(self, tmp_path: Path) -> None:
8888
with pytest.raises(ConfigParseError) as exc_info:
8989
load_test_server_config(str(config_file))
9090
assert "could not be parsed" in str(exc_info.value)
91+
92+
def test_rfc6638_users_preserved_alongside_test_servers(self, tmp_path: Path) -> None:
93+
"""rfc6638_users at top level is preserved when test-servers is unwrapped."""
94+
config_file = tmp_path / "test_servers.yaml"
95+
config_file.write_text("""
96+
test-servers:
97+
radicale:
98+
type: embedded
99+
enabled: true
100+
101+
rfc6638_users:
102+
- url: http://localhost:8802/dav/calendars/user/user1
103+
username: user1
104+
password: x
105+
- url: http://localhost:8802/dav/calendars/user/user2
106+
username: user2
107+
password: x
108+
""")
109+
cfg = load_test_server_config(str(config_file))
110+
assert "radicale" in cfg
111+
assert "rfc6638_users" in cfg
112+
assert len(cfg["rfc6638_users"]) == 2
113+
assert cfg["rfc6638_users"][0]["username"] == "user1"
114+
115+
def test_rfc6638_users_only_config(self, tmp_path: Path) -> None:
116+
"""Config with only rfc6638_users (no test-servers) works correctly."""
117+
config_file = tmp_path / "test_servers.yaml"
118+
config_file.write_text("""
119+
rfc6638_users:
120+
- url: http://localhost:8802/dav/calendars/user/user1
121+
username: user1
122+
password: x
123+
""")
124+
cfg = load_test_server_config(str(config_file))
125+
assert "rfc6638_users" in cfg
126+
assert cfg["rfc6638_users"][0]["url"] == "http://localhost:8802/dav/calendars/user/user1"

0 commit comments

Comments
 (0)