From 7f9e77ae585df89c300a022dce78224929989ad0 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:34:59 +0200 Subject: [PATCH] Add vCard X-label tests --- CHANGELOG.md | 1 + tests/integration/data.rs | 17 ++++++++ tests/integration/validate.rs | 25 +++++++++++ tests/integration_baikal.rs | 80 +++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 752d9a1..e6e386f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. This projec ### Fixed - Self heal on `blobNotFound` errors when exporting data (#13). +- Mapping existing special mailbox fails after `alreadyExists` response (#17). ## [1.0.5] - 2026-06-27 diff --git a/tests/integration/data.rs b/tests/integration/data.rs index 7c6b441..d34a9f8 100644 --- a/tests/integration/data.rs +++ b/tests/integration/data.rs @@ -116,6 +116,7 @@ const VCARD_VARIANTS: &[fn(usize) -> String] = &[ vcard_v4_with_address, vcard_v4_nickname, vcard_v4_birthday, + vcard_v3_apple_item_labels, ]; fn synth_vcards(n: usize) -> Vec { @@ -240,6 +241,22 @@ fn vcard_v4_birthday(i: usize) -> String { ) } +fn vcard_v3_apple_item_labels(i: usize) -> String { + format!( + "BEGIN:VCARD\r\n\ + VERSION:3.0\r\n\ + UID:vandelay-card-ablabels-{i}\r\n\ + FN:Apple Labels {i}\r\n\ + N:Contact{i};AppleLabels;;;\r\n\ + EMAIL;TYPE=INTERNET:ablabels-{i}@vandelay.test\r\n\ + ITEM1.X-ABLABEL:Name1\r\n\ + ITEM2.X-ABLABEL:Name2\r\n\ + ITEM1.X-ABDATE:20171111\r\n\ + ITEM2.X-ABDATE:20111111\r\n\ + END:VCARD\r\n" + ) +} + const ICAL_VARIANTS: &[fn(usize) -> String] = &[ ical_simple_dt, ical_all_day, diff --git a/tests/integration/validate.rs b/tests/integration/validate.rs index f487470..1de4917 100644 --- a/tests/integration/validate.rs +++ b/tests/integration/validate.rs @@ -324,6 +324,22 @@ fn component_top_level_properties(text: &str, name: &str) -> Vec { out } +pub fn grouped_property_values(text: &str, name: &str) -> Vec { + let unfolded = unfold_text(text); + let mut out = Vec::new(); + for line in unfolded.lines() { + let l = line.trim_end_matches('\r'); + let Some(colon) = l.find(':') else { continue }; + let head = &l[..colon]; + let name_part = head.split(';').next().unwrap_or(head); + let bare = name_part.rsplit('.').next().unwrap_or(name_part); + if bare.eq_ignore_ascii_case(name) { + out.push(l[colon + 1..].to_owned()); + } + } + out +} + pub fn has_line_prefix(text: &str, prefix: &str) -> bool { let unfolded = unfold_text(text); unfolded.lines().any(|l| l.starts_with(prefix)) @@ -540,6 +556,15 @@ pub fn assert_contact_round_trip(conn: &Connection, source: &[u8], uid: &str, la ); } + for label_name in ["X-ABLABEL", "X-ABDATE"] { + for value in grouped_property_values(text, label_name) { + assert!( + json_contains_string(&data, &value), + "{label}: contact {uid} {label_name} {value:?} not preserved in stored JSContact vCard roundtrip block" + ); + } + } + if let Some(bday) = extract_property(text, "BDAY") { let iso = format_vcard_date_as_iso(&bday); let present_raw = json_contains_string(&data, &bday); diff --git a/tests/integration_baikal.rs b/tests/integration_baikal.rs index 042a4eb..063084e 100644 --- a/tests/integration_baikal.rs +++ b/tests/integration_baikal.rs @@ -356,6 +356,86 @@ fn baikal_starts_seeds_and_imports() { b.stop().expect("baikal stop"); } +#[test] +#[ignore = "requires Docker"] +fn baikal_carddav_preserves_apple_item_labels() { + let b = Baikal::start().expect("baikal start"); + let account = &b.accounts[0]; + let dav_root = b.dav_root(); + + let client = + integration::dav_client::DavSeed::new(dav_root.clone(), &account.username, &account.password); + let book = format!("/addressbooks/{}/ablabels/", account.username); + let mkbook = r#" + + + + AB Labels + +"#; + client.mkcol(&book, Some(mkbook)).expect("mkcol addressbook"); + + let uid = "apple-item-labels-1"; + let vcard = format!( + "BEGIN:VCARD\r\n\ + VERSION:3.0\r\n\ + UID:{uid}\r\n\ + FN:Apple Labels\r\n\ + ITEM1.X-ABLABEL:Name1\r\n\ + ITEM2.X-ABLABEL:Name2\r\n\ + ITEM1.X-ABDATE:20171111\r\n\ + ITEM2.X-ABDATE:20111111\r\n\ + END:VCARD\r\n" + ); + client + .put( + &format!("{book}apple.vcf"), + "text/vcard; charset=utf-8", + vcard.as_bytes(), + ) + .expect("put vcard"); + + let archive = tmp_archive("baikal-ablabels"); + let cfg = DavImportConfig { + kind: DavKindArg::Carddav, + url: format!("{dav_root}/addressbooks/{}/", account.username), + auth: DavAuth::Basic { + user: account.username.clone(), + password: account.password.clone(), + }, + allow_cleartext: true, + dav_connections: 2, + multiget_batch: 25, + allow_source_change: false, + }; + let summary = import_dav::run(common(&archive), cfg).expect("carddav import"); + assert!( + !summary.any_failed(), + "carddav import had failures: {summary:?}" + ); + + let conn = open_archive(&archive); + let data: String = conn + .query_row( + "SELECT data FROM contact_cards WHERE uid = ?1", + [uid], + |r| r.get(0), + ) + .expect("apple-labels contact missing from archive"); + drop(conn); + eprintln!("archived JSContact:\n{data}"); + + for needle in ["x-abdate", "x-ablabel", "20171111", "20111111", "Name1", "Name2"] { + assert!( + data.to_lowercase().contains(&needle.to_lowercase()), + "Baikal CardDAV import dropped {needle:?}; stored: {data}" + ); + } + + cleanup(&archive); + b.stop().expect("baikal stop"); +} + fn assert_event_collection_exact(conn: &Connection, seed: &AccountSeed) { let names = collection_names(conn, "calendars"); let expected: HashSet = seed