mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-09-11 22:44:29 +08:00
WebDAV PROPFIND/PROPPATCH tests
This commit is contained in:
parent
5416e35d4d
commit
2b9e0816eb
15 changed files with 1311 additions and 142 deletions
|
@ -127,6 +127,10 @@ impl ArchivedDeadProperty {
|
|||
}
|
||||
|
||||
impl DeadElementTag {
|
||||
pub fn new(name: String, attrs: Option<String>) -> Self {
|
||||
DeadElementTag { name, attrs }
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len())
|
||||
}
|
||||
|
|
|
@ -228,6 +228,12 @@ impl DavProperty {
|
|||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for DavProperty {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.tag_name().0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ReportSet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("<D:supported-report><D:report>")?;
|
||||
|
|
|
@ -316,7 +316,7 @@ impl Rfc1123DateTime {
|
|||
}
|
||||
|
||||
impl DavProperty {
|
||||
pub const ALL_PROPS: [DavProperty; 17] = [
|
||||
pub const ALL_PROPS: [DavProperty; 11] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
|
@ -325,15 +325,9 @@ impl DavProperty {
|
|||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::AclRestrictions),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLength),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
];
|
||||
|
||||
pub fn is_all_prop(&self) -> bool {
|
||||
|
@ -347,15 +341,9 @@ impl DavProperty {
|
|||
| DavProperty::WebDav(WebDavProperty::LockDiscovery)
|
||||
| DavProperty::WebDav(WebDavProperty::SupportedLock)
|
||||
| DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)
|
||||
| DavProperty::WebDav(WebDavProperty::SyncToken)
|
||||
| DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet)
|
||||
| DavProperty::WebDav(WebDavProperty::AclRestrictions)
|
||||
| DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet)
|
||||
| DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentLanguage)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentLength)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentType)
|
||||
| DavProperty::WebDav(WebDavProperty::SupportedReportSet)
|
||||
| DavProperty::DeadProperty(_)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ use trc::AddContext;
|
|||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
|
@ -130,17 +131,20 @@ impl CalendarMkColRequestHandler for Server {
|
|||
calendar
|
||||
.insert(access_token, account_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.with_mkcalendar(is_mkcalendar)
|
||||
.to_string(),
|
||||
))
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CalDav)
|
||||
.with_mkcalendar(is_mkcalendar)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED))
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -326,6 +326,7 @@ impl CalendarPropPatchRequestHandler for Server {
|
|||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
calendar.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
|
@ -408,6 +409,7 @@ impl CalendarPropPatchRequestHandler for Server {
|
|||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
event.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
|
|
|
@ -8,6 +8,7 @@ use super::proppatch::CardPropPatchRequestHandler;
|
|||
use crate::{
|
||||
DavError, DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
},
|
||||
|
@ -110,16 +111,19 @@ impl CardMkColRequestHandler for Server {
|
|||
.caused_by(trc::location!())?;
|
||||
book.insert(access_token, account_id, document_id, &mut batch)
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
))
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::CardDav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED))
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,6 +273,7 @@ impl CardPropPatchRequestHandler for Server {
|
|||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
address_book.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
|
@ -354,6 +355,7 @@ impl CardPropPatchRequestHandler for Server {
|
|||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
card.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
|
||||
if self.core.groupware.dead_property_size.is_some() =>
|
||||
|
|
|
@ -129,6 +129,7 @@ impl PropFindRequestHandler for Server {
|
|||
Depth::Zero => false,
|
||||
Depth::Infinity => {
|
||||
if resource.account_id.is_none()
|
||||
|| resource.resource.is_none()
|
||||
|| matches!(resource.collection, Collection::FileNode)
|
||||
{
|
||||
return Err(DavErrorCondition::new(
|
||||
|
@ -1009,7 +1010,7 @@ impl PropFindRequestHandler for Server {
|
|||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Unbind,
|
||||
"Add resources to a collection",
|
||||
"Remove resources from a collection",
|
||||
))
|
||||
.with_supported_privilege(SupportedPrivilege::new(
|
||||
Privilege::Unlock,
|
||||
|
@ -1193,8 +1194,10 @@ impl PropFindRequestHandler for Server {
|
|||
if let ArchivedTimezone::IANA(tz) =
|
||||
&calendar.inner.preferences(account_id).time_zone
|
||||
{
|
||||
fields
|
||||
.push(DavPropertyValue::new(property.clone(), tz.to_string()));
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
Tz::from_id(tz.to_native()).unwrap_or(Tz::UTC).to_string(),
|
||||
));
|
||||
} else {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
|
@ -1238,13 +1241,13 @@ impl PropFindRequestHandler for Server {
|
|||
(CalDavProperty::MinDateTime, ArchivedResource::Calendar(_)) => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
DavValue::Timestamp(i64::MIN),
|
||||
DavValue::String("0001-01-01T00:00:00Z".to_string()),
|
||||
));
|
||||
}
|
||||
(CalDavProperty::MaxDateTime, ArchivedResource::Calendar(_)) => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
DavValue::Timestamp(32531605200),
|
||||
DavValue::String("9999-12-31T23:59:59Z".to_string()),
|
||||
));
|
||||
}
|
||||
(CalDavProperty::MaxInstances, ArchivedResource::Calendar(_)) => {
|
||||
|
|
|
@ -19,6 +19,7 @@ use trc::AddContext;
|
|||
use crate::{
|
||||
DavMethod, PropStatBuilder,
|
||||
common::{
|
||||
ExtractETag,
|
||||
acl::DavAclHandler,
|
||||
lock::{LockRequestHandler, ResourceState},
|
||||
uri::DavUriResource,
|
||||
|
@ -125,16 +126,19 @@ impl FileMkColRequestHandler for Server {
|
|||
.create_document(document_id)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(node))
|
||||
.caused_by(trc::location!())?;
|
||||
let etag = batch.etag();
|
||||
self.commit_batch(batch).await.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(prop_stat) = return_prop_stat {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::Dav)
|
||||
.to_string(),
|
||||
))
|
||||
Ok(HttpResponse::new(StatusCode::CREATED)
|
||||
.with_xml_body(
|
||||
MkColResponse::new(prop_stat.build())
|
||||
.with_namespace(Namespace::Dav)
|
||||
.to_string(),
|
||||
)
|
||||
.with_etag_opt(etag))
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::CREATED))
|
||||
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,6 +186,7 @@ impl FilePropPatchRequestHandler for Server {
|
|||
}
|
||||
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
|
||||
file.created = dt;
|
||||
items.insert_ok(property.property);
|
||||
}
|
||||
(DavProperty::WebDav(WebDavProperty::GetContentType), DavValue::String(name))
|
||||
if file.file.is_some() =>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue