mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-09 13:25:29 +08:00
WebDAV REPORT expand-property, principal-*
This commit is contained in:
parent
d72ae41058
commit
9b002c2d3d
8 changed files with 640 additions and 98 deletions
|
|
@ -247,21 +247,18 @@ impl<'x> DavQuery<'x> {
|
|||
expand: ExpandProperty,
|
||||
headers: RequestHeaders<'x>,
|
||||
) -> Self {
|
||||
let mut props = Vec::with_capacity(expand.properties.len());
|
||||
for item in expand.properties {
|
||||
if !matches!(item.property, DavProperty::DeadProperty(_))
|
||||
&& !props.contains(&item.property)
|
||||
{
|
||||
props.push(item.property);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
resource: DavQueryResource::Uri(resource),
|
||||
propfind: PropFind::Prop(
|
||||
expand
|
||||
.properties
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
if !matches!(item.property, DavProperty::DeadProperty(_)) {
|
||||
Some(item.property)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
propfind: PropFind::Prop(props),
|
||||
depth: match headers.depth {
|
||||
Depth::Zero => 0,
|
||||
_ => 1,
|
||||
|
|
|
|||
|
|
@ -491,7 +491,7 @@ impl PropFindRequestHandler for Server {
|
|||
} else {
|
||||
containers.contains(item.document_id)
|
||||
}
|
||||
})
|
||||
}) && (!query.depth_no_root || item.name != resource)
|
||||
})
|
||||
.map(|item| {
|
||||
PropFindItem::new(resources.format_resource(item), account_id, item)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ use store::roaring::RoaringBitmap;
|
|||
|
||||
use crate::{
|
||||
DavError,
|
||||
common::{DavQuery, DavQueryResource, propfind::PropFindRequestHandler, uri::DavUriResource},
|
||||
common::{
|
||||
DavQuery, DavQueryResource,
|
||||
propfind::PropFindRequestHandler,
|
||||
uri::{DavUriResource, UriResource},
|
||||
},
|
||||
};
|
||||
|
||||
use super::propfind::PrincipalPropFind;
|
||||
|
|
@ -41,17 +45,25 @@ impl PrincipalMatching for Server {
|
|||
headers: RequestHeaders<'_>,
|
||||
mut request: PrincipalMatch,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
let resource = self
|
||||
.validate_uri(access_token, headers.uri)
|
||||
.await
|
||||
.and_then(|uri| uri.into_owned_uri())?;
|
||||
let resource = self.validate_uri(access_token, headers.uri).await?;
|
||||
|
||||
match resource.collection {
|
||||
Collection::AddressBook | Collection::Calendar | Collection::FileNode => {
|
||||
self.handle_dav_query(
|
||||
if request.properties.is_empty() {
|
||||
request
|
||||
.properties
|
||||
.push(DavProperty::WebDav(WebDavProperty::Owner));
|
||||
}
|
||||
if let Some(account_id) = resource.account_id {
|
||||
return self
|
||||
.handle_dav_query(
|
||||
access_token,
|
||||
DavQuery {
|
||||
resource: DavQueryResource::Uri(resource),
|
||||
resource: DavQueryResource::Uri(UriResource {
|
||||
collection: resource.collection,
|
||||
account_id,
|
||||
resource: resource.resource,
|
||||
}),
|
||||
propfind: PropFind::Prop(request.properties),
|
||||
depth: usize::MAX,
|
||||
ret: headers.ret,
|
||||
|
|
@ -59,9 +71,13 @@ impl PrincipalMatching for Server {
|
|||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
}
|
||||
Collection::Principal => {
|
||||
}
|
||||
Collection::Principal => {}
|
||||
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
}
|
||||
|
||||
let mut response = MultiStatus::new(Vec::with_capacity(16));
|
||||
if request.properties.is_empty() {
|
||||
request
|
||||
|
|
@ -71,7 +87,7 @@ impl PrincipalMatching for Server {
|
|||
let request = PropFind::Prop(request.properties);
|
||||
self.prepare_principal_propfind_response(
|
||||
access_token,
|
||||
Collection::Principal,
|
||||
resource.collection,
|
||||
RoaringBitmap::from_iter(access_token.all_ids()).into_iter(),
|
||||
&request,
|
||||
&mut response,
|
||||
|
|
@ -79,7 +95,4 @@ impl PrincipalMatching for Server {
|
|||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
|
||||
}
|
||||
_ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,13 +234,9 @@ impl PrincipalPropFind for Server {
|
|||
}
|
||||
},
|
||||
DavProperty::Principal(principal_property) => match principal_property {
|
||||
PrincipalProperty::AlternateURISet => {
|
||||
fields.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
PrincipalProperty::GroupMemberSet => {
|
||||
fields.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
PrincipalProperty::GroupMembership => {
|
||||
PrincipalProperty::AlternateURISet
|
||||
| PrincipalProperty::GroupMemberSet
|
||||
| PrincipalProperty::GroupMembership => {
|
||||
fields.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
PrincipalProperty::PrincipalURL => {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
356
tests/src/webdav/principals.rs
Normal file
356
tests/src/webdav/principals.rs
Normal 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>"#;
|
||||
|
|
@ -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
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.with_hrefs([format!("{user_base_path}/").as_str(), &test_base_path]);
|
||||
}
|
||||
.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),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue