mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-10-06 18:45:45 +08:00
WebDAV LOCK and multiget/sync-collection REPORT
This commit is contained in:
parent
2b9e0816eb
commit
d72ae41058
8 changed files with 583 additions and 24 deletions
|
@ -45,7 +45,9 @@ impl GroupwareConfig {
|
|||
live_property_size: config
|
||||
.property("dav.limits.size.live-property")
|
||||
.unwrap_or(250),
|
||||
max_lock_timeout: config.property("dav.limits.timeout.max-lock").unwrap_or(60),
|
||||
max_lock_timeout: config
|
||||
.property("dav.limits.timeout.max-lock")
|
||||
.unwrap_or(3600),
|
||||
max_locks_per_user: config
|
||||
.property("dav.limits.max-locks-per-user")
|
||||
.unwrap_or(10),
|
||||
|
|
|
@ -11,6 +11,7 @@ use dav_proto::schema::request::{DavPropertyValue, DeadProperty};
|
|||
use dav_proto::schema::response::{BaseCondition, List, PropResponse};
|
||||
use dav_proto::{Condition, Depth, Timeout};
|
||||
use dav_proto::{RequestHeaders, schema::request::LockInfo};
|
||||
use groupware::hierarchy::DavHierarchy;
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use jmap_proto::types::collection::Collection;
|
||||
|
@ -260,6 +261,13 @@ impl LockRequestHandler for Server {
|
|||
|
||||
lock_item.expires = expires;
|
||||
if let LockRequest::Lock(lock_info) = lock_info {
|
||||
// Validate lock_info
|
||||
if lock_info.owner.as_ref().is_some_and(|o| {
|
||||
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
|
||||
}) {
|
||||
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
|
||||
}
|
||||
|
||||
lock_item.lock_id = store::rand::random::<u64>() ^ expires;
|
||||
lock_item.owner = access_token.primary_id;
|
||||
lock_item.depth_infinity = matches!(headers.depth, Depth::Infinity);
|
||||
|
@ -270,15 +278,19 @@ impl LockRequestHandler for Server {
|
|||
let base_path = base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
|
||||
let active_lock = lock_item.to_active_lock(format!("{base_path}/{resource_path}"));
|
||||
|
||||
HttpResponse::new(StatusCode::CREATED)
|
||||
.with_lock_token(&active_lock.lock_token.as_ref().unwrap().0)
|
||||
.with_xml_body(
|
||||
PropResponse::new(vec![DavPropertyValue::new(
|
||||
WebDavProperty::LockDiscovery,
|
||||
vec![active_lock],
|
||||
)])
|
||||
.to_string(),
|
||||
)
|
||||
HttpResponse::new(if if_lock_token == 0 {
|
||||
StatusCode::CREATED
|
||||
} else {
|
||||
StatusCode::OK
|
||||
})
|
||||
.with_lock_token(&active_lock.lock_token.as_ref().unwrap().0)
|
||||
.with_xml_body(
|
||||
PropResponse::new(vec![DavPropertyValue::new(
|
||||
WebDavProperty::LockDiscovery,
|
||||
vec![active_lock],
|
||||
)])
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
let lock_id = headers
|
||||
.lock_token
|
||||
|
@ -542,10 +554,14 @@ impl LockRequestHandler for Server {
|
|||
// Fetch sync token
|
||||
if needs_sync_token && resource_state.sync_token.is_none() {
|
||||
let change_id = self
|
||||
.store()
|
||||
.get_last_change_id(resource_state.account_id, resource_state.collection)
|
||||
.fetch_dav_resources(
|
||||
access_token,
|
||||
resource_state.account_id,
|
||||
resource_state.collection.main_collection(),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
.caused_by(trc::location!())?
|
||||
.modseq;
|
||||
resource_state.sync_token =
|
||||
Some(Urn::Sync(change_id.unwrap_or_default()).to_string());
|
||||
}
|
||||
|
@ -679,7 +695,7 @@ impl<'x> LockCaches<'x> {
|
|||
pub fn is_cached(&self, resource_state: &ResourceState<'_>) -> Option<usize> {
|
||||
self.caches.iter().position(|cache| {
|
||||
resource_state.account_id == cache.account_id
|
||||
&& resource_state.collection == cache.collection
|
||||
&& resource_state.collection.main_collection() == cache.collection.main_collection()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -844,13 +860,13 @@ impl ArchivedLockItem {
|
|||
|
||||
impl OwnedUri<'_> {
|
||||
pub fn lock_key(&self) -> Vec<u8> {
|
||||
build_lock_key(self.account_id, self.collection)
|
||||
build_lock_key(self.account_id, self.collection.main_collection())
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceState<'_> {
|
||||
pub fn lock_key(&self) -> Vec<u8> {
|
||||
build_lock_key(self.account_id, self.collection)
|
||||
build_lock_key(self.account_id, self.collection.main_collection())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,14 +30,14 @@ pub async fn test(test: &WebDavTest) {
|
|||
client
|
||||
.request("PROPFIND", "/.well-known/carddav", "")
|
||||
.await
|
||||
.match_many(
|
||||
.with_values(
|
||||
"D:multistatus.D:response.D:href",
|
||||
["/dav/card/", "/dav/card/john/"],
|
||||
);
|
||||
test.client("jane")
|
||||
.request("PROPFIND", "/.well-known/caldav", "")
|
||||
.await
|
||||
.match_many(
|
||||
.with_values(
|
||||
"D:multistatus.D:response.D:href",
|
||||
["/dav/cal/", "/dav/cal/jane/", "/dav/cal/support/"],
|
||||
);
|
||||
|
|
257
tests/src/webdav/lock.rs
Normal file
257
tests/src/webdav/lock.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{DavResponse, DummyWebDavClient, WebDavTest};
|
||||
use crate::webdav::GenerateTestDavResource;
|
||||
use dav_proto::schema::property::{DavProperty, WebDavProperty};
|
||||
use groupware::DavResourceName;
|
||||
use hyper::StatusCode;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
let client = test.client("john");
|
||||
|
||||
for resource_type in [
|
||||
DavResourceName::File,
|
||||
DavResourceName::Cal,
|
||||
DavResourceName::Card,
|
||||
] {
|
||||
println!(
|
||||
"Running LOCK/UNLOCK tests ({})...",
|
||||
resource_type.base_path()
|
||||
);
|
||||
let base_path = format!("{}/john", resource_type.base_path());
|
||||
|
||||
// Test 1: Creating a collection under an unmapped resource without providing a lock token should fail
|
||||
let path = format!("{base_path}/do-not-write");
|
||||
let response = client
|
||||
.lock_create(&path, "super-owner", true, "infinity", "Second-123")
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
let lock_token = response
|
||||
.with_value(
|
||||
"D:prop.D:lockdiscovery.D:activelock.D:owner.D:href",
|
||||
"super-owner",
|
||||
)
|
||||
.with_value("D:prop.D:lockdiscovery.D:activelock.D:depth", "infinity")
|
||||
.with_value(
|
||||
"D:prop.D:lockdiscovery.D:activelock.D:timeout",
|
||||
"Second-123",
|
||||
)
|
||||
.lock_token()
|
||||
.to_string();
|
||||
|
||||
// Test 2: Refreshing a lock token with an invalid a lock token should fail
|
||||
client
|
||||
.lock_refresh(&path, "urn:stalwart:davlock:1234", "infinity", "Second-456")
|
||||
.await
|
||||
.with_status(StatusCode::PRECONDITION_FAILED);
|
||||
|
||||
// Test 3: Refreshing a lock token with valid a lock token should succeed
|
||||
client
|
||||
.lock_refresh(&path, &lock_token, "infinity", "Second-456")
|
||||
.await
|
||||
.with_status(StatusCode::OK)
|
||||
.with_value(
|
||||
"D:prop.D:lockdiscovery.D:activelock.D:owner.D:href",
|
||||
"super-owner",
|
||||
)
|
||||
.with_value(
|
||||
"D:prop.D:lockdiscovery.D:activelock.D:timeout",
|
||||
"Second-456",
|
||||
);
|
||||
|
||||
// Test 3: Creating a collection under an unmapped resource with a lock token should fail
|
||||
client
|
||||
.request_with_headers("MKCOL", &path, [], "")
|
||||
.await
|
||||
.with_status(StatusCode::LOCKED)
|
||||
.with_value("D:error.D:lock-token-submitted.D:href", &path);
|
||||
|
||||
// Test 4: Creating a collection under a mapped resource with a lock token should succeed
|
||||
client
|
||||
.request_with_headers(
|
||||
"MKCOL",
|
||||
&path,
|
||||
[("if", format!("(<{lock_token}>)").as_str())],
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
|
||||
// Test 5: Creating a file under a locked resource without a lock token should fail
|
||||
let file_path = format!("{path}/file.txt");
|
||||
let contents = resource_type.generate();
|
||||
client
|
||||
.request("PUT", &file_path, &contents)
|
||||
.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 with a lock token should succeed
|
||||
client
|
||||
.request_with_headers(
|
||||
"PUT",
|
||||
&file_path,
|
||||
[("if", format!("(<{lock_token}>)").as_str())],
|
||||
&contents,
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
|
||||
// Test 7: Locks should be included in propfind responses
|
||||
let response = client
|
||||
.propfind(&path, [DavProperty::WebDav(WebDavProperty::LockDiscovery)])
|
||||
.await;
|
||||
for href in [path.clone() + "/", file_path] {
|
||||
let props = response.properties(&href);
|
||||
props
|
||||
.get(DavProperty::WebDav(WebDavProperty::LockDiscovery))
|
||||
.with_values([
|
||||
"D:activelock.D:owner.D:href:super-owner",
|
||||
"D:activelock.D:timeout:Second-456",
|
||||
"D:activelock.D:depth:infinity",
|
||||
format!("D:activelock.D:locktoken.D:href:{lock_token}").as_str(),
|
||||
format!("D:activelock.D:lockroot.D:href:{path}").as_str(),
|
||||
"D:activelock.D:locktype.D:write",
|
||||
"D:activelock.D:lockscope.D:exclusive",
|
||||
]);
|
||||
}
|
||||
|
||||
// Test 8: Delete with and without a lock token
|
||||
client
|
||||
.request("DELETE", &path, "")
|
||||
.await
|
||||
.with_status(StatusCode::LOCKED)
|
||||
.with_value("D:error.D:lock-token-submitted.D:href", &path);
|
||||
client
|
||||
.request_with_headers(
|
||||
"DELETE",
|
||||
&path,
|
||||
[("if", format!("(<{lock_token}>)").as_str())],
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
|
||||
// Test 9: Unlock with and without a lock token
|
||||
client
|
||||
.unlock(&path, "urn:stalwart:davlock:1234")
|
||||
.await
|
||||
.with_status(StatusCode::CONFLICT)
|
||||
.with_value("D:error.D:lock-token-matches-request-uri", "");
|
||||
client
|
||||
.unlock(&path, &lock_token)
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
|
||||
// Test 10: Locking with a large dead property should fail
|
||||
let path = format!("{base_path}/invalid-lock");
|
||||
client
|
||||
.lock_create(
|
||||
&path,
|
||||
(0..=test.server.core.groupware.dead_property_size.unwrap() + 1)
|
||||
.map(|_| "a")
|
||||
.collect::<String>()
|
||||
.as_str(),
|
||||
true,
|
||||
"infinity",
|
||||
"Second-123",
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::PAYLOAD_TOO_LARGE);
|
||||
|
||||
// Test 11: Too many locks should fail
|
||||
for i in 0..test.server.core.groupware.max_locks_per_user {
|
||||
client
|
||||
.lock_create(
|
||||
&format!("{base_path}/invalid-lock-{i}"),
|
||||
"super-owner",
|
||||
true,
|
||||
"infinity",
|
||||
"Second-123",
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
}
|
||||
client
|
||||
.lock_create(
|
||||
&format!("{base_path}/invalid-lock-greedy"),
|
||||
"super-owner",
|
||||
true,
|
||||
"infinity",
|
||||
"Second-123",
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
client.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
const LOCK_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:lockinfo xmlns:D='DAV:'>
|
||||
<D:lockscope><D:$TYPE/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:owner>
|
||||
<D:href>$OWNER</D:href>
|
||||
</D:owner>
|
||||
</D:lockinfo>"#;
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub async fn lock_create(
|
||||
&self,
|
||||
path: &str,
|
||||
owner: &str,
|
||||
is_exclusive: bool,
|
||||
depth: &str,
|
||||
timeout: &str,
|
||||
) -> DavResponse {
|
||||
let lock_request = LOCK_REQUEST
|
||||
.replace("$TYPE", if is_exclusive { "exclusive" } else { "shared" })
|
||||
.replace("$OWNER", owner);
|
||||
self.request_with_headers(
|
||||
"LOCK",
|
||||
path,
|
||||
[("depth", depth), ("timeout", timeout)],
|
||||
&lock_request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn lock_refresh(
|
||||
&self,
|
||||
path: &str,
|
||||
lock_token: &str,
|
||||
depth: &str,
|
||||
timeout: &str,
|
||||
) -> DavResponse {
|
||||
let condition = format!("(<{lock_token}>)");
|
||||
self.request_with_headers(
|
||||
"LOCK",
|
||||
path,
|
||||
[
|
||||
("if", condition.as_str()),
|
||||
("depth", depth),
|
||||
("timeout", timeout),
|
||||
],
|
||||
"",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unlock(&self, path: &str, lock_token: &str) -> DavResponse {
|
||||
let condition = format!("<{lock_token}>");
|
||||
self.request_with_headers("UNLOCK", path, [("lock-token", condition.as_str())], "")
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl DavResponse {
|
||||
pub fn lock_token(&self) -> &str {
|
||||
self.value("D:prop.D:lockdiscovery.D:activelock.D:locktoken.D:href")
|
||||
}
|
||||
}
|
|
@ -94,11 +94,11 @@ pub async fn test(test: &WebDavTest) {
|
|||
.mkcol("MKCOL", path, ["D:collection", resource_type], [])
|
||||
.await
|
||||
.with_status(StatusCode::FORBIDDEN)
|
||||
.match_one(
|
||||
.with_value(
|
||||
"D:mkcol-response.D:propstat.D:error.D:valid-resourcetype",
|
||||
"",
|
||||
)
|
||||
.match_one("D:mkcol-response.D:propstat.D:prop.D:resourcetype", "");
|
||||
.with_value("D:mkcol-response.D:propstat.D:prop.D:resourcetype", "");
|
||||
}
|
||||
|
||||
// Create using extended MKCOL
|
||||
|
@ -172,8 +172,8 @@ pub async fn test(test: &WebDavTest) {
|
|||
)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED)
|
||||
.match_one("A:mkcalendar-response.D:propstat.D:prop.D:displayname", "")
|
||||
.match_many(
|
||||
.with_value("A:mkcalendar-response.D:propstat.D:prop.D:displayname", "")
|
||||
.with_values(
|
||||
"A:mkcalendar-response.D:propstat.D:status",
|
||||
["HTTP/1.1 200 OK"],
|
||||
);
|
||||
|
|
|
@ -46,9 +46,12 @@ use utils::config::Config;
|
|||
|
||||
pub mod basic;
|
||||
pub mod copy_move;
|
||||
pub mod lock;
|
||||
pub mod mkcol;
|
||||
pub mod multiget;
|
||||
pub mod prop;
|
||||
pub mod put_get;
|
||||
pub mod sync;
|
||||
|
||||
const SERVER: &str = r#"
|
||||
[server]
|
||||
|
@ -358,11 +361,32 @@ pub async fn webdav_tests() {
|
|||
)
|
||||
.await;
|
||||
|
||||
/*
|
||||
TODO:
|
||||
|
||||
- Principals:
|
||||
- PrincipalMatch
|
||||
- PrincipalPropertySearch
|
||||
- PrincipalSearchPropertySet
|
||||
- Expand property
|
||||
- ACLs:
|
||||
- ACL Method
|
||||
- AclPrincipalPropSet
|
||||
|
||||
- Addressbook Query
|
||||
- Calendar Query
|
||||
- Freebusy Query
|
||||
|
||||
*/
|
||||
|
||||
basic::test(&handle).await;
|
||||
put_get::test(&handle).await;
|
||||
mkcol::test(&handle).await;
|
||||
copy_move::test(&handle).await;
|
||||
prop::test(&handle).await;
|
||||
multiget::test(&handle).await;
|
||||
sync::test(&handle).await;
|
||||
lock::test(&handle).await;
|
||||
|
||||
// Print elapsed time
|
||||
let elapsed = start_time.elapsed();
|
||||
|
@ -784,8 +808,15 @@ impl DavResponse {
|
|||
.map(|(_, value)| value.as_str())
|
||||
}
|
||||
|
||||
pub fn value(&self, name: &str) -> &str {
|
||||
self.find_keys(name).next().unwrap_or_else(|| {
|
||||
self.dump_response();
|
||||
panic!("Key {name} not found.")
|
||||
})
|
||||
}
|
||||
|
||||
// Poor man's XPath
|
||||
pub fn match_one(self, query: &str, expect: impl AsRef<str>) -> Self {
|
||||
pub fn with_value(self, query: &str, expect: impl AsRef<str>) -> Self {
|
||||
let expect = expect.as_ref();
|
||||
if let Some(value) = self.find_keys(query).next() {
|
||||
if value != expect {
|
||||
|
@ -799,7 +830,7 @@ impl DavResponse {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn match_many<I, T>(self, query: &str, expect: I) -> Self
|
||||
pub fn with_values<I, T>(self, query: &str, expect: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: AsRef<str>,
|
||||
|
|
104
tests/src/webdav/multiget.rs
Normal file
104
tests/src/webdav/multiget.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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::GenerateTestDavResource;
|
||||
use dav_proto::schema::property::{CalDavProperty, CardDavProperty, DavProperty, WebDavProperty};
|
||||
use groupware::DavResourceName;
|
||||
use hyper::StatusCode;
|
||||
|
||||
const MULTIGET_CALENDAR: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
<C:calendar-data/>
|
||||
</D:prop>
|
||||
<D:href>$PATH_1</D:href>
|
||||
<D:href>$PATH_2</D:href>
|
||||
</C:calendar-multiget>
|
||||
"#;
|
||||
const MULTIGET_ADDRESSBOOK: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<C:addressbook-multiget xmlns:D="DAV:"
|
||||
xmlns:C="urn:ietf:params:xml:ns:carddav">
|
||||
<D:prop>
|
||||
<D:getetag/>
|
||||
<C:address-data/>
|
||||
</D:prop>
|
||||
<D:href>$PATH_1</D:href>
|
||||
<D:href>$PATH_2</D:href>
|
||||
</C:addressbook-multiget>
|
||||
"#;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
let client = test.client("john");
|
||||
|
||||
for resource_type in [DavResourceName::Cal, DavResourceName::Card] {
|
||||
println!(
|
||||
"Running REPORT multiget tests ({})...",
|
||||
resource_type.base_path()
|
||||
);
|
||||
|
||||
let mut paths = Vec::new();
|
||||
for name in ["file1", "file2"] {
|
||||
let contents = resource_type.generate();
|
||||
let path = format!("{}/john/default/{}", resource_type.base_path(), name);
|
||||
let etag = client
|
||||
.request("PUT", &path, contents.as_str())
|
||||
.await
|
||||
.with_status(StatusCode::CREATED)
|
||||
.etag()
|
||||
.to_string();
|
||||
paths.push((path, etag, contents));
|
||||
}
|
||||
|
||||
if resource_type == DavResourceName::Cal {
|
||||
let path = format!("{}/john", resource_type.base_path());
|
||||
let body = MULTIGET_CALENDAR
|
||||
.replace("$PATH_1", &paths[0].0)
|
||||
.replace("$PATH_2", &paths[1].0);
|
||||
let response = client
|
||||
.request("REPORT", &path, &body)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None);
|
||||
for (path, etag, contents) in paths {
|
||||
let props = response.properties(&path);
|
||||
props
|
||||
.get(DavProperty::WebDav(WebDavProperty::GetETag))
|
||||
.with_values([etag.as_str()]);
|
||||
props
|
||||
.get(DavProperty::CalDav(CalDavProperty::CalendarData(
|
||||
Default::default(),
|
||||
)))
|
||||
.with_values([contents.as_str()]);
|
||||
}
|
||||
} else {
|
||||
let path = format!("{}/john", resource_type.base_path());
|
||||
let body = MULTIGET_ADDRESSBOOK
|
||||
.replace("$PATH_1", &paths[0].0)
|
||||
.replace("$PATH_2", &paths[1].0);
|
||||
let response = client
|
||||
.request("REPORT", &path, &body)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None);
|
||||
for (path, etag, contents) in paths {
|
||||
let props = response.properties(&path);
|
||||
props
|
||||
.get(DavProperty::WebDav(WebDavProperty::GetETag))
|
||||
.with_values([etag.as_str()]);
|
||||
props
|
||||
.get(DavProperty::CardDav(CardDavProperty::AddressData(
|
||||
Default::default(),
|
||||
)))
|
||||
.with_values([contents.as_str()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
149
tests/src/webdav/sync.rs
Normal file
149
tests/src/webdav/sync.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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::GenerateTestDavResource;
|
||||
use dav_proto::Depth;
|
||||
use groupware::DavResourceName;
|
||||
use hyper::StatusCode;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
let client = test.client("john");
|
||||
|
||||
for resource_type in [
|
||||
DavResourceName::File,
|
||||
DavResourceName::Cal,
|
||||
DavResourceName::Card,
|
||||
] {
|
||||
println!(
|
||||
"Running REPORT sync-collection tests ({})...",
|
||||
resource_type.base_path()
|
||||
);
|
||||
let user_base_path = format!("{}/john", resource_type.base_path());
|
||||
|
||||
// Test 1: Initial sync
|
||||
let response = client
|
||||
.sync_collection(&user_base_path, "", Depth::Infinity, ["D:getetag"])
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.hrefs().len(),
|
||||
if resource_type == DavResourceName::File {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
},
|
||||
"{:?}",
|
||||
response.hrefs()
|
||||
);
|
||||
let sync_token_1 = response.sync_token().to_string();
|
||||
|
||||
// Test 2: No changes since last sync
|
||||
let response = client
|
||||
.sync_collection(
|
||||
&user_base_path,
|
||||
&sync_token_1,
|
||||
Depth::Infinity,
|
||||
["D:getetag"],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.hrefs(), Vec::<String>::new());
|
||||
|
||||
// Test 3: Create a collection and make sure it is synced
|
||||
let new_collection = format!("{}/new-collection/", user_base_path);
|
||||
client
|
||||
.mkcol("MKCOL", &new_collection, [], [])
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
let response = client
|
||||
.sync_collection(
|
||||
&user_base_path,
|
||||
&sync_token_1,
|
||||
Depth::Infinity,
|
||||
["D:getetag"],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
|
||||
let sync_token_2 = response.sync_token().to_string();
|
||||
|
||||
// Test 4: Create a file and make sure it is synced
|
||||
let new_file = format!("{new_collection}new-file");
|
||||
let contents = resource_type.generate();
|
||||
client
|
||||
.request("PUT", &new_file, &contents)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
let response = client
|
||||
.sync_collection(
|
||||
&user_base_path,
|
||||
&sync_token_1,
|
||||
Depth::Infinity,
|
||||
["D:getetag"],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.hrefs(),
|
||||
vec![new_collection.clone(), new_file.clone()]
|
||||
);
|
||||
let sync_token_3 = response.sync_token().to_string();
|
||||
let response = client
|
||||
.sync_collection(
|
||||
&user_base_path,
|
||||
&sync_token_2,
|
||||
Depth::Infinity,
|
||||
["D:getetag"],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.hrefs(), vec![new_file.clone()]);
|
||||
|
||||
// Test 5: sync-token with Depth 1
|
||||
let response = client
|
||||
.sync_collection(&user_base_path, &sync_token_1, Depth::One, ["D:getetag"])
|
||||
.await;
|
||||
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
|
||||
|
||||
// Test 6: sync-token with Depth 0
|
||||
let response = client
|
||||
.sync_collection(&new_collection, &sync_token_1, Depth::Zero, ["D:getetag"])
|
||||
.await;
|
||||
assert_eq!(response.hrefs(), vec![new_collection.clone()]);
|
||||
|
||||
// Test 7: Outdated sync-token in If header should fail
|
||||
let new_file2 = format!("{new_collection}new-file2");
|
||||
let contents = resource_type.generate();
|
||||
let condition = format!("(<{sync_token_2}>)");
|
||||
client
|
||||
.request_with_headers(
|
||||
"PUT",
|
||||
&new_file2,
|
||||
[("if", condition.as_str())],
|
||||
contents.as_str(),
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::PRECONDITION_FAILED)
|
||||
.with_empty_body();
|
||||
|
||||
// Test 8: Correct sync-token in If header should work
|
||||
let condition = format!("(<{sync_token_3}>)");
|
||||
client
|
||||
.request_with_headers(
|
||||
"PUT",
|
||||
&new_file2,
|
||||
[("if", condition.as_str())],
|
||||
contents.as_str(),
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED)
|
||||
.with_empty_body();
|
||||
|
||||
client
|
||||
.request("DELETE", &new_collection, "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
client.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
Loading…
Add table
Reference in a new issue