WebDAV COPY/MOVE tests
This commit is contained in:
@@ -26,6 +26,8 @@ jmap = { path = "../crates/jmap", features = ["test_mode", "enterprise"] }
|
||||
jmap_proto = { path = "../crates/jmap-proto" }
|
||||
imap = { path = "../crates/imap", features = ["test_mode"] }
|
||||
imap_proto = { path = "../crates/imap-proto" }
|
||||
dav = { path = "../crates/dav", features = ["test_mode"] }
|
||||
dav-proto = { path = "../crates/dav-proto", features = ["test_mode"] }
|
||||
groupware = { path = "../crates/groupware", features = ["test_mode"] }
|
||||
http = { path = "../crates/http", features = ["test_mode", "enterprise"] }
|
||||
http_proto = { path = "../crates/http-proto" }
|
||||
|
||||
@@ -39,10 +39,6 @@ pub async fn test(test: &WebDavTest) {
|
||||
.await
|
||||
.match_many(
|
||||
"D:multistatus.D:response.D:href",
|
||||
[
|
||||
"/dav/cal/",
|
||||
"/dav/cal/jane/",
|
||||
"/dav/cal/support%40example%2Ecom/",
|
||||
],
|
||||
["/dav/cal/", "/dav/cal/jane/", "/dav/cal/support/"],
|
||||
);
|
||||
}
|
||||
|
||||
752
tests/src/webdav/copy_move.rs
Normal file
752
tests/src/webdav/copy_move.rs
Normal file
@@ -0,0 +1,752 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{DavResponse, WebDavTest};
|
||||
use crate::webdav::GenerateTestDavResource;
|
||||
use ahash::AHashSet;
|
||||
use dav_proto::Depth;
|
||||
use groupware::DavResourceName;
|
||||
use hyper::StatusCode;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
let client = test.client("jane");
|
||||
|
||||
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", resource_type.base_path());
|
||||
let group_base_path = format!("{}/support", 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, ["D:getetag"])
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.hrefs().len(),
|
||||
if resource_type == DavResourceName::File {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
},
|
||||
"{:?}",
|
||||
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,
|
||||
["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;
|
||||
|
||||
// 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, ["D:getetag"])
|
||||
.await;
|
||||
replace_prefix(&mut hierarchy, &hierarchy_root, &new_hierarchy_root);
|
||||
assert_result(&response, &hierarchy);
|
||||
client.validate_values(&hierarchy).await;
|
||||
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, ["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, ["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;
|
||||
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, ["D:getetag"])
|
||||
.await;
|
||||
replace_prefix(&mut hierarchy, &new_hierarchy_root, &hierarchy_root);
|
||||
assert_result(&response, &hierarchy);
|
||||
client.validate_values(&hierarchy).await;
|
||||
|
||||
// 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, ["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, ["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"])
|
||||
.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, ["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, ["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, ["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, ["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, ["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, ["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, ["D:getetag"])
|
||||
.await;
|
||||
assert_result(&response, &hierarchy);
|
||||
client.validate_values(&hierarchy).await;
|
||||
let response = client
|
||||
.sync_collection(&group_base_path, "", Depth::Infinity, ["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, ["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, ["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);
|
||||
}
|
||||
|
||||
client.delete_default_containers().await;
|
||||
client.delete_default_containers_by_account("support").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/") && !h.ends_with("/support/") && !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
|
||||
}
|
||||
@@ -131,27 +131,27 @@ pub async fn test(test: &WebDavTest) {
|
||||
["D:collection", "A:calendar"].as_slice(),
|
||||
),
|
||||
] {
|
||||
let response = client
|
||||
let mut response = client
|
||||
.mkcol(
|
||||
"MKCOL",
|
||||
path,
|
||||
resource_types.iter().copied(),
|
||||
properties.iter().copied(),
|
||||
)
|
||||
.await;
|
||||
response
|
||||
.await
|
||||
.with_status(StatusCode::CREATED)
|
||||
.match_many("D:mkcol-response.D:propstat.D:status", ["HTTP/1.1 200 OK"]);
|
||||
for (property, _) in properties {
|
||||
response.match_one(
|
||||
response = response.match_one(
|
||||
&format!("D:mkcol-response.D:propstat.D:prop.{property}"),
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// Check the properties of the created collection
|
||||
let response = client.propfind(path, properties.iter().map(|x| x.0)).await;
|
||||
response
|
||||
let mut response = client
|
||||
.propfind(path, properties.iter().map(|x| x.0))
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.match_one("D:multistatus.D:response.D:href", path)
|
||||
.match_one(
|
||||
@@ -159,7 +159,7 @@ pub async fn test(test: &WebDavTest) {
|
||||
"HTTP/1.1 200 OK",
|
||||
);
|
||||
for (property, value) in properties {
|
||||
response.match_one(
|
||||
response = response.match_one(
|
||||
&format!("D:multistatus.D:response.D:propstat.D:prop.{property}"),
|
||||
value,
|
||||
);
|
||||
|
||||
@@ -21,7 +21,8 @@ use common::{
|
||||
core::BuildServer,
|
||||
manager::boot::build_ipc,
|
||||
};
|
||||
use groupware::hierarchy::DavHierarchy;
|
||||
use dav_proto::Depth;
|
||||
use groupware::{DavResourceName, hierarchy::DavHierarchy};
|
||||
use http::HttpSessionManager;
|
||||
use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION};
|
||||
use imap::core::ImapSessionManager;
|
||||
@@ -36,10 +37,12 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use store::rand::{Rng, distr::Alphanumeric, rng};
|
||||
use tokio::sync::watch;
|
||||
use utils::config::Config;
|
||||
|
||||
pub mod basic;
|
||||
pub mod copy_move;
|
||||
pub mod mkcol;
|
||||
pub mod put_get;
|
||||
|
||||
@@ -327,13 +330,9 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest
|
||||
}
|
||||
}
|
||||
store
|
||||
.create_test_group(
|
||||
"support@example.com",
|
||||
"Support Group",
|
||||
&["support@example.com"],
|
||||
)
|
||||
.create_test_group("support", "Support Group", &["support@example.com"])
|
||||
.await;
|
||||
store.add_to_group("jane", "support@example.com").await;
|
||||
store.add_to_group("jane", "support").await;
|
||||
|
||||
WebDavTest {
|
||||
server: inner.build_server(),
|
||||
@@ -355,9 +354,10 @@ pub async fn webdav_tests() {
|
||||
)
|
||||
.await;
|
||||
|
||||
//basic::test(&handle).await;
|
||||
//put_get::test(&handle).await;
|
||||
basic::test(&handle).await;
|
||||
put_get::test(&handle).await;
|
||||
mkcol::test(&handle).await;
|
||||
copy_move::test(&handle).await;
|
||||
|
||||
// Print elapsed time
|
||||
let elapsed = start_time.elapsed();
|
||||
@@ -498,15 +498,22 @@ impl DummyWebDavClient {
|
||||
let mut request = concat!(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
|
||||
"<D:mkcol xmlns:D=\"DAV:\" xmlns:A=\"urn:ietf:params:xml:ns:caldav\" xmlns:B=\"urn:ietf:params:xml:ns:carddav\">",
|
||||
"<D:set><D:prop><D:resourcetype>"
|
||||
"<D:set><D:prop>"
|
||||
)
|
||||
.to_string();
|
||||
|
||||
for resource_type in resource_types {
|
||||
let mut has_resource_type = false;
|
||||
for (idx, resource_type) in resource_types.into_iter().enumerate() {
|
||||
if idx == 0 {
|
||||
request.push_str("<D:resourcetype>");
|
||||
}
|
||||
request.push_str(&format!("<{resource_type}/>"));
|
||||
has_resource_type = true;
|
||||
}
|
||||
|
||||
request.push_str("</D:resourcetype>");
|
||||
if has_resource_type {
|
||||
request.push_str("</D:resourcetype>");
|
||||
}
|
||||
|
||||
for (key, value) in properties {
|
||||
request.push_str(&format!("<{key}>{value}</{key}>"));
|
||||
@@ -541,9 +548,151 @@ impl DummyWebDavClient {
|
||||
self.request("PROPFIND", path, &request).await
|
||||
}
|
||||
|
||||
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 create_hierarchy(
|
||||
&self,
|
||||
base_path: &str,
|
||||
max_depth: usize,
|
||||
containers_per_level: usize,
|
||||
files_per_container: usize,
|
||||
) -> (String, Vec<(String, String)>) {
|
||||
let resource_type = if base_path.starts_with("/dav/card/") {
|
||||
DavResourceName::Card
|
||||
} else if base_path.starts_with("/dav/cal/") {
|
||||
DavResourceName::Cal
|
||||
} else {
|
||||
DavResourceName::File
|
||||
};
|
||||
|
||||
let mut created_resources = Vec::new();
|
||||
|
||||
self.create_hierarchy_recursive(
|
||||
resource_type,
|
||||
base_path,
|
||||
max_depth,
|
||||
containers_per_level,
|
||||
files_per_container,
|
||||
0,
|
||||
&mut created_resources,
|
||||
)
|
||||
.await;
|
||||
|
||||
let root_folder = created_resources.first().unwrap().0.clone();
|
||||
created_resources.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
(root_folder, created_resources)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_hierarchy_recursive(
|
||||
&self,
|
||||
resource_type: DavResourceName,
|
||||
base_path: &str,
|
||||
max_depth: usize,
|
||||
containers_per_level: usize,
|
||||
files_per_container: usize,
|
||||
current_depth: usize,
|
||||
created_resources: &mut Vec<(String, String)>,
|
||||
) {
|
||||
let folder_name = generate_random_name(4);
|
||||
let folder_path = format!("{base_path}/Folder_{folder_name}");
|
||||
|
||||
self.mkcol("MKCOL", &folder_path, [], [])
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
|
||||
created_resources.push((format!("{folder_path}/"), "".to_string()));
|
||||
|
||||
for _ in 0..files_per_container {
|
||||
let file_name = generate_random_name(8);
|
||||
let file_path = format!(
|
||||
"{folder_path}/{file_name}.{}",
|
||||
match resource_type {
|
||||
DavResourceName::Card => "vcf",
|
||||
DavResourceName::Cal => "ics",
|
||||
DavResourceName::File => "txt",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
);
|
||||
let content = match resource_type {
|
||||
DavResourceName::Card => generate_random_vcard(),
|
||||
DavResourceName::Cal => generate_random_ical(),
|
||||
DavResourceName::File => generate_random_content(100, 500),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.request("PUT", &file_path, &content)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
|
||||
created_resources.push((file_path, content));
|
||||
}
|
||||
|
||||
if current_depth < max_depth {
|
||||
for _ in 0..containers_per_level {
|
||||
Box::pin(self.create_hierarchy_recursive(
|
||||
resource_type,
|
||||
&folder_path,
|
||||
max_depth,
|
||||
containers_per_level,
|
||||
files_per_container,
|
||||
current_depth + 1,
|
||||
created_resources,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn validate_values(&self, items: &[(String, String)]) {
|
||||
for (path, value) in items {
|
||||
if !path.ends_with('/') {
|
||||
self.request("GET", path, "")
|
||||
.await
|
||||
.with_status(StatusCode::OK)
|
||||
.with_body(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_default_containers(&self) {
|
||||
self.delete_default_containers_by_account(self.name).await;
|
||||
}
|
||||
|
||||
pub async fn delete_default_containers_by_account(&self, account: &str) {
|
||||
for col in ["card", "cal"] {
|
||||
self.request("DELETE", &format!("/dav/{col}/{}/default", self.name), "")
|
||||
self.request("DELETE", &format!("/dav/{col}/{account}/default"), "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
}
|
||||
@@ -551,7 +700,7 @@ impl DummyWebDavClient {
|
||||
}
|
||||
|
||||
impl DavResponse {
|
||||
pub fn with_status(&self, status: StatusCode) -> &Self {
|
||||
pub fn with_status(self, status: StatusCode) -> Self {
|
||||
if self.status != status {
|
||||
self.dump_response();
|
||||
panic!("Expected {status} but got {}", self.status)
|
||||
@@ -559,12 +708,12 @@ impl DavResponse {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_redirect_to(&self, url: &str) -> &Self {
|
||||
pub fn with_redirect_to(self, url: &str) -> Self {
|
||||
self.with_status(StatusCode::TEMPORARY_REDIRECT)
|
||||
.with_header("location", url)
|
||||
}
|
||||
|
||||
pub fn with_header(&self, header: &str, value: &str) -> &Self {
|
||||
pub fn with_header(self, header: &str, value: &str) -> Self {
|
||||
if self.headers.get(header).is_some_and(|v| v == value) {
|
||||
self
|
||||
} else {
|
||||
@@ -573,7 +722,7 @@ impl DavResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_body(&self, expect_body: impl AsRef<str>) -> &Self {
|
||||
pub fn with_body(self, expect_body: impl AsRef<str>) -> Self {
|
||||
let expect_body = expect_body.as_ref();
|
||||
if self.body.is_ok() {
|
||||
let body = self.body.as_ref().unwrap();
|
||||
@@ -588,6 +737,20 @@ impl DavResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_empty_body(self) -> Self {
|
||||
if self.body.is_ok() {
|
||||
let body = self.body.as_ref().unwrap();
|
||||
if !body.is_empty() {
|
||||
self.dump_response();
|
||||
panic!("Expected empty body but got {body:?}");
|
||||
}
|
||||
self
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Expected empty body but no body was returned.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self, header: &str) -> &str {
|
||||
if let Some(value) = self.headers.get(header) {
|
||||
value
|
||||
@@ -601,6 +764,24 @@ impl DavResponse {
|
||||
self.header("etag")
|
||||
}
|
||||
|
||||
pub fn sync_token(&self) -> &str {
|
||||
self.find_keys("D:multistatus.D:sync-token")
|
||||
.next()
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
self.dump_response();
|
||||
panic!("Sync token not found.")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hrefs(&self) -> Vec<&str> {
|
||||
let mut hrefs = self
|
||||
.find_keys("D:multistatus.D:response.D:href")
|
||||
.collect::<Vec<_>>();
|
||||
hrefs.sort_unstable();
|
||||
hrefs
|
||||
}
|
||||
|
||||
fn dump_response(&self) {
|
||||
eprintln!("-------------------------------------");
|
||||
eprintln!("Status: {}", self.status);
|
||||
@@ -625,7 +806,7 @@ impl DavResponse {
|
||||
}
|
||||
|
||||
// Poor man's XPath
|
||||
pub fn match_one(&self, query: &str, expect: impl AsRef<str>) -> &Self {
|
||||
pub fn match_one(self, query: &str, expect: impl AsRef<str>) -> Self {
|
||||
let expect = expect.as_ref();
|
||||
if let Some(value) = self.find_keys(query).next() {
|
||||
if value != expect {
|
||||
@@ -639,7 +820,7 @@ impl DavResponse {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn match_many<I, T>(&self, query: &str, expect: I) -> &Self
|
||||
pub fn match_many<I, T>(self, query: &str, expect: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: AsRef<str>,
|
||||
@@ -654,7 +835,7 @@ impl DavResponse {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_failed_precondition(&self, precondition: &str, value: &str) -> &Self {
|
||||
pub fn with_failed_precondition(self, precondition: &str, value: &str) -> Self {
|
||||
let error = format!("D:error.{precondition}");
|
||||
if self.find_keys(&error).next().is_none_or(|v| v != value) {
|
||||
self.dump_response();
|
||||
@@ -825,3 +1006,123 @@ END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
"#;
|
||||
|
||||
pub trait GenerateTestDavResource {
|
||||
fn generate(&self) -> String;
|
||||
}
|
||||
|
||||
impl GenerateTestDavResource for DavResourceName {
|
||||
fn generate(&self) -> String {
|
||||
match self {
|
||||
DavResourceName::Card => generate_random_vcard(),
|
||||
DavResourceName::Cal => generate_random_ical(),
|
||||
DavResourceName::File => generate_random_content(100, 200),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_random_vcard() -> String {
|
||||
r#"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:$UID
|
||||
FN:$NAME
|
||||
END:VCARD
|
||||
"#
|
||||
.replace("$UID", &generate_random_name(8))
|
||||
.replace("$NAME", &generate_random_name(10))
|
||||
.replace('\n', "\r\n")
|
||||
}
|
||||
|
||||
fn generate_random_ical() -> String {
|
||||
r#"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
UID:$UID
|
||||
SUMMARY:$SUMMARY
|
||||
DESCRIPTION:$DESCRIPTION
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"#
|
||||
.replace("$UID", &generate_random_name(8))
|
||||
.replace("$SUMMARY", &generate_random_name(10))
|
||||
.replace("$DESCRIPTION", &generate_random_name(20))
|
||||
.replace('\n', "\r\n")
|
||||
}
|
||||
|
||||
fn generate_random_content(min_chars: usize, max_chars: usize) -> String {
|
||||
let mut rng = rng();
|
||||
let length = rng.random_range(min_chars..=max_chars);
|
||||
|
||||
let words = [
|
||||
"lorem",
|
||||
"ipsum",
|
||||
"dolor",
|
||||
"sit",
|
||||
"amet",
|
||||
"consectetur",
|
||||
"adipiscing",
|
||||
"elit",
|
||||
"sed",
|
||||
"do",
|
||||
"eiusmod",
|
||||
"tempor",
|
||||
"incididunt",
|
||||
"ut",
|
||||
"labore",
|
||||
"et",
|
||||
"dolore",
|
||||
"magna",
|
||||
"aliqua",
|
||||
"ut",
|
||||
"enim",
|
||||
"ad",
|
||||
"minim",
|
||||
"veniam",
|
||||
"quis",
|
||||
"nostrud",
|
||||
"exercitation",
|
||||
"ullamco",
|
||||
"laboris",
|
||||
"nisi",
|
||||
"ut",
|
||||
"aliquip",
|
||||
"ex",
|
||||
"ea",
|
||||
"commodo",
|
||||
"consequat",
|
||||
];
|
||||
|
||||
let mut content = String::with_capacity(length);
|
||||
|
||||
while content.len() < length {
|
||||
let word_idx = rng.random_range(0..words.len());
|
||||
if !content.is_empty() {
|
||||
content.push(' ');
|
||||
}
|
||||
if rng.random_ratio(1, 10) {
|
||||
content.push('.');
|
||||
let word = words[word_idx];
|
||||
let mut chars = word.chars();
|
||||
if let Some(first_char) = chars.next() {
|
||||
content.push_str(&first_char.to_uppercase().to_string());
|
||||
content.push_str(chars.as_str());
|
||||
}
|
||||
} else {
|
||||
content.push_str(words[word_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
if !content.ends_with('.') {
|
||||
content.push('.');
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
fn generate_random_name(length: usize) -> String {
|
||||
let mut rng = rng();
|
||||
(0..length)
|
||||
.map(|_| rng.sample(Alphanumeric) as char)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -136,12 +136,14 @@ pub async fn test(test: &WebDavTest) {
|
||||
while chunky_contents.len() < max_size {
|
||||
chunky_contents.push_str(contents);
|
||||
}
|
||||
let response = client.request("PUT", path, chunky_contents).await;
|
||||
response.with_status(
|
||||
expect
|
||||
.map(|_| StatusCode::PRECONDITION_FAILED)
|
||||
.unwrap_or(StatusCode::PAYLOAD_TOO_LARGE),
|
||||
);
|
||||
let response = client
|
||||
.request("PUT", path, chunky_contents)
|
||||
.await
|
||||
.with_status(
|
||||
expect
|
||||
.map(|_| StatusCode::PRECONDITION_FAILED)
|
||||
.unwrap_or(StatusCode::PAYLOAD_TOO_LARGE),
|
||||
);
|
||||
if let Some(expect) = expect {
|
||||
response.with_failed_precondition(expect, &max_size.to_string());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user