Add vCard X-label tests
This commit is contained in:
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. This projec
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Self heal on `blobNotFound` errors when exporting data (#13).
|
- Self heal on `blobNotFound` errors when exporting data (#13).
|
||||||
|
- Mapping existing special mailbox fails after `alreadyExists` response (#17).
|
||||||
|
|
||||||
## [1.0.5] - 2026-06-27
|
## [1.0.5] - 2026-06-27
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const VCARD_VARIANTS: &[fn(usize) -> String] = &[
|
|||||||
vcard_v4_with_address,
|
vcard_v4_with_address,
|
||||||
vcard_v4_nickname,
|
vcard_v4_nickname,
|
||||||
vcard_v4_birthday,
|
vcard_v4_birthday,
|
||||||
|
vcard_v3_apple_item_labels,
|
||||||
];
|
];
|
||||||
|
|
||||||
fn synth_vcards(n: usize) -> Vec<RawFixture> {
|
fn synth_vcards(n: usize) -> Vec<RawFixture> {
|
||||||
@@ -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] = &[
|
const ICAL_VARIANTS: &[fn(usize) -> String] = &[
|
||||||
ical_simple_dt,
|
ical_simple_dt,
|
||||||
ical_all_day,
|
ical_all_day,
|
||||||
|
|||||||
@@ -324,6 +324,22 @@ fn component_top_level_properties(text: &str, name: &str) -> Vec<String> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn grouped_property_values(text: &str, name: &str) -> Vec<String> {
|
||||||
|
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 {
|
pub fn has_line_prefix(text: &str, prefix: &str) -> bool {
|
||||||
let unfolded = unfold_text(text);
|
let unfolded = unfold_text(text);
|
||||||
unfolded.lines().any(|l| l.starts_with(prefix))
|
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") {
|
if let Some(bday) = extract_property(text, "BDAY") {
|
||||||
let iso = format_vcard_date_as_iso(&bday);
|
let iso = format_vcard_date_as_iso(&bday);
|
||||||
let present_raw = json_contains_string(&data, &bday);
|
let present_raw = json_contains_string(&data, &bday);
|
||||||
|
|||||||
@@ -356,6 +356,86 @@ fn baikal_starts_seeds_and_imports() {
|
|||||||
b.stop().expect("baikal stop");
|
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#"<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<d:mkcol xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav">
|
||||||
|
<d:set><d:prop>
|
||||||
|
<d:resourcetype><d:collection/><c:addressbook/></d:resourcetype>
|
||||||
|
<d:displayname>AB Labels</d:displayname>
|
||||||
|
</d:prop></d:set>
|
||||||
|
</d:mkcol>"#;
|
||||||
|
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) {
|
fn assert_event_collection_exact(conn: &Connection, seed: &AccountSeed) {
|
||||||
let names = collection_names(conn, "calendars");
|
let names = collection_names(conn, "calendars");
|
||||||
let expected: HashSet<String> = seed
|
let expected: HashSet<String> = seed
|
||||||
|
|||||||
Reference in New Issue
Block a user