293 lines
9.2 KiB
Rust
293 lines
9.2 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::utils::{server::TestServer, webdav::GenerateTestDavResource};
|
|
|
|
use ahash::AHashSet;
|
|
use dav_proto::Depth;
|
|
use groupware::DavResourceName;
|
|
use hyper::StatusCode;
|
|
|
|
pub async fn test(test: &TestServer) {
|
|
let client = test.account("john@example.com").webdav_client();
|
|
|
|
for resource_type in [
|
|
DavResourceName::File,
|
|
DavResourceName::Cal,
|
|
DavResourceName::Card,
|
|
] {
|
|
println!(
|
|
"Running REPORT sync-collection tests ({})...",
|
|
resource_type.base_path()
|
|
);
|
|
let user_base_path = format!("{}/john%40example.com/", resource_type.base_path());
|
|
|
|
// Test 1: Initial sync
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_eq!(
|
|
response.hrefs().len(),
|
|
if resource_type == DavResourceName::File {
|
|
1
|
|
} else {
|
|
2
|
|
},
|
|
"{:?}",
|
|
response.hrefs()
|
|
);
|
|
let sync_token_1 = response.sync_token().to_string();
|
|
|
|
// Test 2: No changes since last sync
|
|
let response = client
|
|
.sync_collection(
|
|
&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);
|
|
client
|
|
.mkcol("MKCOL", &new_collection, [], [])
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token_1,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
|
|
let sync_token_2 = response.sync_token().to_string();
|
|
|
|
// Test 4: Create a file and make sure it is synced
|
|
let new_file = format!("{new_collection}new-file");
|
|
let contents = resource_type.generate();
|
|
client
|
|
.request("PUT", &new_file, &contents)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token_1,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
response.hrefs(),
|
|
vec![new_collection.clone(), new_file.clone()]
|
|
);
|
|
let sync_token_3 = response.sync_token().to_string();
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token_2,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
assert_eq!(response.hrefs(), vec![new_file.clone()]);
|
|
|
|
// Test 5: sync-token with Depth 1
|
|
let response = client
|
|
.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,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
|
|
|
|
// Test 7: Outdated sync-token in If header should fail
|
|
let new_file2 = format!("{new_collection}new-file2");
|
|
let contents = resource_type.generate();
|
|
let condition = format!("(<{sync_token_2}>)");
|
|
client
|
|
.request_with_headers(
|
|
"PUT",
|
|
&new_file2,
|
|
[("if", condition.as_str())],
|
|
contents.as_str(),
|
|
)
|
|
.await
|
|
.with_status(StatusCode::PRECONDITION_FAILED)
|
|
.with_empty_body();
|
|
|
|
// Test 8: Correct sync-token in If header should work
|
|
let condition = format!("(<{sync_token_3}>)");
|
|
client
|
|
.request_with_headers(
|
|
"PUT",
|
|
&new_file2,
|
|
[("if", condition.as_str())],
|
|
contents.as_str(),
|
|
)
|
|
.await
|
|
.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);
|
|
|
|
// Test 10: Expect changes after deletion
|
|
client
|
|
.request("DELETE", &new_file, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
sync_token = response.sync_token().to_string();
|
|
response
|
|
.with_href_count(1)
|
|
.with_value("D:multistatus.D:response.D:href", &new_file)
|
|
.with_value(
|
|
"D:multistatus.D:response.D:status",
|
|
"HTTP/1.1 404 Not Found",
|
|
);
|
|
client
|
|
.request("DELETE", &new_collection, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
sync_token = response.sync_token().to_string();
|
|
response
|
|
.with_href_count(1)
|
|
.with_value("D:multistatus.D:response.D:href", &new_collection)
|
|
.with_value(
|
|
"D:multistatus.D:response.D:status",
|
|
"HTTP/1.1 404 Not Found",
|
|
);
|
|
client
|
|
.request("DELETE", &folder_name, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await
|
|
.with_href_count(1)
|
|
.with_value("D:multistatus.D:response.D:href", &folder_name)
|
|
.with_value(
|
|
"D:multistatus.D:response.D:status",
|
|
"HTTP/1.1 404 Not Found",
|
|
);
|
|
}
|
|
|
|
client.delete_default_containers().await;
|
|
test.assert_is_empty().await;
|
|
}
|