Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ jobs:
# Create test user
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"

# Create scheduling test users (user1-user3)
for i in 1 2 3; do
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"
done

# Enable calendar and contacts apps
docker exec ${{ job.services.nextcloud.id }} php occ app:enable calendar || true
docker exec ${{ job.services.nextcloud.id }} php occ app:enable contacts || true
Expand Down
62 changes: 39 additions & 23 deletions caldav/compatibility_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,24 @@ 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"],
},
"scheduling.inbox-delivery": {
"description": "Server delivers incoming scheduling REQUEST messages to the attendee's schedule-inbox (RFC6638 section 4.1). When unsupported, the server implements automatic scheduling: invitations are auto-processed and placed directly on the attendee's calendar without appearing in the inbox. Clients should check this feature to know whether to look for inbox items after sending an invite, or check the attendee calendar directly.",
"links": [
"https://datatracker.ietf.org/doc/html/rfc6638#section-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 +725,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 +846,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 +879,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 +900,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 @@ -1089,6 +1094,15 @@ def dotted_feature_set_list(self, compact=False):
'support': 'fragile',
'behaviour': 'Deleting a recently created calendar fails'},
# Cyrus may not properly reject wrong passwords in some configurations
# Cyrus implements automatic scheduling (RFC6638 section 3.2.3): for cross-user
# invites, the server both auto-processes the invite into the attendee's calendar
# AND delivers an iTIP notification copy to the attendee's schedule-inbox.
# Clients do not need to explicitly accept from the inbox (auto-accept is done),
# but inbox items do appear. This is "quirk" behaviour: both delivery modes happen.
"scheduling.inbox-delivery": {
"support": "quirk",
"behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar",
},
'old_flags': []
}

Expand Down Expand Up @@ -1224,9 +1238,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 +1280,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 +1415,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 +1461,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
64 changes: 57 additions & 7 deletions tests/caldav_test_servers.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ test-servers:
port: ${NEXTCLOUD_PORT:-8801}
username: ${NEXTCLOUD_USERNAME:-testuser}
password: ${NEXTCLOUD_PASSWORD:-testpass}
# setup_nextcloud.sh creates user1-user3 (passwords testpass1-3) for scheduling tests.

cyrus:
type: docker
Expand All @@ -60,6 +61,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 All @@ -84,6 +96,7 @@ test-servers:
port: ${DAVICAL_PORT:-8805}
username: ${DAVICAL_USERNAME:-testuser}
password: ${DAVICAL_PASSWORD:-testpass}
# setup_davical.sh creates user1-user3 (passwords testpass1-3) for scheduling tests.

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

stalwart:
type: docker
Expand Down Expand Up @@ -150,13 +164,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/")
Loading
Loading