Initial commit
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
mod integration;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use integration::Account;
|
||||
use integration::baikal::{AccountSeed, Baikal};
|
||||
use integration::validate::{
|
||||
all_contact_uids, all_event_uids, assert_contact_round_trip, assert_event_round_trip,
|
||||
calendar_event_count, cleanup, collection_id_by_name, collection_names, common,
|
||||
contact_card_count, count, open_archive, tmp_archive,
|
||||
};
|
||||
|
||||
use rusqlite::Connection;
|
||||
use serde_json::Value;
|
||||
use vandelay::error::Error;
|
||||
use vandelay::sync::import_dav;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
|
||||
fn dav_config(kind: DavKindArg, account: &Account, dav_root: &str) -> DavImportConfig {
|
||||
let path = match kind {
|
||||
DavKindArg::Caldav => format!("{dav_root}/calendars/{}/", account.username),
|
||||
DavKindArg::Carddav => format!("{dav_root}/addressbooks/{}/", account.username),
|
||||
DavKindArg::Webdav => format!("{dav_root}/"),
|
||||
};
|
||||
DavImportConfig {
|
||||
kind,
|
||||
url: path,
|
||||
auth: DavAuth::Basic {
|
||||
user: account.username.clone(),
|
||||
password: account.password.clone(),
|
||||
},
|
||||
allow_cleartext: true,
|
||||
dav_connections: 2,
|
||||
multiget_batch: 25,
|
||||
allow_source_change: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn dav_config_via_discovery(
|
||||
kind: DavKindArg,
|
||||
account: &Account,
|
||||
dav_root: &str,
|
||||
) -> DavImportConfig {
|
||||
DavImportConfig {
|
||||
kind,
|
||||
url: format!("{dav_root}/"),
|
||||
auth: DavAuth::Basic {
|
||||
user: account.username.clone(),
|
||||
password: account.password.clone(),
|
||||
},
|
||||
allow_cleartext: true,
|
||||
dav_connections: 2,
|
||||
multiget_batch: 25,
|
||||
allow_source_change: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires Docker"]
|
||||
fn baikal_starts_seeds_and_imports() {
|
||||
let b = Baikal::start().expect("baikal start");
|
||||
let per_account = b.seed_all().expect("baikal seed");
|
||||
assert_eq!(
|
||||
per_account.len(),
|
||||
b.accounts.len(),
|
||||
"seed should return stats for every account"
|
||||
);
|
||||
b.verify_seed(&per_account).expect("baikal verify");
|
||||
|
||||
let dav_root = b.dav_root();
|
||||
|
||||
for seed in &per_account {
|
||||
let account = b
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|a| a.username == seed.username)
|
||||
.expect("account lookup");
|
||||
assert!(
|
||||
!seed.calendars.is_empty(),
|
||||
"{}: expected calendars seeded",
|
||||
seed.username
|
||||
);
|
||||
assert!(
|
||||
!seed.address_books.is_empty(),
|
||||
"{}: expected address books seeded",
|
||||
seed.username
|
||||
);
|
||||
assert!(
|
||||
seed.total_events() > 0,
|
||||
"{}: expected events seeded",
|
||||
seed.username
|
||||
);
|
||||
assert!(
|
||||
seed.total_contacts() > 0,
|
||||
"{}: expected contacts seeded",
|
||||
seed.username
|
||||
);
|
||||
|
||||
let cal_archive = tmp_archive(&format!("baikal-cal-{}", seed.username));
|
||||
let summary = import_dav::run(
|
||||
common(&cal_archive),
|
||||
dav_config(DavKindArg::Caldav, account, &dav_root),
|
||||
)
|
||||
.expect("caldav import");
|
||||
assert!(
|
||||
!summary.any_failed(),
|
||||
"{}: first caldav import had failures: {summary:?}",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&cal_archive);
|
||||
assert_event_collection_exact(&conn, seed);
|
||||
assert_event_uids_round_trip(&conn, seed);
|
||||
assert_event_data_well_formed(&conn);
|
||||
for plan in &seed.calendars {
|
||||
for item in &plan.items {
|
||||
assert_event_round_trip(
|
||||
&conn,
|
||||
&item.source,
|
||||
&item.uid,
|
||||
&format!("{}/{}", seed.username, plan.display_name),
|
||||
);
|
||||
}
|
||||
}
|
||||
let cal_count = count(&conn, "calendars") as usize;
|
||||
let ev_count = count(&conn, "calendar_events") as usize;
|
||||
let blob_count = count(&conn, "blobs") as usize;
|
||||
drop(conn);
|
||||
|
||||
let summary2 = import_dav::run(
|
||||
common(&cal_archive),
|
||||
dav_config(DavKindArg::Caldav, account, &dav_root),
|
||||
)
|
||||
.expect("idempotent caldav re-import");
|
||||
assert!(
|
||||
!summary2.any_failed(),
|
||||
"{}: idempotent caldav re-import had failures",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&cal_archive);
|
||||
assert_eq!(
|
||||
count(&conn, "calendars") as usize,
|
||||
cal_count,
|
||||
"{}: idempotent re-import changed calendar count",
|
||||
seed.username
|
||||
);
|
||||
assert_eq!(
|
||||
count(&conn, "calendar_events") as usize,
|
||||
ev_count,
|
||||
"{}: idempotent re-import changed event count",
|
||||
seed.username
|
||||
);
|
||||
assert_eq!(
|
||||
count(&conn, "blobs") as usize,
|
||||
blob_count,
|
||||
"{}: idempotent re-import changed blob count",
|
||||
seed.username
|
||||
);
|
||||
drop(conn);
|
||||
|
||||
let card_archive = tmp_archive(&format!("baikal-card-{}", seed.username));
|
||||
let summary = import_dav::run(
|
||||
common(&card_archive),
|
||||
dav_config(DavKindArg::Carddav, account, &dav_root),
|
||||
)
|
||||
.expect("carddav import");
|
||||
assert!(
|
||||
!summary.any_failed(),
|
||||
"{}: first carddav import had failures: {summary:?}",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&card_archive);
|
||||
assert_contact_collection_exact(&conn, seed);
|
||||
assert_contact_uids_round_trip(&conn, seed);
|
||||
assert_contact_data_well_formed(&conn);
|
||||
for plan in &seed.address_books {
|
||||
for item in &plan.items {
|
||||
assert_contact_round_trip(
|
||||
&conn,
|
||||
&item.source,
|
||||
&item.uid,
|
||||
&format!("{}/{}", seed.username, plan.display_name),
|
||||
);
|
||||
}
|
||||
}
|
||||
let book_count = count(&conn, "address_books") as usize;
|
||||
let card_count = count(&conn, "contact_cards") as usize;
|
||||
drop(conn);
|
||||
|
||||
let summary2 = import_dav::run(
|
||||
common(&card_archive),
|
||||
dav_config(DavKindArg::Carddav, account, &dav_root),
|
||||
)
|
||||
.expect("idempotent carddav re-import");
|
||||
assert!(
|
||||
!summary2.any_failed(),
|
||||
"{}: idempotent carddav re-import had failures",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&card_archive);
|
||||
assert_eq!(
|
||||
count(&conn, "address_books") as usize,
|
||||
book_count,
|
||||
"{}: idempotent re-import changed address book count",
|
||||
seed.username
|
||||
);
|
||||
assert_eq!(
|
||||
count(&conn, "contact_cards") as usize,
|
||||
card_count,
|
||||
"{}: idempotent re-import changed contact count",
|
||||
seed.username
|
||||
);
|
||||
drop(conn);
|
||||
|
||||
let vanish_href = seed.calendars[0]
|
||||
.items
|
||||
.first()
|
||||
.map(|i| i.href.clone())
|
||||
.expect("at least one event seeded");
|
||||
let vanish_uid = seed.calendars[0].items.first().unwrap().uid.clone();
|
||||
b.delete_item(account, &vanish_href)
|
||||
.expect("baikal delete vanished item");
|
||||
let summary_after_vanish = import_dav::run(
|
||||
common(&cal_archive),
|
||||
dav_config(DavKindArg::Caldav, account, &dav_root),
|
||||
)
|
||||
.expect("caldav re-import after vanish");
|
||||
assert!(
|
||||
!summary_after_vanish.any_failed(),
|
||||
"{}: re-import after vanish had failures",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&cal_archive);
|
||||
assert_eq!(
|
||||
count(&conn, "calendar_events") as usize,
|
||||
ev_count - 1,
|
||||
"{}: vanished event was not pruned",
|
||||
seed.username
|
||||
);
|
||||
let uids_now = all_event_uids(&conn);
|
||||
assert!(
|
||||
!uids_now.contains(&vanish_uid),
|
||||
"{}: vanished event uid {vanish_uid} still in archive",
|
||||
seed.username
|
||||
);
|
||||
drop(conn);
|
||||
|
||||
let icals = integration::data::load_icals().expect("load icals");
|
||||
let (_added_event_href, added_event_uid) = b
|
||||
.add_event(account, 0, &format!("bk-{}", seed.username), &icals)
|
||||
.expect("baikal add event");
|
||||
let summary_after_add_event = import_dav::run(
|
||||
common(&cal_archive),
|
||||
dav_config(DavKindArg::Caldav, account, &dav_root),
|
||||
)
|
||||
.expect("caldav re-import after add");
|
||||
assert!(
|
||||
!summary_after_add_event.any_failed(),
|
||||
"{}: re-import after add-event had failures",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&cal_archive);
|
||||
let after_add_events = count(&conn, "calendar_events") as usize;
|
||||
assert_eq!(
|
||||
after_add_events, ev_count,
|
||||
"{}: add-then-import should restore count to baseline",
|
||||
seed.username
|
||||
);
|
||||
let uids_now = all_event_uids(&conn);
|
||||
assert!(
|
||||
uids_now.contains(&added_event_uid),
|
||||
"{}: added event uid {added_event_uid} not found in archive",
|
||||
seed.username
|
||||
);
|
||||
drop(conn);
|
||||
|
||||
let vcards = integration::data::load_vcards().expect("load vcards");
|
||||
let (_added_card_href, added_card_uid) = b
|
||||
.add_contact(account, 0, &format!("bk-{}", seed.username), &vcards)
|
||||
.expect("baikal add contact");
|
||||
let summary_after_add_contact = import_dav::run(
|
||||
common(&card_archive),
|
||||
dav_config(DavKindArg::Carddav, account, &dav_root),
|
||||
)
|
||||
.expect("carddav re-import after add");
|
||||
assert!(
|
||||
!summary_after_add_contact.any_failed(),
|
||||
"{}: re-import after add-contact had failures",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&card_archive);
|
||||
let after_add_contacts = count(&conn, "contact_cards") as usize;
|
||||
assert_eq!(
|
||||
after_add_contacts,
|
||||
card_count + 1,
|
||||
"{}: add-then-import should grow contact count by 1",
|
||||
seed.username
|
||||
);
|
||||
let imported_contact_uids = all_contact_uids(&conn);
|
||||
assert!(
|
||||
imported_contact_uids.contains(&added_card_uid),
|
||||
"{}: added contact uid {added_card_uid} not found",
|
||||
seed.username
|
||||
);
|
||||
drop(conn);
|
||||
|
||||
let discovery_archive = tmp_archive(&format!("baikal-cal-discovery-{}", seed.username));
|
||||
let discovery_summary = import_dav::run(
|
||||
common(&discovery_archive),
|
||||
dav_config_via_discovery(DavKindArg::Caldav, account, &dav_root),
|
||||
)
|
||||
.expect("caldav import via discovery from dav root");
|
||||
assert!(
|
||||
!discovery_summary.any_failed(),
|
||||
"{}: caldav import via discovery had failures: {discovery_summary:?}",
|
||||
seed.username
|
||||
);
|
||||
let conn = open_archive(&discovery_archive);
|
||||
let discovered_events = count(&conn, "calendar_events") as usize;
|
||||
assert_eq!(
|
||||
discovered_events, ev_count,
|
||||
"{}: discovery-based import event count mismatch (server holds {ev_count} after vanish+add)",
|
||||
seed.username
|
||||
);
|
||||
drop(conn);
|
||||
cleanup(&discovery_archive);
|
||||
|
||||
cleanup(&cal_archive);
|
||||
cleanup(&card_archive);
|
||||
}
|
||||
|
||||
let primary = &b.accounts[0];
|
||||
let other = &b.accounts[1];
|
||||
let shared_archive = tmp_archive("baikal-source-change");
|
||||
import_dav::run(
|
||||
common(&shared_archive),
|
||||
dav_config(DavKindArg::Caldav, primary, &dav_root),
|
||||
)
|
||||
.expect("seed archive with primary user");
|
||||
let err = import_dav::run(
|
||||
common(&shared_archive),
|
||||
dav_config(DavKindArg::Caldav, other, &dav_root),
|
||||
)
|
||||
.expect_err("expected source-change abort");
|
||||
assert!(
|
||||
matches!(err, Error::SourceChange(_)),
|
||||
"expected SourceChange, got {err:?}"
|
||||
);
|
||||
cleanup(&shared_archive);
|
||||
|
||||
b.stop().expect("baikal stop");
|
||||
}
|
||||
|
||||
fn assert_event_collection_exact(conn: &Connection, seed: &AccountSeed) {
|
||||
let names = collection_names(conn, "calendars");
|
||||
let expected: HashSet<String> = seed
|
||||
.calendars
|
||||
.iter()
|
||||
.map(|c| c.display_name.clone())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names, expected,
|
||||
"{}: calendar displayname set mismatch",
|
||||
seed.username
|
||||
);
|
||||
for plan in &seed.calendars {
|
||||
let cal_id = collection_id_by_name(conn, "calendars", &plan.display_name)
|
||||
.unwrap_or_else(|| panic!("calendar {} missing", plan.display_name));
|
||||
let got = calendar_event_count(conn, cal_id);
|
||||
assert_eq!(
|
||||
got as usize,
|
||||
plan.items.len(),
|
||||
"{}: calendar {} event count {got} != seeded {}",
|
||||
seed.username,
|
||||
plan.display_name,
|
||||
plan.items.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_contact_collection_exact(conn: &Connection, seed: &AccountSeed) {
|
||||
let names = collection_names(conn, "address_books");
|
||||
let expected: HashSet<String> = seed
|
||||
.address_books
|
||||
.iter()
|
||||
.map(|c| c.display_name.clone())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names, expected,
|
||||
"{}: address book displayname set mismatch",
|
||||
seed.username
|
||||
);
|
||||
for plan in &seed.address_books {
|
||||
let book_id = collection_id_by_name(conn, "address_books", &plan.display_name)
|
||||
.unwrap_or_else(|| panic!("address book {} missing", plan.display_name));
|
||||
let got = contact_card_count(conn, book_id);
|
||||
assert_eq!(
|
||||
got as usize,
|
||||
plan.items.len(),
|
||||
"{}: book {} contact count {got} != seeded {}",
|
||||
seed.username,
|
||||
plan.display_name,
|
||||
plan.items.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_event_uids_round_trip(conn: &Connection, seed: &AccountSeed) {
|
||||
let imported = all_event_uids(conn);
|
||||
let expected = seed.event_uids();
|
||||
let missing: Vec<_> = expected.difference(&imported).collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"{}: event uids missing from archive: {missing:?}",
|
||||
seed.username
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_contact_uids_round_trip(conn: &Connection, seed: &AccountSeed) {
|
||||
let imported = all_contact_uids(conn);
|
||||
let expected = seed.contact_uids();
|
||||
let missing: Vec<_> = expected.difference(&imported).collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"{}: contact uids missing from archive: {missing:?}",
|
||||
seed.username
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_event_data_well_formed(conn: &Connection) {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT data, data_type FROM calendar_events")
|
||||
.expect("prepare");
|
||||
let rows: Vec<(String, String)> = stmt
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
|
||||
.expect("query")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
assert!(!rows.is_empty(), "no calendar_events rows");
|
||||
for (raw, data_type) in rows {
|
||||
assert_eq!(
|
||||
data_type, "Event",
|
||||
"all VEVENT seeds must land as Event, got data_type={data_type}, raw={raw}"
|
||||
);
|
||||
let v: Value = serde_json::from_str(&raw)
|
||||
.unwrap_or_else(|e| panic!("calendar_events.data invalid JSON: {e}; raw={raw}"));
|
||||
let obj = v.as_object().expect("event data is object");
|
||||
let kind = obj
|
||||
.get("@type")
|
||||
.or_else(|| obj.get("type"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
assert_eq!(kind, "Event", "JSCalendar @type must be Event, got {kind}");
|
||||
assert!(obj.contains_key("uid"), "event JSON missing uid: {obj:?}");
|
||||
assert!(
|
||||
obj.contains_key("start"),
|
||||
"event JSON missing start: {obj:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_contact_data_well_formed(conn: &Connection) {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT uid, data FROM contact_cards")
|
||||
.expect("prepare");
|
||||
let rows: Vec<(String, String)> = stmt
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
|
||||
.expect("query")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
assert!(!rows.is_empty(), "no contact_cards rows");
|
||||
for (uid, raw) in rows {
|
||||
assert!(!uid.is_empty(), "contact uid empty");
|
||||
let v: Value = serde_json::from_str(&raw)
|
||||
.unwrap_or_else(|e| panic!("contact_cards.data invalid JSON: {e}; raw={raw}"));
|
||||
let obj = v.as_object().expect("contact data is object");
|
||||
let kind = obj
|
||||
.get("@type")
|
||||
.or_else(|| obj.get("type"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
assert_eq!(kind, "Card", "JSContact @type must be Card, got {kind}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user