WebDAV PROPFIND/PROPPATCH tests
This commit is contained in:
@@ -13,6 +13,7 @@ use hyper::StatusCode;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
let client = test.client("jane");
|
||||
let mike_noquota = test.client("mike");
|
||||
|
||||
for resource_type in [
|
||||
DavResourceName::File,
|
||||
@@ -649,10 +650,72 @@ pub async fn test(test: &WebDavTest) {
|
||||
.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/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/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/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").await;
|
||||
mike_noquota.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use hyper::StatusCode;
|
||||
|
||||
use crate::webdav::{TEST_FILE_1, TEST_ICAL_1, TEST_VCARD_1, TEST_VTIMEZONE_1};
|
||||
|
||||
use super::WebDavTest;
|
||||
use super::{DavResponse, DummyWebDavClient, WebDavTest};
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
println!("Running MKCOL tests...");
|
||||
@@ -102,7 +102,7 @@ pub async fn test(test: &WebDavTest) {
|
||||
}
|
||||
|
||||
// Create using extended MKCOL
|
||||
for (path, properties, resource_types) in [
|
||||
for (path, expected_properties, resource_types) in [
|
||||
(
|
||||
"/dav/file/john/my-named-files/",
|
||||
[("D:displayname", "Named Files")].as_slice(),
|
||||
@@ -131,38 +131,34 @@ pub async fn test(test: &WebDavTest) {
|
||||
["D:collection", "A:calendar"].as_slice(),
|
||||
),
|
||||
] {
|
||||
let mut response = client
|
||||
let response = client
|
||||
.mkcol(
|
||||
"MKCOL",
|
||||
path,
|
||||
resource_types.iter().copied(),
|
||||
properties.iter().copied(),
|
||||
expected_properties.iter().copied(),
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED)
|
||||
.match_many("D:mkcol-response.D:propstat.D:status", ["HTTP/1.1 200 OK"]);
|
||||
for (property, _) in properties {
|
||||
response = response.match_one(
|
||||
&format!("D:mkcol-response.D:propstat.D:prop.{property}"),
|
||||
"",
|
||||
);
|
||||
.into_propfind_response("D:mkcol-response".into());
|
||||
let properties = response.properties("");
|
||||
for (property, _) in expected_properties {
|
||||
properties
|
||||
.get(property)
|
||||
.with_status(StatusCode::OK)
|
||||
.with_values([""]);
|
||||
}
|
||||
|
||||
// Check the properties of the created collection
|
||||
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(
|
||||
"D:multistatus.D:response.D:propstat.D:status",
|
||||
"HTTP/1.1 200 OK",
|
||||
);
|
||||
for (property, value) in properties {
|
||||
response = response.match_one(
|
||||
&format!("D:multistatus.D:response.D:propstat.D:prop.{property}"),
|
||||
value,
|
||||
);
|
||||
let response = client
|
||||
.propfind(path, expected_properties.iter().map(|x| x.0))
|
||||
.await;
|
||||
let properties = response.properties(path);
|
||||
for (property, value) in expected_properties {
|
||||
properties
|
||||
.get(property)
|
||||
.with_status(StatusCode::OK)
|
||||
.with_values([*value]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,3 +196,44 @@ pub async fn test(test: &WebDavTest) {
|
||||
client.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub async fn mkcol(
|
||||
&self,
|
||||
method: &str,
|
||||
path: &str,
|
||||
resource_types: impl IntoIterator<Item = &str>,
|
||||
properties: impl IntoIterator<Item = (&str, &str)>,
|
||||
) -> DavResponse {
|
||||
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>"
|
||||
)
|
||||
.to_string();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if has_resource_type {
|
||||
request.push_str("</D:resourcetype>");
|
||||
}
|
||||
|
||||
for (key, value) in properties {
|
||||
request.push_str(&format!("<{key}>{value}</{key}>"));
|
||||
}
|
||||
request.push_str("</D:prop></D:set></D:mkcol>");
|
||||
|
||||
if method == "MKCALENDAR" {
|
||||
request = request.replace("D:mkcol", "A:mkcalendar");
|
||||
}
|
||||
|
||||
self.request(method, path, &request).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
};
|
||||
use ::managesieve::core::ManageSieveSessionManager;
|
||||
use ::store::Stores;
|
||||
use ahash::AHashMap;
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use common::{
|
||||
Caches, Core, Data, DavResource, DavResources, Inner, Server,
|
||||
@@ -21,7 +21,10 @@ use common::{
|
||||
core::BuildServer,
|
||||
manager::boot::build_ipc,
|
||||
};
|
||||
use dav_proto::Depth;
|
||||
use dav_proto::{
|
||||
Depth,
|
||||
schema::property::{DavProperty, WebDavProperty},
|
||||
};
|
||||
use groupware::{DavResourceName, hierarchy::DavHierarchy};
|
||||
use http::HttpSessionManager;
|
||||
use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION};
|
||||
@@ -44,6 +47,7 @@ use utils::config::Config;
|
||||
pub mod basic;
|
||||
pub mod copy_move;
|
||||
pub mod mkcol;
|
||||
pub mod prop;
|
||||
pub mod put_get;
|
||||
|
||||
const SERVER: &str = r#"
|
||||
@@ -316,7 +320,7 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest
|
||||
("john", "secret2", "John Doe", "jdoe@example.com"),
|
||||
("jane", "secret3", "Jane Smith", "jane.smith@example.com"),
|
||||
("bill", "secret4", "Bill Foobar", "bill@example,com"),
|
||||
("mike", "secret5", "Mile Noquota", "mike@example,com"),
|
||||
("mike", "secret5", "Mike Noquota", "mike@example,com"),
|
||||
] {
|
||||
let account_id = store
|
||||
.create_test_user(account, secret, name, &[email])
|
||||
@@ -326,7 +330,7 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest
|
||||
DummyWebDavClient::new(account_id, account, secret, email),
|
||||
);
|
||||
if account == "mike" {
|
||||
store.set_test_quota(account, 10).await;
|
||||
store.set_test_quota(account, 1024).await;
|
||||
}
|
||||
}
|
||||
store
|
||||
@@ -358,6 +362,7 @@ pub async fn webdav_tests() {
|
||||
put_get::test(&handle).await;
|
||||
mkcol::test(&handle).await;
|
||||
copy_move::test(&handle).await;
|
||||
prop::test(&handle).await;
|
||||
|
||||
// Print elapsed time
|
||||
let elapsed = start_time.elapsed();
|
||||
@@ -400,6 +405,7 @@ pub struct DummyWebDavClient {
|
||||
credentials: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DavResponse {
|
||||
headers: AHashMap<String, String>,
|
||||
status: StatusCode,
|
||||
@@ -488,66 +494,6 @@ impl DummyWebDavClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mkcol(
|
||||
&self,
|
||||
method: &str,
|
||||
path: &str,
|
||||
resource_types: impl IntoIterator<Item = &str>,
|
||||
properties: impl IntoIterator<Item = (&str, &str)>,
|
||||
) -> DavResponse {
|
||||
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>"
|
||||
)
|
||||
.to_string();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if has_resource_type {
|
||||
request.push_str("</D:resourcetype>");
|
||||
}
|
||||
|
||||
for (key, value) in properties {
|
||||
request.push_str(&format!("<{key}>{value}</{key}>"));
|
||||
}
|
||||
request.push_str("</D:prop></D:set></D:mkcol>");
|
||||
|
||||
if method == "MKCALENDAR" {
|
||||
request = request.replace("D:mkcol", "A:mkcalendar");
|
||||
}
|
||||
|
||||
self.request(method, path, &request).await
|
||||
}
|
||||
|
||||
pub async fn propfind(
|
||||
&self,
|
||||
path: &str,
|
||||
properties: impl IntoIterator<Item = &str>,
|
||||
) -> DavResponse {
|
||||
let mut request = concat!(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
|
||||
"<D:propfind 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:propfind>");
|
||||
|
||||
self.request("PROPFIND", path, &request).await
|
||||
}
|
||||
|
||||
pub async fn sync_collection(
|
||||
&self,
|
||||
path: &str,
|
||||
@@ -581,6 +527,19 @@ impl DummyWebDavClient {
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
}
|
||||
|
||||
pub async fn available_quota(&self, path: &str) -> u64 {
|
||||
self.propfind(
|
||||
path,
|
||||
[DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes)],
|
||||
)
|
||||
.await
|
||||
.properties(path)
|
||||
.get(DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes))
|
||||
.value()
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create_hierarchy(
|
||||
&self,
|
||||
base_path: &str,
|
||||
@@ -782,6 +741,26 @@ impl DavResponse {
|
||||
hrefs
|
||||
}
|
||||
|
||||
pub fn with_hrefs<'x>(self, hrefs: impl IntoIterator<Item = &'x str>) -> Self {
|
||||
let expected_hrefs = hrefs.into_iter().collect::<AHashSet<_>>();
|
||||
let hrefs = self
|
||||
.find_keys("D:multistatus.D:response.D:href")
|
||||
.collect::<AHashSet<_>>();
|
||||
if expected_hrefs != hrefs {
|
||||
self.dump_response();
|
||||
|
||||
println!("\nMissing: {:?}", expected_hrefs.difference(&hrefs));
|
||||
println!("\nExtra: {:?}", hrefs.difference(&expected_hrefs));
|
||||
|
||||
panic!(
|
||||
"Hierarchy mismatch: expected {} items, received {} items",
|
||||
expected_hrefs.len(),
|
||||
hrefs.len()
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn dump_response(&self) {
|
||||
eprintln!("-------------------------------------");
|
||||
eprintln!("Status: {}", self.status);
|
||||
@@ -868,19 +847,34 @@ fn flatten_xml(xml: &str) -> Vec<(String, String)> {
|
||||
Event::Start(ref e) => {
|
||||
let name = str::from_utf8(e.name().as_ref()).unwrap().to_string();
|
||||
path.push(name);
|
||||
let base_path = path.join(".");
|
||||
for attr in e.attributes() {
|
||||
let attr = attr.unwrap();
|
||||
let key = str::from_utf8(attr.key.as_ref()).unwrap().to_string();
|
||||
let value = attr.unescape_value().unwrap();
|
||||
let value_str = value.trim().to_string();
|
||||
|
||||
result.push((format!("{}.[{}]", path.join("."), key), value_str));
|
||||
result.push((format!("{}.[{}]", base_path, key), value_str));
|
||||
}
|
||||
text_content = None;
|
||||
}
|
||||
Event::Empty(ref e) => {
|
||||
let name = str::from_utf8(e.name().as_ref()).unwrap().to_string();
|
||||
result.push((format!("{}.{}", path.join("."), name), "".to_string()));
|
||||
let base_path = format!("{}.{}", path.join("."), name);
|
||||
let mut has_attrs = false;
|
||||
|
||||
for attr in e.attributes() {
|
||||
let attr = attr.unwrap();
|
||||
let key = str::from_utf8(attr.key.as_ref()).unwrap().to_string();
|
||||
let value = attr.unescape_value().unwrap();
|
||||
let value_str = value.trim().to_string();
|
||||
has_attrs = true;
|
||||
result.push((format!("{}.[{}]", base_path, key), value_str));
|
||||
}
|
||||
|
||||
if !has_attrs {
|
||||
result.push((base_path, "".to_string()));
|
||||
}
|
||||
}
|
||||
Event::Text(e) => {
|
||||
let text = e.unescape().unwrap();
|
||||
|
||||
1033
tests/src/webdav/prop.rs
Normal file
1033
tests/src/webdav/prop.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -151,24 +151,48 @@ pub async fn test(test: &WebDavTest) {
|
||||
|
||||
// PUT requests cannot exceed quota
|
||||
let mike_noquota = test.client("mike");
|
||||
for (path, ct, content) in [
|
||||
("/dav/file/mike/file1.txt", "text/plain", TEST_FILE_1),
|
||||
(
|
||||
"/dav/card/mike/default/card1.vcf",
|
||||
"text/vcard; charset=utf-8",
|
||||
TEST_VCARD_1,
|
||||
),
|
||||
(
|
||||
"/dav/cal/mike/default/event1.ics",
|
||||
"text/calendar; charset=utf-8",
|
||||
TEST_ICAL_1,
|
||||
),
|
||||
for resource_type in [
|
||||
DavResourceName::File,
|
||||
DavResourceName::Card,
|
||||
DavResourceName::Cal,
|
||||
] {
|
||||
let path = format!("{}/mike/quota-test/", resource_type.base_path());
|
||||
mike_noquota
|
||||
.request_with_headers("PUT", path, [("content-type", ct)], content)
|
||||
.mkcol("MKCOL", &path, [], [])
|
||||
.await
|
||||
.with_status(StatusCode::PRECONDITION_FAILED)
|
||||
.with_failed_precondition("D:quota-not-exceeded", "");
|
||||
.with_status(StatusCode::CREATED);
|
||||
let mut num_success = 0;
|
||||
let mut did_fail = false;
|
||||
|
||||
for i in 0..100 {
|
||||
let content = resource_type.generate();
|
||||
let available = mike_noquota.available_quota(&path).await;
|
||||
|
||||
let response = mike_noquota
|
||||
.request_with_headers("PUT", &format!("{path}file{i}"), [], &content)
|
||||
.await;
|
||||
if available > content.len() as u64 {
|
||||
num_success += 1;
|
||||
response.with_status(StatusCode::CREATED);
|
||||
} else {
|
||||
response
|
||||
.with_status(StatusCode::PRECONDITION_FAILED)
|
||||
.with_failed_precondition("D:quota-not-exceeded", "");
|
||||
did_fail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// PUT precondition enforcement
|
||||
|
||||
Reference in New Issue
Block a user