WebDAV PROPFIND/PROPPATCH tests

This commit is contained in:
mdecimus
2025-05-02 15:43:53 +02:00
parent 5416e35d4d
commit 2b9e0816eb
15 changed files with 1311 additions and 142 deletions

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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