WebDAV REPORT expand-property, principal-*

This commit is contained in:
mdecimus
2025-05-03 17:34:41 +02:00
parent d72ae41058
commit 9b002c2d3d
8 changed files with 640 additions and 98 deletions

View File

@@ -81,8 +81,15 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::CREATED);
// Test 5: Creating a file under a locked resource without a lock token should fail
// Test 5: Creating a lock under an infinity locked resource should fail
let file_path = format!("{path}/file.txt");
client
.lock_create(&file_path, "super-owner", true, "0", "Second-123")
.await
.with_status(StatusCode::LOCKED)
.with_value("D:error.D:lock-token-submitted.D:href", &path);
// Test 6: Creating a file under a locked resource without a lock token should fail
let contents = resource_type.generate();
client
.request("PUT", &file_path, &contents)
@@ -90,7 +97,7 @@ pub async fn test(test: &WebDavTest) {
.with_status(StatusCode::LOCKED)
.with_value("D:error.D:lock-token-submitted.D:href", &path);
// Test 6: Creating a file under a locked resource with a lock token should succeed
// Test 7: Creating a file under a locked resource with a lock token should succeed
client
.request_with_headers(
"PUT",
@@ -101,7 +108,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::CREATED);
// Test 7: Locks should be included in propfind responses
// Test 8: Locks should be included in propfind responses
let response = client
.propfind(&path, [DavProperty::WebDav(WebDavProperty::LockDiscovery)])
.await;
@@ -120,7 +127,7 @@ pub async fn test(test: &WebDavTest) {
]);
}
// Test 8: Delete with and without a lock token
// Test 9: Delete with and without a lock token
client
.request("DELETE", &path, "")
.await
@@ -136,7 +143,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::NO_CONTENT);
// Test 9: Unlock with and without a lock token
// Test 10: Unlock with and without a lock token
client
.unlock(&path, "urn:stalwart:davlock:1234")
.await
@@ -147,7 +154,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::NO_CONTENT);
// Test 10: Locking with a large dead property should fail
// Test 11: Locking with a large dead property should fail
let path = format!("{base_path}/invalid-lock");
client
.lock_create(
@@ -163,7 +170,7 @@ pub async fn test(test: &WebDavTest) {
.await
.with_status(StatusCode::PAYLOAD_TOO_LARGE);
// Test 11: Too many locks should fail
// Test 12: Too many locks should fail
for i in 0..test.server.core.groupware.max_locks_per_user {
client
.lock_create(

View File

@@ -49,6 +49,7 @@ pub mod copy_move;
pub mod lock;
pub mod mkcol;
pub mod multiget;
pub mod principals;
pub mod prop;
pub mod put_get;
pub mod sync;
@@ -218,6 +219,19 @@ disabled-events = ["network.*"]
"#;
pub const TEST_DAV_USERS: &[(&str, &str, &str, &str)] = &[
("admin", "secret1", "Superuser", "admin@example,com"),
("john", "secret2", "John Doe", "jdoe@example.com"),
(
"jane",
"secret3",
"Jane Doe-Smith",
"jane.smith@example.com",
),
("bill", "secret4", "Bill Foobar", "bill@example,com"),
("mike", "secret5", "Mike Noquota", "mike@example,com"),
];
#[allow(dead_code)]
pub struct WebDavTest {
server: Server,
@@ -318,21 +332,15 @@ async fn init_webdav_tests(store_id: &str, delete_if_exists: bool) -> WebDavTest
// Create test accounts
let mut clients = AHashMap::new();
for (account, secret, name, email) in [
("admin", "secret1", "Superuser", "admin@example,com"),
("john", "secret2", "John Doe", "jdoe@example.com"),
("jane", "secret3", "Jane Smith", "jane.smith@example.com"),
("bill", "secret4", "Bill Foobar", "bill@example,com"),
("mike", "secret5", "Mike Noquota", "mike@example,com"),
] {
for (account, secret, name, email) in TEST_DAV_USERS {
let account_id = store
.create_test_user(account, secret, name, &[email])
.await;
clients.insert(
account,
*account,
DummyWebDavClient::new(account_id, account, secret, email),
);
if account == "mike" {
if *account == "mike" {
store.set_test_quota(account, 1024).await;
}
}
@@ -364,11 +372,6 @@ pub async fn webdav_tests() {
/*
TODO:
- Principals:
- PrincipalMatch
- PrincipalPropertySearch
- PrincipalSearchPropertySet
- Expand property
- ACLs:
- ACL Method
- AclPrincipalPropSet
@@ -387,6 +390,7 @@ pub async fn webdav_tests() {
multiget::test(&handle).await;
sync::test(&handle).await;
lock::test(&handle).await;
principals::test(&handle).await;
// Print elapsed time
let elapsed = start_time.elapsed();

View File

@@ -0,0 +1,356 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::WebDavTest;
use crate::webdav::{TEST_DAV_USERS, prop::ALL_DAV_PROPERTIES};
use dav_proto::schema::property::{DavProperty, PrincipalProperty, WebDavProperty};
use groupware::DavResourceName;
use hyper::StatusCode;
pub async fn test(test: &WebDavTest) {
println!("Running principals tests...");
let client = test.client("jane");
let principal_path = format!("D:href:{}/", DavResourceName::Principal.base_path());
let jane_principal_path = format!("D:href:{}/jane/", DavResourceName::Principal.base_path());
// Test 1: PROPFIND on /dav/pal should return all principals
let response = client
.propfind(
DavResourceName::Principal.collection_path(),
ALL_DAV_PROPERTIES,
)
.await;
for (account, _, name, _) in TEST_DAV_USERS {
let props = response.properties(&format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
account
));
let path_pal = format!(
"D:href:{}/{}/",
DavResourceName::Principal.base_path(),
account
);
let path_card = format!("D:href:{}/{}/", DavResourceName::Card.base_path(), account);
let path_cal = format!("D:href:{}/{}/", DavResourceName::Cal.base_path(), account);
props
.get(DavProperty::WebDav(WebDavProperty::DisplayName))
.with_values([*name])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal))
.with_values([jane_principal_path.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(PrincipalProperty::PrincipalURL))
.with_values([path_pal.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::Owner))
.with_values([path_pal.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet))
.with_values([path_cal.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(
PrincipalProperty::AddressbookHomeSet,
))
.with_values([path_card.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet))
.with_values([principal_path.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::SupportedReportSet))
.with_values([
"D:supported-report.D:report.D:principal-property-search",
"D:supported-report.D:report.D:principal-search-property-set",
"D:supported-report.D:report.D:principal-match",
])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::ResourceType))
.with_values(["D:principal", "D:collection"])
.with_status(StatusCode::OK);
}
// Test 2: PROPFIND on /dav/[resource] should return user and shared resources
for resource_type in [
DavResourceName::File,
DavResourceName::Cal,
DavResourceName::Card,
] {
let supported_reports = match resource_type {
DavResourceName::File => [
"D:supported-report.D:report.D:sync-collection",
"D:supported-report.D:report.D:acl-principal-prop-set",
"D:supported-report.D:report.D:principal-match",
]
.as_slice(),
DavResourceName::Cal => [
"D:supported-report.D:report.A:free-busy-query",
"D:supported-report.D:report.A:calendar-query",
"D:supported-report.D:report.D:expand-property",
"D:supported-report.D:report.D:sync-collection",
"D:supported-report.D:report.D:acl-principal-prop-set",
"D:supported-report.D:report.D:principal-match",
"D:supported-report.D:report.A:calendar-multiget",
]
.as_slice(),
DavResourceName::Card => [
"D:supported-report.D:report.B:addressbook-query",
"D:supported-report.D:report.D:acl-principal-prop-set",
"D:supported-report.D:report.D:expand-property",
"D:supported-report.D:report.B:addressbook-multiget",
"D:supported-report.D:report.D:principal-match",
"D:supported-report.D:report.D:sync-collection",
]
.as_slice(),
_ => unreachable!(),
};
let privilege_set = if resource_type == DavResourceName::Cal {
[
"D:privilege.D:read-current-user-privilege-set",
"D:privilege.D:write-acl",
"D:privilege.A:read-free-busy",
"D:privilege.D:read-acl",
"D:privilege.D:write-properties",
"D:privilege.D:write",
"D:privilege.D:write-content",
"D:privilege.D:unlock",
"D:privilege.D:all",
"D:privilege.D:read",
"D:privilege.D:bind",
"D:privilege.D:unbind",
]
.as_slice()
} else {
[
"D:privilege.D:all",
"D:privilege.D:read",
"D:privilege.D:write",
"D:privilege.D:write-properties",
"D:privilege.D:write-content",
"D:privilege.D:unlock",
"D:privilege.D:read-acl",
"D:privilege.D:read-current-user-privilege-set",
"D:privilege.D:write-acl",
"D:privilege.D:bind",
"D:privilege.D:unbind",
]
.as_slice()
};
let response = client
.propfind(resource_type.collection_path(), ALL_DAV_PROPERTIES)
.await;
let props = response.properties(resource_type.collection_path());
props
.get(DavProperty::WebDav(WebDavProperty::SupportedReportSet))
.with_values(supported_reports.iter().copied())
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::ResourceType))
.with_values(["D:collection"])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal))
.with_values([jane_principal_path.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet))
.with_values([format!("D:href:{}/jane/", DavResourceName::Cal.base_path()).as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(
PrincipalProperty::AddressbookHomeSet,
))
.with_values([format!("D:href:{}/jane/", DavResourceName::Card.base_path()).as_str()])
.with_status(StatusCode::OK);
for (account, _, name, _) in TEST_DAV_USERS
.iter()
.filter(|(account, _, _, _)| ["jane", "support"].contains(account))
{
let path_card = format!("D:href:{}/{}/", DavResourceName::Card.base_path(), account);
let path_cal = format!("D:href:{}/{}/", DavResourceName::Cal.base_path(), account);
let path_pal = format!(
"D:href:{}/{}/",
DavResourceName::Principal.base_path(),
account
);
let props = response.properties(&format!("{}/{account}/", resource_type.base_path()));
props
.get(DavProperty::WebDav(WebDavProperty::DisplayName))
.with_values([*name])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::ResourceType))
.with_values(["D:collection"])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal))
.with_values([jane_principal_path.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet))
.with_values(privilege_set.iter().copied())
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::SupportedReportSet))
.with_values(supported_reports.iter().copied())
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(PrincipalProperty::PrincipalURL))
.with_values([path_pal.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet))
.with_values([principal_path.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::Owner))
.with_values([path_pal.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet))
.with_values([path_cal.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::Principal(
PrincipalProperty::AddressbookHomeSet,
))
.with_values([path_card.as_str()])
.with_status(StatusCode::OK);
props
.get(DavProperty::WebDav(WebDavProperty::SyncToken))
.with_status(StatusCode::OK)
.is_not_empty();
props
.get(DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes))
.with_status(StatusCode::OK)
.is_not_empty();
props
.get(DavProperty::WebDav(WebDavProperty::QuotaUsedBytes))
.with_status(StatusCode::OK)
.is_not_empty();
}
// Test 3: principal-match-query on resources
let response = client
.request(
"REPORT",
resource_type.collection_path(),
PRINCIPAL_MATCH_QUERY,
)
.await
.with_status(StatusCode::MULTI_STATUS)
.into_propfind_response(None);
response.with_hrefs([
format!("{}/jane/", resource_type.base_path()).as_str(),
format!("{}/support/", resource_type.base_path()).as_str(),
]);
}
// Test 4: principal-match-query on principals
let response = client
.request(
"REPORT",
DavResourceName::Principal.collection_path(),
PRINCIPAL_MATCH_QUERY,
)
.await
.with_status(StatusCode::MULTI_STATUS)
.into_propfind_response(None);
response.with_hrefs([
format!("{}/jane/", DavResourceName::Principal.base_path()).as_str(),
format!("{}/support/", DavResourceName::Principal.base_path()).as_str(),
]);
// Test 5: principal-search-property-set REPORT
let response = client
.request(
"REPORT",
DavResourceName::Principal.collection_path(),
PRINCIPAL_SEARCH_PROPERTY_SET_QUERY,
)
.await
.with_status(StatusCode::OK);
response
.with_value(
"D:principal-search-property-set.D:principal-search-property.D:prop.D:displayname",
"",
)
.with_value(
"D:principal-search-property-set.D:principal-search-property.D:description",
"Account or Group name",
);
// Test 6: principal-property-search REPORT
let response = client
.request(
"REPORT",
DavResourceName::Principal.collection_path(),
PRINCIPAL_PROPERTY_SEARCH_QUERY.replace("$NAME", "doe"),
)
.await
.with_status(StatusCode::MULTI_STATUS)
.into_propfind_response(None);
response.with_hrefs([
format!("{}/jane/", DavResourceName::Principal.base_path()).as_str(),
format!("{}/john/", DavResourceName::Principal.base_path()).as_str(),
]);
response
.properties(&format!("{}/jane/", DavResourceName::Principal.base_path()))
.get(DavProperty::WebDav(WebDavProperty::DisplayName))
.with_values([TEST_DAV_USERS
.iter()
.find(|(account, _, _, _)| *account == "jane")
.unwrap()
.2])
.with_status(StatusCode::OK);
client
.request(
"REPORT",
DavResourceName::Principal.collection_path(),
PRINCIPAL_PROPERTY_SEARCH_QUERY.replace("$NAME", "support"),
)
.await
.with_status(StatusCode::MULTI_STATUS)
.into_propfind_response(None)
.with_hrefs([format!("{}/support/", DavResourceName::Principal.base_path()).as_str()]);
client.delete_default_containers().await;
test.assert_is_empty().await;
}
const PRINCIPAL_MATCH_QUERY: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<D:principal-match xmlns:D="DAV:">
<D:principal-property>
<D:owner/>
<D:displayname/>
</D:principal-property>
</D:principal-match>"#;
const PRINCIPAL_SEARCH_PROPERTY_SET_QUERY: &str =
r#"<?xml version="1.0" encoding="utf-8" ?><D:principal-search-property-set xmlns:D="DAV:"/>"#;
const PRINCIPAL_PROPERTY_SEARCH_QUERY: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<D:principal-property-search xmlns:D="DAV:">
<D:property-search>
<D:prop>
<D:displayname/>
</D:prop>
<D:match>$NAME</D:match>
</D:property-search>
<D:prop xmlns:B="http://www.example.com/ns/">
<D:displayname/>
</D:prop>
</D:principal-property-search>"#;

View File

@@ -85,20 +85,23 @@ pub async fn test(test: &WebDavTest) {
}
// Test 5: PROPFIND Depth 1 on user base path
let response = client
client
.request_with_headers("PROPFIND", &user_base_path, [("depth", "1")], "")
.await;
if resource_type != DavResourceName::File {
response.with_status(StatusCode::MULTI_STATUS).with_hrefs([
format!("{user_base_path}/").as_str(),
format!("{user_base_path}/default/").as_str(),
&test_base_path,
]);
} else {
response
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([format!("{user_base_path}/").as_str(), &test_base_path]);
}
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs(
[
format!("{user_base_path}/default/").as_str(),
format!("{user_base_path}/").as_str(),
&test_base_path,
]
.into_iter()
.skip(if resource_type == DavResourceName::File {
1
} else {
0
}),
);
// Test 6: PROPFIND Depth 1 on created collection
client
@@ -117,7 +120,60 @@ pub async fn test(test: &WebDavTest) {
StatusCode::MULTI_STATUS
});
// Test 8: Retrieve all static properties
// Test 8 PROPFIND with depth-no-root
client
.request_with_headers(
"PROPFIND",
&user_base_path,
[("depth", "1"), ("prefer", "depth-noroot")],
"",
)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs(
[
format!("{user_base_path}/default/").as_str(),
&test_base_path,
]
.into_iter()
.skip(if resource_type == DavResourceName::File {
1
} else {
0
}),
);
client
.request_with_headers(
"PROPFIND",
&test_base_path,
[("depth", "1"), ("prefer", "depth-noroot")],
"",
)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([test_path.as_str()]);
// Test 8 PROPFIND with prefer return=minimal
let response = client
.propfind_with_headers(&test_base_path, ALL_DAV_PROPERTIES, [])
.await;
response
.properties(&test_base_path)
.is_defined(DavProperty::WebDav(WebDavProperty::GetETag))
.is_defined(DavProperty::Principal(PrincipalProperty::GroupMembership));
let response = client
.propfind_with_headers(
&test_base_path,
ALL_DAV_PROPERTIES,
[("prefer", "return=minimal")],
)
.await;
response
.properties(&test_base_path)
.is_defined(DavProperty::WebDav(WebDavProperty::GetETag))
.is_undefined(DavProperty::Principal(PrincipalProperty::GroupMembership));
// Test 9: Retrieve all static properties
for (path, etag, is_file) in [
(&test_base_path, &etag_folder, false),
(&test_path, &etag_file, true),
@@ -370,11 +426,34 @@ pub async fn test(test: &WebDavTest) {
}
}
// Test 10: expand-property report
for path in [&test_base_path, &test_path] {
let response = client
.request("REPORT", path, EXPAND_REPORT_QUERY)
.await
.with_status(StatusCode::MULTI_STATUS)
.into_propfind_response(None);
let properties = response.properties(path);
for prop in [
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::Owner),
] {
properties.get(prop).with_some_values([
format!(
"D:response.D:href:{}/jane/",
DavResourceName::Principal.base_path(),
)
.as_str(),
"D:response.D:propstat.D:prop.D:displayname:Jane Doe-Smith",
]);
}
}
for (path, etag, is_file) in [
(&test_base_path, &etag_folder, false),
(&test_path, &etag_file, true),
] {
// Test 9: PROPPATCH should fail when a precondition fails
// Test 11: PROPPATCH should fail when a precondition fails
client
.proppatch(
path,
@@ -407,7 +486,7 @@ pub async fn test(test: &WebDavTest) {
.with_status(StatusCode::OK)
.without_values([etag.as_str()]);
// Test 10: PROPPATCH set on DAV properties
// Test 12: PROPPATCH set on DAV properties
client
.patch_and_check(
path,
@@ -431,7 +510,7 @@ pub async fn test(test: &WebDavTest) {
)
.await;
// Test 11: PROPPATCH remove on DAV properties
// Test 13: PROPPATCH remove on DAV properties
let mut props = vec![
(
DavProperty::DeadProperty(DeadElementTag::new(
@@ -450,7 +529,7 @@ pub async fn test(test: &WebDavTest) {
match resource_type {
DavResourceName::File if is_file => {
// Test 12: Change a file's content-type
// Test 14: Change a file's content-type
client
.patch_and_check(
path,
@@ -462,7 +541,7 @@ pub async fn test(test: &WebDavTest) {
.await;
}
DavResourceName::Cal if !is_file => {
// Test 13: Change a calendar's properties
// Test 15: Change a calendar's properties
client
.patch_and_check(
path,
@@ -498,7 +577,7 @@ pub async fn test(test: &WebDavTest) {
.await;
}
DavResourceName::Card if !is_file => {
// Test 14: Change an addressbook's properties
// Test 16: Change an addressbook's properties
client
.patch_and_check(
path,
@@ -521,7 +600,7 @@ pub async fn test(test: &WebDavTest) {
_ => (),
}
// Test 15: PROPPATCH should fail on large properties
// Test 17: PROPPATCH should fail on large properties
let mut chunky_props = vec![
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::DeadProperty(DeadElementTag::new(
@@ -572,7 +651,7 @@ pub async fn test(test: &WebDavTest) {
.with_description("Property value is too long");
}
// Test 16: PROPPATCH should fail on invalid calendar property values
// Test 18: PROPPATCH should fail on invalid calendar property values
if !is_file && resource_type == DavResourceName::Cal {
let response = client
.proppatch(
@@ -603,7 +682,16 @@ pub async fn test(test: &WebDavTest) {
.with_description("Invalid calendar timezone");
}
}
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;
}
#[derive(Debug)]
@@ -637,6 +725,16 @@ impl DavMultiStatus {
}),
}
}
pub fn with_hrefs<'x>(&self, expect_hrefs: impl IntoIterator<Item = &'x str>) -> &Self {
let expect_hrefs: AHashSet<_> = expect_hrefs.into_iter().collect();
let hrefs: AHashSet<_> = self.hrefs.keys().map(|s| s.as_str()).collect();
if hrefs != expect_hrefs {
self.response.dump_response();
panic!("Expected hrefs {expect_hrefs:?}, but got {hrefs:?}",);
}
self
}
}
pub struct DavPropertyResult<'x> {
@@ -671,6 +769,33 @@ impl DavPropertyResult<'_> {
)
})
}
pub fn is_defined(&self, name: impl AsRef<str>) -> &Self {
if self
.properties
.0
.iter()
.any(|prop| prop.values.contains_key(name.as_ref()))
{
self
} else {
self.response.dump_response();
panic!("Expected property {} to be defined", name.as_ref());
}
}
pub fn is_undefined(&self, name: impl AsRef<str>) -> &Self {
if self
.properties
.0
.iter()
.any(|prop| prop.values.contains_key(name.as_ref()))
{
self.response.dump_response();
panic!("Expected property {} to be undefined", name.as_ref());
}
self
}
}
impl<'x> DavQueryResult<'x> {
@@ -689,6 +814,23 @@ impl<'x> DavQueryResult<'x> {
self
}
pub fn with_some_values(&self, expected_values: impl IntoIterator<Item = &'x str>) -> &Self {
let values = self
.values
.iter()
.map(|s| s.as_str())
.collect::<AHashSet<_>>();
for expected_value in expected_values {
if !values.contains(expected_value) {
self.response.dump_response();
panic!("Expected at least one of {expected_value:?} values, but got {values:?}",);
}
}
self
}
pub fn without_values(&self, expected_values: impl IntoIterator<Item = &'x str>) -> &Self {
let expected_values = AHashSet::from_iter(expected_values);
let values = self
@@ -895,6 +1037,19 @@ impl DummyWebDavClient {
}
pub async fn propfind<I, T>(&self, path: &str, properties: I) -> DavMultiStatus
where
I: IntoIterator<Item = T>,
T: AsRef<str>,
{
self.propfind_with_headers(path, properties, []).await
}
pub async fn propfind_with_headers<I, T>(
&self,
path: &str,
properties: I,
headers: impl IntoIterator<Item = (&'static str, &str)>,
) -> DavMultiStatus
where
I: IntoIterator<Item = T>,
T: AsRef<str>,
@@ -913,7 +1068,7 @@ impl DummyWebDavClient {
request.push_str("</D:prop></D:propfind>");
self.request("PROPFIND", path, &request)
self.request_with_headers("PROPFIND", path, headers, &request)
.await
.with_status(StatusCode::MULTI_STATUS)
.into_propfind_response(None)
@@ -975,7 +1130,21 @@ impl Default for DavItem {
}
}
const ALL_DAV_PROPERTIES: &[DavProperty] = &[
const EXPAND_REPORT_QUERY: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<D:expand-property xmlns:D="DAV:"
xmlns:A="urn:ietf:params:xml:ns:caldav"
xmlns:B="urn:ietf:params:xml:ns:carddav">
<A:property name="calendar-description"/>
<B:property name="addressbook-description"/>
<D:property name="current-user-principal">
<D:property name="displayname"/>
</D:property>
<D:property name="owner">
<D:property name="displayname"/>
</D:property>
</D:expand-property>"#;
pub const ALL_DAV_PROPERTIES: &[DavProperty] = &[
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetContentLanguage),