Fix sync issues

This commit is contained in:
Maurus Decimus
2026-05-29 19:49:37 +02:00
parent 576073f8c9
commit 0dfbaf8096
9 changed files with 614 additions and 49 deletions
+187
View File
@@ -66,6 +66,66 @@ fn discovery_uses_url_as_homeset_when_collection_present() {
assert!(disc.collections[0].props.is_calendar);
}
#[test]
fn discovery_resolves_per_user_principal_even_when_url_lists_collections() {
let mut server = mockito::Server::new();
let url = server.url();
let collections_body = format!(
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>{url}/dav/cal/[email protected]/default/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype><d:collection/><c:calendar/></d:resourcetype>
<d:displayname>Default</d:displayname>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#
);
let _listing = server
.mock("PROPFIND", "/dav/cal/")
.match_header("depth", "1")
.with_status(207)
.with_header("content-type", "application/xml; charset=utf-8")
.with_body(&collections_body)
.create();
let principal_body = format!(
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>{url}/dav/cal/</d:href>
<d:propstat>
<d:prop>
<d:current-user-principal><d:href>{url}/dav/principals/[email protected]/</d:href></d:current-user-principal>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#
);
let _principal = server
.mock("PROPFIND", "/dav/cal/")
.match_header("depth", "0")
.with_status(207)
.with_header("content-type", "application/xml; charset=utf-8")
.with_body(&principal_body)
.create();
let c = client(0);
let disc = discover(&c, DavKind::Caldav, &format!("{url}/dav/cal/")).expect("discover");
assert_eq!(disc.collections.len(), 1);
assert_eq!(
disc.principal_url.as_deref(),
Some(format!("{url}/dav/principals/[email protected]/").as_str()),
"account identity must be the per-user principal, not the shared base DAV root"
);
}
#[test]
fn discovery_falls_through_principal_to_home_set() {
let mut server = mockito::Server::new();
@@ -1129,6 +1189,133 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() {
assert_eq!(event_counts.1.created, 2);
}
#[test]
fn dav_source_change_protection_fires_across_users_on_same_root() {
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use std::path::PathBuf;
use vandelay::logging::Logger;
use vandelay::sync::CommonConfig;
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run};
let mut server = mockito::Server::new();
let url = server.url();
let listing = format!(
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>{url}/dav/cal/shared/default/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype><d:collection/><c:calendar/></d:resourcetype>
<d:displayname>Default</d:displayname>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#
);
let _listing = server
.mock("PROPFIND", "/dav/cal/")
.match_header("depth", "1")
.with_status(207)
.with_header("content-type", "application/xml; charset=utf-8")
.with_body(&listing)
.create();
let empty_items = format!(
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>{url}/dav/cal/shared/default/</d:href>
<d:propstat>
<d:prop><d:resourcetype><d:collection/><c:calendar/></d:resourcetype></d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#
);
let _items = server
.mock("PROPFIND", "/dav/cal/shared/default/")
.with_status(207)
.with_header("content-type", "application/xml; charset=utf-8")
.with_body(&empty_items)
.create();
let auth_a = format!("Basic {}", STANDARD.encode("[email protected]:passA"));
let auth_b = format!("Basic {}", STANDARD.encode("[email protected]:passB"));
let principal = |who: &str| {
format!(
r#"<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>{url}/dav/cal/</d:href>
<d:propstat>
<d:prop><d:current-user-principal><d:href>{url}/dav/principals/{who}/</d:href></d:current-user-principal></d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>"#
)
};
let _pa = server
.mock("PROPFIND", "/dav/cal/")
.match_header("depth", "0")
.match_header("authorization", auth_a.as_str())
.with_status(207)
.with_header("content-type", "application/xml; charset=utf-8")
.with_body(principal("[email protected]"))
.create();
let _pb = server
.mock("PROPFIND", "/dav/cal/")
.match_header("depth", "0")
.match_header("authorization", auth_b.as_str())
.with_status(207)
.with_header("content-type", "application/xml; charset=utf-8")
.with_body(principal("[email protected]"))
.create();
let archive: PathBuf = std::env::temp_dir().join(format!(
"vandelay-dav-srcchange-{}-{}.sqlite",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = std::fs::remove_file(&archive);
let common = |archive: &PathBuf| CommonConfig {
archive: archive.clone(),
threads: 1,
dry_run: false,
max_retries: 0,
allow_invalid_certs: true,
logger: Logger::from_flags(false, 0),
};
let config = |user: &str, pass: &str| DavImportConfig {
kind: DavKindArg::Caldav,
url: format!("{url}/dav/cal/"),
auth: DavAuth::Basic {
user: user.to_owned(),
password: pass.to_owned(),
},
allow_cleartext: true,
dav_connections: 1,
multiget_batch: 50,
allow_source_change: false,
};
run(common(&archive), config("[email protected]", "passA")).expect("user A import ok");
let err = run(common(&archive), config("[email protected]", "passB")).unwrap_err();
let _ = std::fs::remove_file(&archive);
assert!(
matches!(err, vandelay::error::Error::SourceChange(_)),
"importing a different user into the same archive must trigger source-change protection; got {err:?}"
);
}
#[test]
fn source_change_protection_rejects_different_session_url_same_account() {
use rusqlite::Connection;