905 lines
32 KiB
Rust
905 lines
32 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::{DavResponse, GenerateTestDavResource},
|
|
};
|
|
use ahash::AHashSet;
|
|
use dav_proto::Depth;
|
|
use groupware::DavResourceName;
|
|
use hyper::StatusCode;
|
|
use registry::schema::structs::Action;
|
|
|
|
pub async fn test(test: &TestServer, assisted_discovery: bool) {
|
|
let admin = test.account("admin@example.com");
|
|
let client = test.account("jane@example.com").webdav_client();
|
|
let mike_noquota = test.account("mike@example.com").webdav_client();
|
|
|
|
for resource_type in [
|
|
DavResourceName::File,
|
|
DavResourceName::Cal,
|
|
DavResourceName::Card,
|
|
] {
|
|
println!("Running COPY/MOVE tests ({})...", resource_type.base_path());
|
|
let user_base_path = format!("{}/jane%40example.com", resource_type.base_path());
|
|
let group_base_path = format!("{}/support%40example.com", resource_type.base_path());
|
|
let default_test_depth = if resource_type == DavResourceName::File {
|
|
2
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// Obtain sync token
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
|
|
// TODO: Fix tests for assisted discovery
|
|
assert_eq!(
|
|
response.hrefs().len(),
|
|
if resource_type == DavResourceName::File {
|
|
1
|
|
} else {
|
|
2 + usize::from(assisted_discovery)
|
|
},
|
|
"{:?}",
|
|
response.hrefs()
|
|
);
|
|
|
|
// Create nested files and folders
|
|
let (hierarchy_root, mut hierarchy) = client
|
|
.create_hierarchy(&user_base_path, default_test_depth, 2, 3)
|
|
.await;
|
|
let prev_sync_token = response.sync_token();
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
prev_sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
let sync_token = response.sync_token();
|
|
let changed_hrefs = response.hrefs();
|
|
assert_ne!(sync_token, prev_sync_token);
|
|
assert_eq!(
|
|
changed_hrefs,
|
|
hierarchy.iter().map(|x| x.0.as_str()).collect::<Vec<_>>(),
|
|
"lengths {} & {}",
|
|
changed_hrefs.len(),
|
|
hierarchy.len()
|
|
);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Delete cache an resync
|
|
admin.registry_create_object(Action::InvalidateCaches).await;
|
|
let response = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
prev_sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await;
|
|
let sync_token = response.sync_token();
|
|
let changed_hrefs = response.hrefs();
|
|
assert_ne!(sync_token, prev_sync_token);
|
|
assert_eq!(
|
|
changed_hrefs,
|
|
hierarchy.iter().map(|x| x.0.as_str()).collect::<Vec<_>>(),
|
|
"lengths {} & {}",
|
|
changed_hrefs.len(),
|
|
hierarchy.len()
|
|
);
|
|
|
|
// Copying and moving to the same or root containers is invalid
|
|
for method in ["COPY", "MOVE"] {
|
|
for destination in [
|
|
"/dav",
|
|
"/dav/cal",
|
|
"/dav/card",
|
|
"/dav/file",
|
|
"/dav/pal",
|
|
hierarchy_root.as_str(),
|
|
] {
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&hierarchy_root,
|
|
[("destination", destination)],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::BAD_GATEWAY);
|
|
}
|
|
}
|
|
|
|
// Test 1: Rename container
|
|
let new_hierarchy_root = format!("{user_base_path}/Test_Folder/");
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&hierarchy_root,
|
|
[("destination", new_hierarchy_root.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
replace_prefix(&mut hierarchy, &hierarchy_root, &new_hierarchy_root);
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
// Validate changes
|
|
let changes = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await
|
|
.with_href_count(2)
|
|
.into_propfind_response(None);
|
|
changes
|
|
.properties(&hierarchy_root)
|
|
.with_status(StatusCode::NOT_FOUND);
|
|
changes
|
|
.properties(&new_hierarchy_root)
|
|
.with_status(StatusCode::OK);
|
|
let hierarchy_root = new_hierarchy_root;
|
|
|
|
// Test 2: Copy container
|
|
let new_hierarchy_root = format!("{user_base_path}/Test_Folder_Copy/");
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&hierarchy_root,
|
|
[("destination", new_hierarchy_root.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.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);
|
|
copied_hierarchy.extend_from_slice(&hierarchy);
|
|
assert_result(&response, &copied_hierarchy);
|
|
client.validate_values(&copied_hierarchy).await;
|
|
|
|
// Test 3: Delete original container
|
|
client
|
|
.request("DELETE", &new_hierarchy_root, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 4: Create a shallow container and overwrite the previous one using MOVE
|
|
let (new_hierarchy_root, mut hierarchy) =
|
|
client.create_hierarchy(&user_base_path, 0, 0, 3).await;
|
|
let sync_token = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await
|
|
.sync_token()
|
|
.to_string();
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&new_hierarchy_root,
|
|
[("destination", hierarchy_root.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
replace_prefix(&mut hierarchy, &new_hierarchy_root, &hierarchy_root);
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
// Validate changes
|
|
let changes = client
|
|
.sync_collection(
|
|
&user_base_path,
|
|
&sync_token,
|
|
Depth::Infinity,
|
|
None,
|
|
["D:getetag"],
|
|
)
|
|
.await
|
|
.into_propfind_response(None);
|
|
changes
|
|
.properties(&new_hierarchy_root)
|
|
.with_status(StatusCode::NOT_FOUND);
|
|
changes
|
|
.properties(&hierarchy_root)
|
|
.with_status(StatusCode::OK);
|
|
|
|
// Test 5: Create a deep container and overwrite the previous one using COPY
|
|
let (new_hierarchy_root, new_hierarchy) = client
|
|
.create_hierarchy(&user_base_path, default_test_depth, 1, 2)
|
|
.await;
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&new_hierarchy_root,
|
|
[("destination", hierarchy_root.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
let response = client
|
|
.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);
|
|
let mut full_hierarchy = new_hierarchy.clone();
|
|
full_hierarchy.extend_from_slice(&orig_hierarchy);
|
|
assert_result(&response, &full_hierarchy);
|
|
client.validate_values(&full_hierarchy).await;
|
|
|
|
// Test 6: Copy and move containers to a shared account
|
|
let shared_hierarchy_root_1 = format!("{group_base_path}/Test_Shared_Folder_1/");
|
|
let shared_hierarchy_root_2 = format!("{group_base_path}/Test_Shared_Folder_2/");
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&new_hierarchy_root,
|
|
[("destination", shared_hierarchy_root_1.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&hierarchy_root,
|
|
[("destination", shared_hierarchy_root_2.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.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, None, ["D:getetag"])
|
|
.await;
|
|
replace_prefix(
|
|
&mut full_hierarchy,
|
|
&new_hierarchy_root,
|
|
&shared_hierarchy_root_1,
|
|
);
|
|
replace_prefix(
|
|
&mut full_hierarchy,
|
|
&hierarchy_root,
|
|
&shared_hierarchy_root_2,
|
|
);
|
|
assert_result(&response, &full_hierarchy);
|
|
client.validate_values(&full_hierarchy).await;
|
|
|
|
// Delete all containers
|
|
for shared_container in [
|
|
shared_hierarchy_root_1,
|
|
shared_hierarchy_root_2,
|
|
hierarchy_root,
|
|
] {
|
|
client
|
|
.request("DELETE", &shared_container, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
}
|
|
|
|
// Create test containers
|
|
let mut hierarchy = vec![];
|
|
for folder_name in ["folder1", "folder2", "folder3"] {
|
|
let folder_path = format!("{user_base_path}/{folder_name}/");
|
|
|
|
client
|
|
.mkcol("MKCOL", &folder_path, [], [])
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
|
|
for file_name in ["file1", "file2", "file3"] {
|
|
let file_path = format!("{folder_path}{file_name}");
|
|
let file_contents = resource_type.generate();
|
|
client
|
|
.request("PUT", &file_path, &file_contents)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
hierarchy.push((file_path, file_contents));
|
|
}
|
|
|
|
hierarchy.push((folder_path, "".to_string()));
|
|
}
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 7: Copying or moving files to the root container is not allowed
|
|
let folder1_file1 = format!("{user_base_path}/folder1/file1");
|
|
if resource_type != DavResourceName::File {
|
|
for method in ["COPY", "MOVE"] {
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&folder1_file1,
|
|
[("destination", user_base_path.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::BAD_GATEWAY);
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&folder1_file1,
|
|
[("destination", format!("{user_base_path}/folder2").as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::BAD_GATEWAY);
|
|
}
|
|
}
|
|
|
|
// Test 8: Copying or moving to the same location is not allowed
|
|
for method in ["COPY", "MOVE"] {
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&folder1_file1,
|
|
[("destination", folder1_file1.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::BAD_GATEWAY);
|
|
}
|
|
|
|
// Test 9: Rename file
|
|
let folder1_file1_new = format!("{user_base_path}/folder1/file1_new");
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&folder1_file1,
|
|
[("destination", folder1_file1_new.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
rename(&mut hierarchy, &folder1_file1, &folder1_file1_new);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 10: Move a file under a different container
|
|
let folder2_file1_from_folder1 = format!("{user_base_path}/folder2/file1_from_folder1");
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&folder1_file1_new,
|
|
[("destination", folder2_file1_from_folder1.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
rename(
|
|
&mut hierarchy,
|
|
&folder1_file1_new,
|
|
&folder2_file1_from_folder1,
|
|
);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 11: Move and overwrite a file under a different container
|
|
let folder1_file2 = format!("{user_base_path}/folder1/file2");
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&folder2_file1_from_folder1,
|
|
[("destination", folder1_file2.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
delete(&mut hierarchy, &folder1_file2);
|
|
rename(&mut hierarchy, &folder2_file1_from_folder1, &folder1_file2);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 12: Copy a file under a different container
|
|
let file3_path = format!("{user_base_path}/folder1/file3");
|
|
let folder3_file3_from_folder1 = format!("{user_base_path}/folder3/file3_from_folder1");
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&file3_path,
|
|
[("destination", folder3_file3_from_folder1.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
copy(&mut hierarchy, &file3_path, &folder3_file3_from_folder1);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 12: Copy and overwrite a file under a different container
|
|
let folder2_file2 = format!("{user_base_path}/folder2/file2");
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&folder3_file3_from_folder1,
|
|
[("destination", folder2_file2.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
delete(&mut hierarchy, &folder2_file2);
|
|
copy(&mut hierarchy, &folder3_file3_from_folder1, &folder2_file2);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 13: Copy and move files to a shared container
|
|
let shared_hierarchy_root = format!("{group_base_path}/Test_Child_Folder/");
|
|
let folder3_file1 = format!("{user_base_path}/folder3/file1");
|
|
let shared_file_1 = format!("{shared_hierarchy_root}shared_file_1");
|
|
let shared_file_2 = format!("{shared_hierarchy_root}shared_file_2");
|
|
client
|
|
.mkcol("MKCOL", &shared_hierarchy_root, [], [])
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&folder3_file1,
|
|
[("destination", shared_file_1.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&folder1_file2,
|
|
[("destination", shared_file_2.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let shared_hierarchy = vec![
|
|
(shared_hierarchy_root.clone(), "".to_string()),
|
|
(
|
|
shared_file_1,
|
|
get_contents(&hierarchy, &folder3_file1).unwrap(),
|
|
),
|
|
(
|
|
shared_file_2,
|
|
get_contents(&hierarchy, &folder1_file2).unwrap(),
|
|
),
|
|
];
|
|
delete(&mut hierarchy, &folder3_file1);
|
|
let response = client
|
|
.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, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &shared_hierarchy);
|
|
client.validate_values(&shared_hierarchy).await;
|
|
client
|
|
.request("DELETE", &shared_hierarchy_root, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
|
|
if resource_type == DavResourceName::File {
|
|
// Test 14: Move a container under a different container
|
|
let folder2 = format!("{user_base_path}/folder2/");
|
|
let folder3 = format!("{user_base_path}/folder3/");
|
|
let folder2_folder3 = format!("{user_base_path}/folder2/folder3/");
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&folder3,
|
|
[("destination", folder2_folder3.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
replace_prefix(&mut hierarchy, &folder3, &folder2_folder3);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
|
|
// Test 15: Moving or copying a parent under a child is not allowed
|
|
for method in ["MOVE", "COPY"] {
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&folder2_folder3,
|
|
[("destination", folder2.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::BAD_GATEWAY);
|
|
}
|
|
|
|
// Test 16: Copy a container under a different container
|
|
let folder1 = format!("{user_base_path}/folder1/");
|
|
let folder2_folder1 = format!("{user_base_path}/folder2/folder1/");
|
|
client
|
|
.request_with_headers(
|
|
"COPY",
|
|
&folder1,
|
|
[("destination", folder2_folder1.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
|
|
.await;
|
|
copy_prefix(&mut hierarchy, &folder1, &folder2_folder1);
|
|
assert_result(&response, &hierarchy);
|
|
client.validate_values(&hierarchy).await;
|
|
} else {
|
|
// Test 17: UID collision
|
|
let folder1 = format!("{user_base_path}/folder1/");
|
|
let folder2 = format!("{user_base_path}/folder2/");
|
|
let file_contents = resource_type.generate();
|
|
for folder_path in [&folder1, &folder2] {
|
|
let file_path = format!("{folder_path}uid_test");
|
|
client
|
|
.request("PUT", &file_path, file_contents.as_str())
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
}
|
|
let uid_file_src = format!("{folder1}uid_test");
|
|
let uid_file_dest = format!("{folder2}uid_test_dup");
|
|
for method in ["COPY", "MOVE"] {
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&uid_file_src,
|
|
[("destination", uid_file_dest.as_str())],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::PRECONDITION_FAILED)
|
|
.with_failed_precondition(
|
|
if resource_type == DavResourceName::Cal {
|
|
"A:no-uid-conflict.D:href"
|
|
} else {
|
|
"B:no-uid-conflict.D:href"
|
|
},
|
|
&format!("{folder2}uid_test"),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Delete all containers and create a new one
|
|
client
|
|
.request("DELETE", &format!("{user_base_path}/folder3/"), "")
|
|
.await
|
|
.with_status(if resource_type == DavResourceName::File {
|
|
StatusCode::NOT_FOUND
|
|
} else {
|
|
StatusCode::NO_CONTENT
|
|
});
|
|
for folder in ["folder1", "folder2"] {
|
|
let folder_path = format!("{user_base_path}/{folder}/");
|
|
client
|
|
.request("DELETE", &folder_path, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
}
|
|
|
|
// Create a new test container and file
|
|
let test_base_path = format!("{user_base_path}/My_Test_Folder/");
|
|
client
|
|
.mkcol("MKCOL", &test_base_path, [], [])
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let test_contents_1 = resource_type.generate();
|
|
let test_contents_2 = resource_type.generate();
|
|
let test_file1_path = format!("{test_base_path}test_file_1");
|
|
let test_file2_path = format!("{test_base_path}test_file_2");
|
|
let test_etag_1 = client
|
|
.request("PUT", &test_file1_path, test_contents_1.as_str())
|
|
.await
|
|
.with_status(StatusCode::CREATED)
|
|
.etag()
|
|
.to_string();
|
|
let test_etag_2 = client
|
|
.request("PUT", &test_file2_path, test_contents_2.as_str())
|
|
.await
|
|
.with_status(StatusCode::CREATED)
|
|
.etag()
|
|
.to_string();
|
|
|
|
// Test 18: Failed DAV preconditions
|
|
for method in ["COPY", "MOVE"] {
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&test_file1_path,
|
|
[
|
|
("destination", test_file2_path.as_str()),
|
|
("overwrite", "F"),
|
|
],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::PRECONDITION_FAILED)
|
|
.with_empty_body();
|
|
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&test_file1_path,
|
|
[
|
|
("destination", test_file2_path.as_str()),
|
|
("if-none-match", "*"),
|
|
],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::PRECONDITION_FAILED)
|
|
.with_empty_body();
|
|
|
|
let iff = format!(
|
|
"<{test_file1_path}> (Not [{test_etag_1}]) <{test_file2_path}> (Not [{test_etag_2}])",
|
|
);
|
|
client
|
|
.request_with_headers(
|
|
method,
|
|
&test_file1_path,
|
|
[
|
|
("destination", test_file2_path.as_str()),
|
|
("if", iff.as_str()),
|
|
],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::PRECONDITION_FAILED)
|
|
.with_empty_body();
|
|
}
|
|
|
|
// Test 18: Successful DAV preconditions
|
|
let iff =
|
|
format!("<{test_file1_path}> ([{test_etag_1}]) <{test_file2_path}> ([{test_etag_2}])",);
|
|
client
|
|
.request_with_headers(
|
|
"MOVE",
|
|
&test_file1_path,
|
|
[
|
|
("destination", test_file2_path.as_str()),
|
|
("if", iff.as_str()),
|
|
],
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
|
|
// Delete the test container
|
|
client
|
|
.request("DELETE", &test_base_path, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
|
|
// Test 19: Quota enforcement (on CalDAV/CardDAV items are linked, not copied therefore there is no quota increase)
|
|
if resource_type == DavResourceName::File {
|
|
let path = format!(
|
|
"{}/mike%40example.com/quota-test/",
|
|
resource_type.base_path()
|
|
);
|
|
let content = resource_type.generate();
|
|
mike_noquota
|
|
.mkcol("MKCOL", &path, [], [])
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
mike_noquota
|
|
.request_with_headers("PUT", &format!("{path}file"), [], &content)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let mut num_success = 0;
|
|
let mut did_fail = false;
|
|
|
|
for i in 0..100 {
|
|
let response = mike_noquota
|
|
.request_with_headers(
|
|
"COPY",
|
|
&path,
|
|
[(
|
|
"destination",
|
|
format!(
|
|
"{}/mike%40example.com/quota-test{i}",
|
|
resource_type.base_path()
|
|
)
|
|
.as_str(),
|
|
)],
|
|
&content,
|
|
)
|
|
.await;
|
|
match response.status {
|
|
StatusCode::CREATED => {
|
|
num_success += 1;
|
|
}
|
|
StatusCode::PRECONDITION_FAILED => {
|
|
did_fail = true;
|
|
break;
|
|
}
|
|
_ => panic!("Unexpected status code: {:?}", response.status),
|
|
}
|
|
}
|
|
if !did_fail {
|
|
panic!("Quota test failed: {} files created", num_success);
|
|
}
|
|
if num_success == 0 {
|
|
panic!("Quota test failed: no files created");
|
|
}
|
|
|
|
mike_noquota
|
|
.request("DELETE", &path, "")
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
for i in 0..num_success {
|
|
mike_noquota
|
|
.request(
|
|
"DELETE",
|
|
&format!(
|
|
"{}/mike%40example.com/quota-test{i}",
|
|
resource_type.base_path()
|
|
),
|
|
"",
|
|
)
|
|
.await
|
|
.with_status(StatusCode::NO_CONTENT);
|
|
}
|
|
}
|
|
}
|
|
|
|
client.delete_default_containers().await;
|
|
client
|
|
.delete_default_containers_by_account("support@example.com")
|
|
.await;
|
|
mike_noquota.delete_default_containers().await;
|
|
test.assert_is_empty().await;
|
|
}
|
|
|
|
fn assert_result(response: &DavResponse, hierarchy: &[(String, String)]) {
|
|
assert!(!hierarchy.is_empty());
|
|
let response = response
|
|
.hrefs()
|
|
.into_iter()
|
|
.filter(|h| {
|
|
!h.ends_with("/jane%40example.com/")
|
|
&& !h.ends_with("/support%40example.com/")
|
|
&& !h.ends_with("/default/")
|
|
})
|
|
.collect::<AHashSet<_>>();
|
|
let hierarchy = hierarchy
|
|
.iter()
|
|
.map(|x| x.0.as_str())
|
|
.collect::<AHashSet<_>>();
|
|
|
|
if hierarchy != response {
|
|
println!("\nMissing: {:?}", hierarchy.difference(&response));
|
|
println!("\nExtra: {:?}", response.difference(&hierarchy));
|
|
|
|
panic!(
|
|
"Hierarchy mismatch: expected {} items, received {} items",
|
|
hierarchy.len(),
|
|
response.len()
|
|
);
|
|
}
|
|
}
|
|
|
|
fn replace_prefix(items: &mut [(String, String)], old_prefix: &str, new_prefix: &str) {
|
|
let mut did_replace = false;
|
|
for (href, _) in items.iter_mut() {
|
|
if let Some(value) = href.strip_prefix(old_prefix) {
|
|
*href = format!("{new_prefix}{value}");
|
|
did_replace = true;
|
|
}
|
|
}
|
|
if !did_replace {
|
|
panic!("Prefix not found: {}", old_prefix);
|
|
}
|
|
}
|
|
|
|
fn rename(items: &mut [(String, String)], old_name: &str, new_name: &str) {
|
|
for (href, _) in items.iter_mut() {
|
|
if href == old_name {
|
|
*href = new_name.to_string();
|
|
return;
|
|
}
|
|
}
|
|
panic!("Item not found: {}", old_name);
|
|
}
|
|
|
|
fn delete(items: &mut Vec<(String, String)>, name: &str) {
|
|
let mut did_delete = false;
|
|
items.retain(|(href, _)| {
|
|
did_delete = did_delete || href == name;
|
|
href != name
|
|
});
|
|
|
|
if !did_delete {
|
|
panic!("Item not found: {}", name);
|
|
}
|
|
}
|
|
|
|
fn copy(items: &mut Vec<(String, String)>, old_name: &str, new_name: &str) {
|
|
for (href, contents) in items.iter_mut() {
|
|
if href == old_name {
|
|
let value = (new_name.to_string(), contents.to_string());
|
|
items.push(value);
|
|
return;
|
|
}
|
|
}
|
|
panic!("Item not found: {}", old_name);
|
|
}
|
|
|
|
fn copy_prefix(items: &mut Vec<(String, String)>, old_prefix: &str, new_prefix: &str) {
|
|
let mut new_items = vec![];
|
|
for (href, contents) in items.iter() {
|
|
if let Some(value) = href.strip_prefix(old_prefix) {
|
|
new_items.push((format!("{new_prefix}{value}"), contents.to_string()));
|
|
}
|
|
}
|
|
if !new_items.is_empty() {
|
|
items.extend(new_items);
|
|
} else {
|
|
panic!("Prefix not found: {}", old_prefix);
|
|
}
|
|
}
|
|
|
|
fn get_contents(items: &[(String, String)], name: &str) -> Option<String> {
|
|
for (href, contents) in items.iter() {
|
|
if href == name {
|
|
return Some(contents.to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|