Skip to content

docs(config): document auth_type and other connection keys #1557

docs(config): document auth_type and other connection keys

docs(config): document auth_type and other connection keys #1557

Workflow file for this run

---
name: tests
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
tests:
name: ${{ matrix.python }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ${{ github.event_name == 'pull_request' && fromJSON('["3.10", "3.14"]') || fromJSON('["3.10", "3.11", "3.12", "3.13", "3.14"]') }}
services:
baikal:
image: ckulka/baikal:nginx
ports:
- 8800:80
options: >-
--health-cmd "curl -f http://localhost/ || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--health-start-period 30s
nextcloud:
image: nextcloud:latest
ports:
- 8801:80
env:
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: admin
options: >-
--health-cmd "curl -f http://localhost/status.php || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--health-start-period 60s
cyrus:
# Pinned to last known-good build (pre-April-2026 multi-stage rebuild
# that broke CalDAV startup). Unpin once upstream is fixed.
# Working digest confirmed in CI run 23748898521 (2026-03-30).
image: ghcr.io/cyrusimap/cyrus-docker-test-server@sha256:d639a9116691a7a1c875073486c419d60843e5ef8e32e65c5ef56283874dbf2c
ports:
- 8802:8080
- 8001:8001
env:
DEFAULTDOMAIN: example.com
SERVERNAME: cyrus-test
options: >-
--health-cmd "curl -s http://localhost:8080/ || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--health-start-period 60s
sogo-db:
image: mariadb:11
env:
MYSQL_DATABASE: sogo
MYSQL_USER: sogo
MYSQL_PASSWORD: sogo
MYSQL_ROOT_PASSWORD: sogo
options: >-
--health-cmd "healthcheck.sh --connect --innodb_initialized"
--health-interval 5s
--health-timeout 5s
--health-retries 20
--health-start-period 10s
--network-alias db
sogo:
image: japoch/sogo:latest
ports:
- 8803:80
env:
sogo_user: testuser
sogo_pass: testpass
sogo_name: Test User
sogo_fqhn: example.com
bedework3:
image: ioggstream/bedework:latest
ports:
- 8804:8080
options: >-
--health-cmd "curl -f http://localhost:8080/bedework/ || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 15
--health-start-period 120s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- name: Configure Baikal with pre-seeded database
run: |
# Copy pre-configured database and config to Baikal container
docker cp tests/docker-test-servers/baikal/Specific/. ${{ job.services.baikal.id }}:/var/www/baikal/Specific/
docker cp tests/docker-test-servers/baikal/config/. ${{ job.services.baikal.id }}:/var/www/baikal/config/
# Fix permissions for SQLite
docker exec ${{ job.services.baikal.id }} chown -R nginx:nginx /var/www/baikal/Specific /var/www/baikal/config
docker exec ${{ job.services.baikal.id }} chmod -R 770 /var/www/baikal/Specific
# Restart to pick up configuration
docker restart ${{ job.services.baikal.id }}
- name: Wait for Baikal to be ready
run: |
sleep 5
if timeout 60 bash -c 'until curl -f http://localhost:8800/ 2>/dev/null; do echo "Waiting..."; sleep 2; done'; then
echo "✓ Baikal is ready!"
else
echo "✗ Error: Baikal did not become ready within 60 seconds"
exit 1
fi
- name: Configure Nextcloud
run: |
# Wait for Nextcloud web server to be up
echo "Waiting for Nextcloud web server..."
if timeout 60 bash -c 'until curl -f http://localhost:8801/status.php 2>/dev/null; do echo -n "."; sleep 2; done'; then
echo ""
echo "✓ Web server is up"
else
echo ""
echo "✗ Error: Nextcloud web server did not become ready within 60 seconds"
exit 1
fi
# Install Nextcloud if not already installed
if ! docker exec ${{ job.services.nextcloud.id }} php occ status 2>/dev/null | grep -q "installed: true"; then
echo "Installing Nextcloud..."
docker exec ${{ job.services.nextcloud.id }} php occ maintenance:install \
--database=sqlite \
--admin-user=admin \
--admin-pass=admin
echo "✓ Nextcloud installed"
else
echo "✓ Nextcloud is already installed"
fi
# Disable password policy
docker exec ${{ job.services.nextcloud.id }} php occ app:disable password_policy || true
# 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
# Set email addresses for scheduling users (required for calendar-user-address-set)
for i in 1 2 3; do
docker exec ${{ job.services.nextcloud.id }} php occ user:setting "user${i}" settings email "user${i}@localhost" || true
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
# Disable rate limiting and bruteforce protection
docker exec ${{ job.services.nextcloud.id }} php occ config:system:set ratelimit.enabled --value=false --type=boolean || true
docker exec ${{ job.services.nextcloud.id }} php occ app:disable bruteforcesettings || true
docker exec ${{ job.services.nextcloud.id }} php occ config:system:set auth.bruteforce.protection.enabled --value=false --type=boolean || true
# Disable CalDAV trashbin: setting calendarRetentionObligation=0 makes
# CalDavBackend hard-delete objects instead of soft-deleting them.
# Without this, deleted events remain in oc_calendarobjects with a deleted_at
# timestamp, causing UNIQUE constraint violations when tests recreate the same UID.
docker exec ${{ job.services.nextcloud.id }} php occ config:app:set dav calendarRetentionObligation --value=0 || true
docker exec ${{ job.services.nextcloud.id }} php occ dav:retention:clean-up || true
# Configure CalDAV rate limits
docker exec ${{ job.services.nextcloud.id }} php occ config:app:set dav rateLimitCalendarCreation --value=99999 || true
docker exec ${{ job.services.nextcloud.id }} php occ config:app:set dav maximumCalendarsSubscriptions --value=-1 || true
# Add IP whitelist for rate limiting
docker exec ${{ job.services.nextcloud.id }} php occ config:system:set ratelimit.whitelist.0 --value='172.17.0.0/16' || true
docker exec ${{ job.services.nextcloud.id }} php occ config:system:set ratelimit.whitelist.1 --value='127.0.0.1' || true
# Clear rate limit cache. The SQLite file is named after the `dbname`
# config value, which defaults to `owncloud` — look it up rather than
# guessing, and check it exists first: PDO creates a missing SQLite
# file, so a wrong path silently yields "no such table" for every
# DELETE below.
DB_NAME=$(docker exec ${{ job.services.nextcloud.id }} php occ config:system:get dbname 2>/dev/null | tr -d '\r\n')
DB_PATH="/var/www/html/data/${DB_NAME:-owncloud}.db"
if docker exec ${{ job.services.nextcloud.id }} test -f "$DB_PATH"; then
docker exec ${{ job.services.nextcloud.id }} php -r "
\$db = new PDO('sqlite:$DB_PATH');
foreach (['oc_ratelimit_entries', 'oc_bruteforce_attempts'] as \$table) {
try {
\$db->exec(\"DELETE FROM \$table\");
} catch (PDOException \$e) {
fwrite(STDERR, \"skipping \$table: \" . \$e->getMessage() . \"\n\");
}
}
echo \"Cleared rate limit and bruteforce caches\n\";
" || true
else
echo "No database found at $DB_PATH — skipping cache cleanup"
fi
echo "Nextcloud is configured!"
- name: Configure Cyrus
run: |
# Copy imapd.conf with virtdomains: off (required for iTIP scheduling delivery).
# The default virtdomains: userid setting causes caladdress_lookup() to preserve
# the full email form (user2@example.com) while mailbox ACLs use the short form
# (user2), resulting in 403 errors when delivering iTIP invites.
sed 's/{{DEFAULTDOMAIN}}/example.com/g; s/{{SERVERNAME}}/cyrus-test/g' \
tests/docker-test-servers/cyrus/imapd.conf > /tmp/imapd_expanded.conf
docker cp /tmp/imapd_expanded.conf ${{ job.services.cyrus.id }}:/srv/cyrus-docker-test-server.git/imapd.conf
docker restart ${{ job.services.cyrus.id }}
echo "✓ Cyrus reconfigured with virtdomains: off"
- name: Wait for Cyrus to be ready
run: |
echo "Waiting for Cyrus server..."
# Cyrus takes a bit longer to initialize
sleep 10
if timeout 60 bash -c 'until curl -s http://localhost:8802/ 2>/dev/null | grep -q .; do echo -n "."; sleep 2; done'; then
echo ""
echo "✓ Cyrus HTTP server is ready"
else
echo ""
echo "✗ Error: Cyrus HTTP server did not become ready within 60 seconds"
exit 1
fi
# Verify CalDAV access with pre-created user
# Cyrus CalDAV can take significant time to initialize, especially in CI
echo "Waiting for CalDAV access..."
if timeout 120 bash -c 'until curl -s -X PROPFIND -H "Depth: 0" -u user1:x http://localhost:8802/dav/calendars/user/user1/ 2>/dev/null | grep -qi "multistatus\|collection" ; do echo -n "."; sleep 2; done'; then
echo ""
echo "✓ Cyrus CalDAV with pre-created user ready"
else
echo ""
echo "✗ Error: Cyrus CalDAV did not become accessible within 120 seconds"
echo "Attempting to debug..."
echo ""
echo "=== Container logs ==="
docker logs ${{ job.services.cyrus.id }} 2>&1 | tail -100 || true
echo ""
echo "=== Listening ports inside container ==="
docker exec ${{ job.services.cyrus.id }} netstat -tlnp 2>&1 || true
echo ""
echo "=== Running processes ==="
docker exec ${{ job.services.cyrus.id }} ps aux 2>&1 || true
echo ""
echo "=== HTTP response from host ==="
curl -v http://localhost:8802/ 2>&1 || true
echo ""
echo "=== CalDAV PROPFIND response from host ==="
curl -v -X PROPFIND -H "Depth: 0" -u user1:x http://localhost:8802/dav/calendars/user/user1/ 2>&1 || true
echo ""
echo "=== HTTP response from inside container ==="
docker exec ${{ job.services.cyrus.id }} curl -v http://localhost:8080/ 2>&1 || true
exit 1
fi
- name: Configure SOGo
run: |
echo "Configuring SOGo..."
# Wait for database to be ready
echo "Waiting for database..."
sleep 5
# Initialize database with test user
docker cp tests/docker-test-servers/sogo/init-sogo-users.sql ${{ job.services.sogo-db.id }}:/tmp/init-sogo-users.sql
docker exec -i ${{ job.services.sogo-db.id }} mariadb -usogo -psogo sogo < tests/docker-test-servers/sogo/init-sogo-users.sql
echo "✓ Database initialized with test user"
# Copy SOGo configuration (database is now reachable via network alias 'db')
docker cp tests/docker-test-servers/sogo/sogo.conf ${{ job.services.sogo.id }}:/etc/sogo/sogo.conf
echo "✓ SOGo configuration copied"
# Restart SOGo to pick up configuration
docker restart ${{ job.services.sogo.id }}
# Wait for SOGo to be ready
echo "Waiting for SOGo to start..."
if timeout 60 bash -c 'until curl -f http://localhost:8803/SOGo/ 2>/dev/null; do echo -n "."; sleep 2; done'; then
echo ""
echo "✓ SOGo is ready"
else
echo ""
echo "✗ Error: SOGo did not become ready within 60 seconds"
exit 1
fi
# Verify CalDAV access
echo "Verifying CalDAV access..."
if curl -s -X PROPFIND -H "Depth: 0" -u testuser:testpass http://localhost:8803/SOGo/dav/testuser/Calendar/ 2>/dev/null | grep -qi "multistatus"; then
echo "✓ SOGo CalDAV access verified"
else
echo "✗ Error: SOGo CalDAV access failed"
exit 1
fi
- name: Configure Bedework 3.10.3
run: |
echo "Waiting for Bedework..."
# Bedework/JBoss takes longer to start up
if timeout 180 bash -c 'until curl -f http://localhost:8804/bedework/ 2>/dev/null; do echo -n "."; sleep 5; done'; then
echo ""
echo "✓ Bedework web interface is ready"
else
echo ""
echo "✗ Error: Bedework did not become ready within 180 seconds"
exit 1
fi
# Verify CalDAV access with default user (vbede:bedework)
echo "Verifying CalDAV access..."
if curl -s -X PROPFIND -H "Depth: 0" -u vbede:bedework http://localhost:8804/ucaldav/user/vbede/ 2>/dev/null | grep -qi "multistatus"; then
echo "✓ Bedework CalDAV access verified"
else
echo "✗ Error: Bedework CalDAV access failed"
exit 1
fi
# Runs the full test suite (sync + async) against all servers above.
# Async tests run with niquests (the default install).
- run: tox -e py
env:
NEXTCLOUD_URL: http://localhost:8801
BAIKAL_URL: http://localhost:8800
CYRUS_URL: http://localhost:8802
SOGO_URL: http://localhost:8803
BEDEWORK3_URL: http://localhost:8804
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- run: tox -e docs
style:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ hashFiles('.pre-commit-config.yaml') }}
- run: pip install tox
- run: tox -e style
deptry:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('tox.ini') }}
- run: pip install tox
- run: tox -e deptry
# The three async-* jobs below exist to test the async backend *selection* logic,
# not to re-run the async integration tests. The async HTTP library
# (_USE_HTTPX / _USE_NIQUESTS / _HTTPX_FLAVOUR) is chosen at import time based on
# what is installed, so the only way to exercise each fallback path is to
# manipulate the installed packages before running pytest. The main `tests`
# job above already covers async tests with niquests (the default install);
# these jobs cover the httpxyz and plain-httpx fallback paths and explicitly
# assert the right _USE_* flag before running tests.
async-niquests:
# Explicit labelled check that the niquests path (default) works in isolation.
# Complements the main `tests` job; uninstalls httpx/httpxyz to ensure niquests
# is selected, then runs unit tests only (no server required).
name: async (niquests)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies without httpx/httpxyz
run: |
pip install --editable .[test]
pip uninstall -y httpx httpxyz
- name: Verify niquests is used
run: |
python -c "
from caldav.async_davclient import _USE_HTTPX, _USE_NIQUESTS
assert not _USE_HTTPX, 'httpx/httpxyz should not be available'
assert _USE_NIQUESTS, 'niquests should be used'
print('✓ Using niquests for async HTTP')
"
- name: Run async tests with niquests
run: pytest tests/test_async_davclient.py -v
async-httpxyz:
# Uninstalls niquests and installs httpxyz to force the httpxyz fallback path.
# Runs unit tests only (no server required).
name: async (httpxyz fallback)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies with httpxyz, without niquests
run: |
pip install --editable .[test]
pip uninstall -y niquests
pip install httpxyz
- name: Verify httpxyz is used
run: |
python -c "
from caldav.async_davclient import _HTTPX_FLAVOUR, _USE_HTTPX, _USE_NIQUESTS
assert not _USE_NIQUESTS, 'niquests should not be available'
assert _HTTPX_FLAVOUR == 'httpxyz', f'expected httpxyz, got {_HTTPX_FLAVOUR}'
assert _USE_HTTPX, '_USE_HTTPX should be set when httpxyz is used'
print('✓ Using httpxyz for async HTTP')
"
- name: Run async tests with httpxyz
run: pytest tests/test_async_davclient.py -v
async-httpx2:
# Uninstalls niquests and httpxyz and installs httpx2 - Pydantic's
# continuation of httpx, a separate package rather than a new httpx release.
# Unlike httpxyz it does not register itself in sys.modules as "httpx", so
# this job is what catches code that reaches for httpx by name.
# Runs unit tests plus the Xandikos async integration tests (embedded, no
# service container needed), so the backend is exercised over real HTTP.
name: async (httpx2 fallback)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies with httpx2, without niquests or httpxyz
run: |
pip install --editable .[test]
pip uninstall -y niquests httpxyz httpx
pip install httpx2
- name: Verify httpx2 is used
run: |
python -c "
from caldav.async_davclient import _HTTPX_FLAVOUR, _USE_HTTPX, _USE_NIQUESTS
assert not _USE_NIQUESTS, 'niquests should not be available'
assert _USE_HTTPX, '_USE_HTTPX should be set when httpx2 is used'
assert _HTTPX_FLAVOUR == 'httpx2', f'expected httpx2, got {_HTTPX_FLAVOUR}'
print('✓ Using httpx2 for async HTTP')
"
- name: Run async tests with httpx2
# Xandikos runs embedded, so no service container is needed; the
# selection is deliberately narrow rather than "everything not baikal",
# since the runner has docker and would otherwise auto-discover the
# docker test servers.
run: pytest tests/test_async_davclient.py tests/test_async_integration.py -v -k "xandikos or Xandikos"
async-httpx:
# Uninstalls both niquests and httpxyz to force the plain-httpx fallback path.
# Runs unit tests + a real integration test against Baikal (the lightest server)
# to verify end-to-end async HTTP with this backend.
name: async (httpx fallback)
runs-on: ubuntu-latest
services:
baikal:
image: ckulka/baikal:nginx
ports:
- 8800:80
options: >-
--health-cmd "curl -f http://localhost/ || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--health-start-period 30s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies with httpx only
run: |
pip install --editable .[test]
pip uninstall -y niquests httpxyz
- name: Configure Baikal with pre-seeded database
run: |
docker cp tests/docker-test-servers/baikal/Specific/. ${{ job.services.baikal.id }}:/var/www/baikal/Specific/
docker cp tests/docker-test-servers/baikal/config/. ${{ job.services.baikal.id }}:/var/www/baikal/config/
docker exec ${{ job.services.baikal.id }} chown -R nginx:nginx /var/www/baikal/Specific /var/www/baikal/config
docker exec ${{ job.services.baikal.id }} chmod -R 770 /var/www/baikal/Specific
docker restart ${{ job.services.baikal.id }}
- name: Wait for Baikal to be ready
run: |
if timeout 60 bash -c 'until curl -f http://localhost:8800/ 2>/dev/null; do echo "Waiting..."; sleep 2; done'; then
echo "✓ Baikal is ready!"
else
echo "✗ Error: Baikal did not become ready within 60 seconds"
exit 1
fi
- name: Verify httpx is used
run: |
python -c "
from caldav.async_davclient import _HTTPX_FLAVOUR, _USE_HTTPX, _USE_NIQUESTS
assert not _USE_NIQUESTS, 'niquests should not be available'
assert _HTTPX_FLAVOUR == 'httpx', f'expected httpx, got {_HTTPX_FLAVOUR}'
assert _USE_HTTPX, 'httpx should be used'
print('✓ Using httpx for async HTTP')
"
- name: Run async tests with httpx
run: pytest tests/test_async_davclient.py tests/test_async_integration.py -v -k baikal
env:
BAIKAL_URL: http://localhost:8800
sync-requests:
# Test that sync code works with requests when niquests is not installed
name: sync (requests fallback)
runs-on: ubuntu-latest
services:
baikal:
image: ckulka/baikal:nginx
ports:
- 8800:80
options: >-
--health-cmd "curl -f http://localhost/ || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
--health-start-period 30s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies with requests instead of niquests
run: |
pip install --editable .[test]
pip uninstall -y niquests
pip install requests
- name: Configure Baikal with pre-seeded database
run: |
docker cp tests/docker-test-servers/baikal/Specific/. ${{ job.services.baikal.id }}:/var/www/baikal/Specific/
docker cp tests/docker-test-servers/baikal/config/. ${{ job.services.baikal.id }}:/var/www/baikal/config/
docker exec ${{ job.services.baikal.id }} chown -R nginx:nginx /var/www/baikal/Specific /var/www/baikal/config
docker exec ${{ job.services.baikal.id }} chmod -R 770 /var/www/baikal/Specific
docker restart ${{ job.services.baikal.id }}
- name: Wait for Baikal to be ready
run: |
if timeout 60 bash -c 'until curl -f http://localhost:8800/ 2>/dev/null; do echo "Waiting..."; sleep 2; done'; then
echo "✓ Baikal is ready!"
else
echo "✗ Error: Baikal did not become ready within 60 seconds"
exit 1
fi
- name: Verify requests is used
run: |
python -c "
from caldav.lib.http_sync import USE_REQUESTS, USE_NIQUESTS
assert USE_REQUESTS, 'requests should be available'
assert not USE_NIQUESTS, 'niquests should not be available'
print('✓ Using requests for sync HTTP')
"
- name: Run sync tests with requests
run: pytest tests/test_caldav.py -v -k "Baikal or Radicale" --ignore=tests/test_async_integration.py
env:
BAIKAL_URL: http://localhost:8800