Sync collection truncation + freebusy coalescing

This commit is contained in:
mdecimus
2025-05-05 19:33:34 +02:00
parent 6e89af9698
commit b8329afeba
11 changed files with 382 additions and 158 deletions

View File

@@ -19,7 +19,7 @@ use hyper::StatusCode;
use store::write::serialize::rkyv_unarchive;
pub async fn test(test: &WebDavTest) {
println!("Running REPORT calendar-query tests...");
println!("Running REPORT calendar-query & free-busy-query tests...");
let client = test.client("john");
let cal_path = format!("{}/john/default/", DavResourceName::Cal.base_path());
@@ -732,8 +732,7 @@ BEGIN:VFREEBUSY
DTSTART:20060104T140000Z
DTEND:20060105T220000Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060104T150000Z/20060104T160000Z
FREEBUSY;FBTYPE=BUSY:20060105T170000Z/20060105T180000Z
FREEBUSY;FBTYPE=BUSY:20060104T190000Z/20060104T200000Z
FREEBUSY;FBTYPE=BUSY:20060104T190000Z/20060104T200000Z;20060105T170000Z/20060105T180000Z
FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20060105T100000Z/20060105T120000Z
END:VFREEBUSY
END:VCALENDAR
@@ -753,11 +752,9 @@ BEGIN:VFREEBUSY
DTSTART:20060101T000000Z
DTEND:20060104T140000Z
DTSTAMP:20250505T105255Z
FREEBUSY;FBTYPE=BUSY:20060102T150000Z/20060102T160000Z
FREEBUSY;FBTYPE=BUSY:20060102T170000Z/20060102T180000Z;20060103T170000Z/20060103T180000Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z
FREEBUSY:20060103T100000Z/20060103T120000Z
FREEBUSY:20060104T100000Z/20060104T120000Z
FREEBUSY;FBTYPE=BUSY:20060102T150000Z/20060102T160000Z;20060102T170000Z/20060102T180000Z;
20060103T100000Z/20060103T120000Z;20060103T170000Z/20060103T180000Z;20060104T100000Z/20060104T120000Z
END:VFREEBUSY
END:VCALENDAR
"#;

View File

@@ -31,7 +31,7 @@ pub async fn test(test: &WebDavTest) {
// Obtain sync token
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_eq!(
response.hrefs().len(),
@@ -54,6 +54,7 @@ pub async fn test(test: &WebDavTest) {
&user_base_path,
prev_sync_token,
Depth::Infinity,
None,
["D:getetag"],
)
.await;
@@ -103,7 +104,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::CREATED);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
replace_prefix(&mut hierarchy, &hierarchy_root, &new_hierarchy_root);
assert_result(&response, &hierarchy);
@@ -122,7 +123,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::CREATED);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
let mut copied_hierarchy = hierarchy.clone();
replace_prefix(&mut copied_hierarchy, &hierarchy_root, &new_hierarchy_root);
@@ -136,7 +137,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::NO_CONTENT);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -154,7 +155,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::NO_CONTENT);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
replace_prefix(&mut hierarchy, &new_hierarchy_root, &hierarchy_root);
assert_result(&response, &hierarchy);
@@ -174,7 +175,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::NO_CONTENT);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
let mut orig_hierarchy = new_hierarchy.clone();
replace_prefix(&mut orig_hierarchy, &new_hierarchy_root, &hierarchy_root);
@@ -205,12 +206,12 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::CREATED);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &orig_hierarchy);
client.validate_values(&orig_hierarchy).await;
let response = client
.sync_collection(&group_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&group_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
replace_prefix(
&mut full_hierarchy,
@@ -260,7 +261,7 @@ pub async fn test(test: &WebDavTest) {
hierarchy.push((folder_path, "".to_string()));
}
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -316,7 +317,7 @@ pub async fn test(test: &WebDavTest) {
.with_status(StatusCode::CREATED);
rename(&mut hierarchy, &folder1_file1, &folder1_file1_new);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -338,7 +339,7 @@ pub async fn test(test: &WebDavTest) {
&folder2_file1_from_folder1,
);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -357,7 +358,7 @@ pub async fn test(test: &WebDavTest) {
delete(&mut hierarchy, &folder1_file2);
rename(&mut hierarchy, &folder2_file1_from_folder1, &folder1_file2);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -376,7 +377,7 @@ pub async fn test(test: &WebDavTest) {
.with_status(StatusCode::CREATED);
copy(&mut hierarchy, &file3_path, &folder3_file3_from_folder1);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -395,7 +396,7 @@ pub async fn test(test: &WebDavTest) {
delete(&mut hierarchy, &folder2_file2);
copy(&mut hierarchy, &folder3_file3_from_folder1, &folder2_file2);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -440,12 +441,12 @@ pub async fn test(test: &WebDavTest) {
];
delete(&mut hierarchy, &folder3_file1);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
let response = client
.sync_collection(&group_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&group_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &shared_hierarchy);
client.validate_values(&shared_hierarchy).await;
@@ -470,7 +471,7 @@ pub async fn test(test: &WebDavTest) {
.with_status(StatusCode::CREATED);
replace_prefix(&mut hierarchy, &folder3, &folder2_folder3);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_result(&response, &hierarchy);
client.validate_values(&hierarchy).await;
@@ -501,7 +502,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::CREATED);
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
copy_prefix(&mut hierarchy, &folder1, &folder2_folder1);
assert_result(&response, &hierarchy);

View File

@@ -21,10 +21,7 @@ use common::{
core::BuildServer,
manager::boot::build_ipc,
};
use dav_proto::{
Depth,
schema::property::{DavProperty, WebDavProperty},
};
use dav_proto::schema::property::{DavProperty, WebDavProperty};
use groupware::{DavResourceName, hierarchy::DavHierarchy};
use http::HttpSessionManager;
use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION};
@@ -80,6 +77,7 @@ pub async fn webdav_tests() {
principals::test(&handle).await;
acl::test(&handle).await;
card_query::test(&handle).await;
cal_query::test(&handle).await;
// Print elapsed time
let elapsed = start_time.elapsed();
@@ -336,39 +334,6 @@ impl DummyWebDavClient {
}
}
pub async fn sync_collection(
&self,
path: &str,
sync_token: &str,
depth: Depth,
properties: impl IntoIterator<Item = &str>,
) -> DavResponse {
let mut request = concat!(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
"<D:sync-collection xmlns:D=\"DAV:\" xmlns:A=\"urn:ietf:params:xml:ns:caldav\" xmlns:B=\"urn:ietf:params:xml:ns:carddav\">",
"<D:prop>"
)
.to_string();
for property in properties {
request.push_str(&format!("<{property}/>"));
}
request.push_str("</D:prop><D:sync-token>");
request.push_str(sync_token);
request.push_str("</D:sync-token><D:sync-level>");
request.push_str(match depth {
Depth::One => "1",
Depth::Infinity => "infinite",
_ => "0",
});
request.push_str("</D:sync-level></D:sync-collection>");
self.request("REPORT", path, &request)
.await
.with_status(StatusCode::MULTI_STATUS)
}
pub async fn available_quota(&self, path: &str) -> u64 {
self.propfind(
path,

View File

@@ -4,8 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::WebDavTest;
use super::{DavResponse, DummyWebDavClient, WebDavTest};
use crate::webdav::GenerateTestDavResource;
use ahash::AHashSet;
use dav_proto::Depth;
use groupware::DavResourceName;
use hyper::StatusCode;
@@ -22,11 +23,11 @@ pub async fn test(test: &WebDavTest) {
"Running REPORT sync-collection tests ({})...",
resource_type.base_path()
);
let user_base_path = format!("{}/john", resource_type.base_path());
let user_base_path = format!("{}/john/", resource_type.base_path());
// Test 1: Initial sync
let response = client
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
.await;
assert_eq!(
response.hrefs().len(),
@@ -46,13 +47,14 @@ pub async fn test(test: &WebDavTest) {
&user_base_path,
&sync_token_1,
Depth::Infinity,
None,
["D:getetag"],
)
.await;
assert_eq!(response.hrefs(), Vec::<String>::new());
// Test 3: Create a collection and make sure it is synced
let new_collection = format!("{}/new-collection/", user_base_path);
let new_collection = format!("{}new-collection/", user_base_path);
client
.mkcol("MKCOL", &new_collection, [], [])
.await
@@ -62,6 +64,7 @@ pub async fn test(test: &WebDavTest) {
&user_base_path,
&sync_token_1,
Depth::Infinity,
None,
["D:getetag"],
)
.await;
@@ -80,6 +83,7 @@ pub async fn test(test: &WebDavTest) {
&user_base_path,
&sync_token_1,
Depth::Infinity,
None,
["D:getetag"],
)
.await;
@@ -93,6 +97,7 @@ pub async fn test(test: &WebDavTest) {
&user_base_path,
&sync_token_2,
Depth::Infinity,
None,
["D:getetag"],
)
.await;
@@ -100,13 +105,25 @@ pub async fn test(test: &WebDavTest) {
// Test 5: sync-token with Depth 1
let response = client
.sync_collection(&user_base_path, &sync_token_1, Depth::One, ["D:getetag"])
.sync_collection(
&user_base_path,
&sync_token_1,
Depth::One,
None,
["D:getetag"],
)
.await;
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
// Test 6: sync-token with Depth 0
let response = client
.sync_collection(&new_collection, &sync_token_1, Depth::Zero, ["D:getetag"])
.sync_collection(
&new_collection,
&sync_token_1,
Depth::Zero,
None,
["D:getetag"],
)
.await;
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
@@ -138,6 +155,79 @@ pub async fn test(test: &WebDavTest) {
.with_status(StatusCode::CREATED)
.with_empty_body();
// Test 9: Limit
let mut sync_token = client
.sync_collection(
&new_collection,
&sync_token_3,
Depth::Zero,
None,
["D:getetag"],
)
.await
.sync_token()
.to_string();
let (folder_name, files) = client
.create_hierarchy(user_base_path.trim_end_matches('/'), 1, 0, 10)
.await;
let mut expected_changes = files
.iter()
.map(|x| x.0.as_str())
.chain([folder_name.as_str()])
.collect::<AHashSet<_>>();
for _ in 0..10 {
let response = client
.sync_collection(
&user_base_path,
&sync_token,
Depth::Infinity,
2.into(),
["D:getetag"],
)
.await;
sync_token = response.sync_token().to_string();
let hrefs = response.hrefs();
if hrefs.is_empty() {
break;
}
let mut has_user_base_path = false;
let mut item_count = 0;
for href in hrefs {
if href == user_base_path {
has_user_base_path = true;
} else if expected_changes.remove(href) {
item_count += 1;
} else {
panic!("Unexpected href: {href}");
}
}
if has_user_base_path {
assert_eq!(item_count, 2);
response
.with_value(
"D:multistatus.D:response.D:status",
"HTTP/1.1 507 Insufficient Storage",
)
.with_value(
"D:multistatus.D:response.D:error.D:number-of-matches-within-limits",
"",
)
.with_value(
"D:multistatus.D:response.D:responsedescription",
"The number of matches exceeds the limit of 2",
);
} else {
assert!(item_count <= 2);
break;
}
}
assert!(expected_changes.is_empty(), "{:?}", expected_changes);
client
.request("DELETE", &folder_name, "")
.await
.with_status(StatusCode::NO_CONTENT);
client
.request("DELETE", &new_collection, "")
.await
@@ -147,3 +237,47 @@ pub async fn test(test: &WebDavTest) {
client.delete_default_containers().await;
test.assert_is_empty().await;
}
impl DummyWebDavClient {
pub async fn sync_collection(
&self,
path: &str,
sync_token: &str,
depth: Depth,
limit: Option<usize>,
properties: impl IntoIterator<Item = &str>,
) -> DavResponse {
let mut request = concat!(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
"<D:sync-collection xmlns:D=\"DAV:\" xmlns:A=\"urn:ietf:params:xml:ns:caldav\" xmlns:B=\"urn:ietf:params:xml:ns:carddav\">",
"<D:prop>"
)
.to_string();
for property in properties {
request.push_str(&format!("<{property}/>"));
}
request.push_str("</D:prop><D:sync-token>");
request.push_str(sync_token);
request.push_str("</D:sync-token><D:sync-level>");
request.push_str(match depth {
Depth::One => "1",
Depth::Infinity => "infinite",
_ => "0",
});
request.push_str("</D:sync-level>");
if let Some(limit) = limit {
request.push_str("<D:limit><D:nresults>");
request.push_str(&limit.to_string());
request.push_str("</D:nresults></D:limit>");
}
request.push_str("</D:sync-collection>");
self.request("REPORT", path, &request)
.await
.with_status(StatusCode::MULTI_STATUS)
}
}