Initial commit

This commit is contained in:
Maurus Decimus
2026-05-29 18:02:15 +02:00
commit 576073f8c9
263 changed files with 78638 additions and 0 deletions
+369
View File
@@ -0,0 +1,369 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
mod integration;
mod seeder;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use integration::stalwart::shared as shared_stalwart;
use rusqlite::Connection;
fn base_url() -> &'static str {
shared_stalwart().base_url()
}
use vandelay::jmap::account::AccountSelector;
use vandelay::jmap::http::Auth;
use vandelay::logging::Logger;
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
fn tmp_archive(tag: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"vandelay-{tag}-{}-{}.sqlite",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = std::fs::remove_file(&p);
p
}
fn common(archive: &Path) -> CommonConfig {
CommonConfig {
archive: archive.to_path_buf(),
threads: 4,
dry_run: false,
max_retries: 5,
allow_invalid_certs: true,
logger: Logger::from_flags(false, 0),
}
}
fn caldav_cfg(base_url: &str, domain: &str, localpart: &str) -> DavImportConfig {
DavImportConfig {
kind: DavKindArg::Caldav,
url: base_url.to_owned(),
auth: DavAuth::Basic {
user: format!("{localpart}@{domain}"),
password: seeder::USER_PASSWORD.to_owned(),
},
allow_cleartext: false,
dav_connections: 4,
multiget_batch: 50,
allow_source_change: false,
}
}
fn count(conn: &Connection, table: &str) -> i64 {
conn.query_row(&format!("SELECT count(*) FROM {table}"), [], |r| r.get(0))
.unwrap()
}
fn jmap_basic(localpart: &str, domain: &str) -> Auth {
Auth::Basic {
user: format!("{localpart}@{domain}"),
password: seeder::USER_PASSWORD.to_owned(),
}
}
fn export_cfg(base_url: &str, domain: &str, localpart: &str, account_id: &str) -> ExportConfig {
ExportConfig {
connect: ConnectConfig {
url: base_url.to_owned(),
auth: jmap_basic(localpart, domain),
account: AccountSelector::Id(account_id.to_owned()),
},
objects: None,
prune: false,
yes: true,
}
}
fn jmap_import_cfg(
base_url: &str,
domain: &str,
localpart: &str,
account_id: &str,
) -> ImportConfig {
ImportConfig {
connect: ConnectConfig {
url: base_url.to_owned(),
auth: jmap_basic(localpart, domain),
account: AccountSelector::Id(account_id.to_owned()),
},
objects: None,
allow_source_change: false,
}
}
fn event_uids(conn: &Connection) -> HashSet<String> {
let mut stmt = conn
.prepare("SELECT json_extract(data, '$.uid') FROM calendar_events")
.unwrap();
let rows = stmt
.query_map([], |r| r.get::<_, Option<String>>(0))
.unwrap();
let mut out = HashSet::new();
for r in rows {
if let Some(u) = r.unwrap() {
out.insert(u);
}
}
out
}
#[test]
#[ignore = "requires Docker"]
fn caldav_import_test1_yields_calendars_and_events() {
let fx = seeder::provision(base_url()).expect("provision");
let acc = fx.account("test1").expect("test1");
let archive = tmp_archive("caldav");
assert_eq!(fx.domain, seeder::DOMAIN);
assert!(!fx.domain_id.is_empty(), "domain id resolved");
assert_eq!(
fx.admin_login,
(
seeder::ADMIN_USER.to_owned(),
seeder::ADMIN_PASSWORD.to_owned()
)
);
assert!(!acc.admin_role, "test1 is a regular user, not admin");
let seeded = acc.seeded.as_ref().expect("test1 seed stats");
assert!(
seeded.calendars > 0,
"test1 layout requests an extra calendar"
);
assert!(seeded.events > 0, "test1 layout seeds events");
assert!(
seeded.mailboxes_created > 0,
"test1 has a seeded mailbox tree"
);
assert!(seeded.emails > 0, "test1 has seeded emails");
assert!(seeded.address_books > 0, "test1 has seeded address books");
assert!(seeded.contacts > 0, "test1 has seeded contacts");
assert!(seeded.file_nodes > 0, "test1 has seeded file nodes");
assert!(seeded.identity, "test1 has a custom identity");
assert_eq!(
seeded.sieve_active,
Some(true),
"test1 has an active sieve script"
);
let summary = sync::import_dav::run(
common(&archive),
caldav_cfg(&fx.base_url, &fx.domain, &acc.localpart),
)
.expect("import");
assert!(!summary.any_failed(), "import had failures: {summary:?}");
let conn = Connection::open(&archive).unwrap();
let calendars = count(&conn, "calendars") as usize;
let events = count(&conn, "calendar_events") as usize;
assert!(
calendars >= seeded.calendars,
"imported calendars ({calendars}) covers seeded layout ({})",
seeded.calendars,
);
assert!(
events >= seeded.events,
"imported events ({events}) covers seeded layout ({})",
seeded.events,
);
seeder::teardown(base_url()).expect("teardown");
}
#[test]
#[ignore = "requires Docker"]
fn caldav_import_covers_all_three_sync_in_accounts() {
let fx = seeder::provision(base_url()).expect("provision");
for localpart in ["test1", "test2", "test3"] {
let acc = fx.account(localpart).expect(localpart);
let archive = tmp_archive(&format!("caldav-all-{localpart}"));
let summary = sync::import_dav::run(
common(&archive),
caldav_cfg(&fx.base_url, &fx.domain, &acc.localpart),
)
.unwrap_or_else(|e| panic!("import {localpart} failed: {e:?}"));
assert!(
!summary.any_failed(),
"{localpart} import had failures: {summary:?}"
);
let conn = Connection::open(&archive).unwrap();
let calendars = count(&conn, "calendars") as usize;
assert!(
calendars >= 1,
"{localpart} has at least one calendar (Stalwart default)"
);
}
seeder::teardown(base_url()).expect("teardown");
}
#[test]
#[ignore = "requires Docker"]
fn caldav_import_with_tiny_multiget_batch_converges_identically() {
let fx = seeder::provision(base_url()).expect("provision");
let acc = fx.account("test1").expect("test1");
let archive_big = tmp_archive("caldav-batch-big");
sync::import_dav::run(
common(&archive_big),
caldav_cfg(&fx.base_url, &fx.domain, &acc.localpart),
)
.expect("import big batch");
let conn = Connection::open(&archive_big).unwrap();
let big = count(&conn, "calendar_events");
drop(conn);
let archive_small = tmp_archive("caldav-batch-small");
let mut cfg = caldav_cfg(&fx.base_url, &fx.domain, &acc.localpart);
cfg.multiget_batch = 1;
sync::import_dav::run(common(&archive_small), cfg).expect("import small batch");
let conn = Connection::open(&archive_small).unwrap();
let small = count(&conn, "calendar_events");
assert_eq!(big, small, "batch size must not affect imported count");
seeder::teardown(base_url()).expect("teardown");
}
#[test]
#[ignore = "requires Docker"]
fn caldav_round_trip_via_jmap_export_converges() {
let fx = seeder::provision(base_url()).expect("provision");
let src = fx.account("test1").expect("test1");
let tgt = fx.account("test4").expect("test4");
let archive = tmp_archive("caldav-rt");
let imp = sync::import_dav::run(
common(&archive),
caldav_cfg(&fx.base_url, &fx.domain, &src.localpart),
)
.expect("dav import");
assert!(!imp.any_failed(), "dav import had failures: {imp:?}");
let conn = Connection::open(&archive).unwrap();
let local_events = count(&conn, "calendar_events");
assert!(local_events > 0, "DAV import landed events in the archive");
drop(conn);
let e1 = sync::export::run(
common(&archive),
export_cfg(&fx.base_url, &fx.domain, &tgt.localpart, &tgt.account_id),
)
.expect("export 1");
assert!(!e1.any_failed(), "export 1 had failures: {e1:?}");
let created_events_1: u64 = e1
.per_type
.iter()
.filter(|(t, _)| *t == "CalendarEvent")
.map(|(_, c)| c.created)
.sum();
assert!(
created_events_1 > 0,
"first export created CalendarEvent rows on the target: {e1:?}"
);
let e2 = sync::export::run(
common(&archive),
export_cfg(&fx.base_url, &fx.domain, &tgt.localpart, &tgt.account_id),
)
.expect("export 2");
let created_2: u64 = e2.per_type.iter().map(|(_, c)| c.created).sum();
assert_eq!(
created_2, 0,
"second export must be convergent (no new creates): {e2:?}"
);
let skipped_2: u64 = e2.per_type.iter().map(|(_, c)| c.skipped).sum();
assert!(
skipped_2 > 0,
"second export matched existing target objects: {e2:?}"
);
seeder::teardown(base_url()).expect("teardown");
}
#[test]
#[ignore = "requires Docker"]
fn caldav_cross_protocol_parity_with_jmap_matches_uid_set() {
let fx = seeder::provision(base_url()).expect("provision");
let src = fx.account("test1").expect("test1");
let archive_jmap = tmp_archive("caldav-parity-jmap");
let s_jmap = sync::import_jmap::run(
common(&archive_jmap),
jmap_import_cfg(&fx.base_url, &fx.domain, &src.localpart, &src.account_id),
)
.expect("jmap import");
assert!(!s_jmap.any_failed(), "jmap import failures: {s_jmap:?}");
let archive_dav = tmp_archive("caldav-parity-dav");
let s_dav = sync::import_dav::run(
common(&archive_dav),
caldav_cfg(&fx.base_url, &fx.domain, &src.localpart),
)
.expect("dav import");
assert!(!s_dav.any_failed(), "dav import failures: {s_dav:?}");
let conn_jmap = Connection::open(&archive_jmap).unwrap();
let uids_jmap = event_uids(&conn_jmap);
drop(conn_jmap);
let conn_dav = Connection::open(&archive_dav).unwrap();
let uids_dav = event_uids(&conn_dav);
drop(conn_dav);
assert!(
!uids_jmap.is_empty(),
"jmap import should yield at least one event uid"
);
assert!(
!uids_dav.is_empty(),
"dav import should yield at least one event uid"
);
let only_in_jmap: HashSet<_> = uids_jmap.difference(&uids_dav).collect();
let only_in_dav: HashSet<_> = uids_dav.difference(&uids_jmap).collect();
assert!(
only_in_jmap.is_empty() && only_in_dav.is_empty(),
"uid sets must match across protocols: only-in-jmap={only_in_jmap:?} only-in-dav={only_in_dav:?}"
);
seeder::teardown(base_url()).expect("teardown");
}
#[test]
#[ignore = "requires Docker"]
fn caldav_import_is_idempotent_on_second_run() {
let fx = seeder::provision(base_url()).expect("provision");
let acc = fx.account("test1").expect("test1");
assert!(!acc.admin_role);
let archive = tmp_archive("caldav-idem");
sync::import_dav::run(
common(&archive),
caldav_cfg(&fx.base_url, &fx.domain, &acc.localpart),
)
.expect("import 1");
let conn1 = Connection::open(&archive).unwrap();
let n1 = count(&conn1, "calendar_events");
drop(conn1);
sync::import_dav::run(
common(&archive),
caldav_cfg(&fx.base_url, &fx.domain, &acc.localpart),
)
.expect("import 2");
let conn2 = Connection::open(&archive).unwrap();
let n2 = count(&conn2, "calendar_events");
assert_eq!(n1, n2, "second import does not duplicate events");
seeder::teardown(base_url()).expect("teardown");
}