Before this change that was impossible. There was a real, wiremock-tested CardDavClient in nuncio-contacts, but nothing in the product could reach it: nuncio_core::AccountConfig had no way to express a CardDAV address book, so Contacts/Sync in production always fell through to its honest-error branch and
only worked when a test double was injected through the overrides seam. The
capability existed in the library and was unreachable in the product.
How
Wired the same way the CalDAV vertical already is, rather than via a parallel
mechanism:
Account model — AccountProtocol::CardDav and Transport::CardDav(DavTransport). Added is_caldav()/is_carddav() so
callers select a specific DAV flavour; is_dav() remains the "not a mail
account" predicate (which keeps the mail scheduler's exclusion correct).
Calendar sync now filters on is_caldav(), so a CardDAV account can never be
picked up as a calendar.
Storage — reuses the existing collection_url column, distinguished by the protocol discriminator. No schema change; nothing added to migrate().
Proto — additive only: ACCOUNT_PROTOCOL_CARDDAV = 4 and a carddav
case in the AccountConfig.transport oneof at field 19. No renumbering, no
type changes. Descriptor golden regenerated per the test's own instructions.
Engine — contacts_sync::sync_carddav_account builds a real CardDavClient from the persisted collection URL, with the credential read
just-in-time from the OS keyring and wrapped in Zeroizing. Never from
plaintext in source or SQLite.
Honest failure — Contacts/Sync returns a typed NotConfigured
(FAILED_PRECONDITION) when no CardDAV account matches, including when the
named account exists but speaks another protocol. Previously an untyped Status::internal. Never a fabricated synced_count.
CLI — --protocol carddav, with --collection-url required for either
DAV flavour.
Tests
New crates/nunciod/tests/carddav_production_e2e_test.rs drives the full daemon
with default overrides (no injected backend), covering:
a successful per-account sync the way production drives it — account added over
the real Accounts/AddAccount RPC, real CardDavClient issuing a genuine
CardDAV REPORT against wiremock, contacts persisted and immediately visible
to ListContacts;
an account that is configured but is not CardDAV → FAILED_PRECONDITION;
an account with no CardDAV configuration at all → FAILED_PRECONDITION;
credential resolution through the mocked vault (SecretManager::mock() / MockKeyring);
unauthenticated access still rejected.
Plus a store round-trip test proving a CardDAV account reloads as CardDAV and not
as the CalDAV variant it shares physical columns with. No live network anywhere.
Gate
cargo fmt --all -- --check, cargo check-all, and cargo test-all all green
(43 suites, 0 failures). Ran RUST_TEST_THREADS=2 cargo test-all three times
with zero FAILED lines each.
## What a user can now do that they could not before
Add a CardDAV account and actually sync their contacts:
```
nuncio account add --protocol carddav \
--email me@example.com \
--collection-url https://dav.example.com/addressbooks/me/default/
nuncio contact sync --account acct-me-at-example-com
```
Before this change that was impossible. There was a real, wiremock-tested
`CardDavClient` in `nuncio-contacts`, but nothing in the product could reach it:
`nuncio_core::AccountConfig` had no way to express a CardDAV address book, so
`Contacts/Sync` in production always fell through to its honest-error branch and
only worked when a test double was injected through the overrides seam. The
capability existed in the library and was unreachable in the product.
## How
Wired the same way the CalDAV vertical already is, rather than via a parallel
mechanism:
- **Account model** — `AccountProtocol::CardDav` and
`Transport::CardDav(DavTransport)`. Added `is_caldav()`/`is_carddav()` so
callers select a specific DAV flavour; `is_dav()` remains the "not a mail
account" predicate (which keeps the mail scheduler's exclusion correct).
Calendar sync now filters on `is_caldav()`, so a CardDAV account can never be
picked up as a calendar.
- **Storage** — reuses the existing `collection_url` column, distinguished by the
`protocol` discriminator. **No schema change**; nothing added to `migrate()`.
- **Proto** — additive only: `ACCOUNT_PROTOCOL_CARDDAV = 4` and a `carddav`
case in the `AccountConfig.transport` oneof at field 19. No renumbering, no
type changes. Descriptor golden regenerated per the test's own instructions.
- **Engine** — `contacts_sync::sync_carddav_account` builds a real
`CardDavClient` from the persisted collection URL, with the credential read
just-in-time from the OS keyring and wrapped in `Zeroizing`. Never from
plaintext in source or SQLite.
- **Honest failure** — `Contacts/Sync` returns a typed `NotConfigured`
(`FAILED_PRECONDITION`) when no CardDAV account matches, including when the
named account exists but speaks another protocol. Previously an untyped
`Status::internal`. Never a fabricated `synced_count`.
- **CLI** — `--protocol carddav`, with `--collection-url` required for either
DAV flavour.
## Tests
New `crates/nunciod/tests/carddav_production_e2e_test.rs` drives the full daemon
with **default overrides** (no injected backend), covering:
- a successful per-account sync the way production drives it — account added over
the real `Accounts/AddAccount` RPC, real `CardDavClient` issuing a genuine
CardDAV `REPORT` against wiremock, contacts persisted and immediately visible
to `ListContacts`;
- an account that is configured but is **not** CardDAV → `FAILED_PRECONDITION`;
- an account with no CardDAV configuration at all → `FAILED_PRECONDITION`;
- credential resolution through the mocked vault (`SecretManager::mock()` /
`MockKeyring`);
- unauthenticated access still rejected.
Plus a store round-trip test proving a CardDAV account reloads as CardDAV and not
as the CalDAV variant it shares physical columns with. No live network anywhere.
## Gate
`cargo fmt --all -- --check`, `cargo check-all`, and `cargo test-all` all green
(43 suites, 0 failures). Ran `RUST_TEST_THREADS=2 cargo test-all` three times
with zero `FAILED` lines each.
Closes #255
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What a user can now do that they could not before
Add a CardDAV account and actually sync their contacts:
Before this change that was impossible. There was a real, wiremock-tested
CardDavClientinnuncio-contacts, but nothing in the product could reach it:nuncio_core::AccountConfighad no way to express a CardDAV address book, soContacts/Syncin production always fell through to its honest-error branch andonly worked when a test double was injected through the overrides seam. The
capability existed in the library and was unreachable in the product.
How
Wired the same way the CalDAV vertical already is, rather than via a parallel
mechanism:
AccountProtocol::CardDavandTransport::CardDav(DavTransport). Addedis_caldav()/is_carddav()socallers select a specific DAV flavour;
is_dav()remains the "not a mailaccount" predicate (which keeps the mail scheduler's exclusion correct).
Calendar sync now filters on
is_caldav(), so a CardDAV account can never bepicked up as a calendar.
collection_urlcolumn, distinguished by theprotocoldiscriminator. No schema change; nothing added tomigrate().ACCOUNT_PROTOCOL_CARDDAV = 4and acarddavcase in the
AccountConfig.transportoneof at field 19. No renumbering, notype changes. Descriptor golden regenerated per the test's own instructions.
contacts_sync::sync_carddav_accountbuilds a realCardDavClientfrom the persisted collection URL, with the credential readjust-in-time from the OS keyring and wrapped in
Zeroizing. Never fromplaintext in source or SQLite.
Contacts/Syncreturns a typedNotConfigured(
FAILED_PRECONDITION) when no CardDAV account matches, including when thenamed account exists but speaks another protocol. Previously an untyped
Status::internal. Never a fabricatedsynced_count.--protocol carddav, with--collection-urlrequired for eitherDAV flavour.
Tests
New
crates/nunciod/tests/carddav_production_e2e_test.rsdrives the full daemonwith default overrides (no injected backend), covering:
the real
Accounts/AddAccountRPC, realCardDavClientissuing a genuineCardDAV
REPORTagainst wiremock, contacts persisted and immediately visibleto
ListContacts;FAILED_PRECONDITION;FAILED_PRECONDITION;SecretManager::mock()/MockKeyring);Plus a store round-trip test proving a CardDAV account reloads as CardDAV and not
as the CalDAV variant it shares physical columns with. No live network anywhere.
Gate
cargo fmt --all -- --check,cargo check-all, andcargo test-allall green(43 suites, 0 failures). Ran
RUST_TEST_THREADS=2 cargo test-allthree timeswith zero
FAILEDlines each.Closes #255