Registry testing - part 8
This commit is contained in:
@@ -399,37 +399,6 @@ pub async fn test(test: &WebDavTest) {
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub async fn acl<'x>(
|
||||
&self,
|
||||
query: &str,
|
||||
principal_href: &str,
|
||||
grant: impl IntoIterator<Item = &'x str>,
|
||||
) -> DavResponse {
|
||||
let body = ACL_QUERY.replace("$HREF", principal_href).replace(
|
||||
"$GRANT",
|
||||
&grant.into_iter().fold(String::new(), |mut output, g| {
|
||||
use std::fmt::Write;
|
||||
let _ = write!(output, "<D:privilege><D:{g}/></D:privilege>");
|
||||
output
|
||||
}),
|
||||
);
|
||||
self.request("ACL", query, &body).await
|
||||
}
|
||||
}
|
||||
|
||||
const ACL_QUERY: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:acl xmlns:D="DAV:">
|
||||
<D:ace>
|
||||
<D:principal>
|
||||
<D:href>$HREF</D:href>
|
||||
</D:principal>
|
||||
<D:grant>
|
||||
$GRANT
|
||||
</D:grant>
|
||||
</D:ace>
|
||||
</D:acl>"#;
|
||||
|
||||
const ACL_PRINCIPAL_QUERY: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:acl-principal-prop-set xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
|
||||
@@ -202,64 +202,6 @@ pub async fn test(test: &WebDavTest) {
|
||||
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")
|
||||
|
||||
@@ -256,43 +256,3 @@ pub async fn test(test: &WebDavTest) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,446 +282,6 @@ impl WebDavTest {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct DummyWebDavClient {
|
||||
account_id: u32,
|
||||
name: &'static str,
|
||||
email: &'static str,
|
||||
credentials: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DavResponse {
|
||||
headers: AHashMap<String, String>,
|
||||
status: StatusCode,
|
||||
body: Result<String, String>,
|
||||
xml: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub fn new(
|
||||
account_id: u32,
|
||||
name: &'static str,
|
||||
secret: &'static str,
|
||||
email: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
account_id,
|
||||
name,
|
||||
email,
|
||||
credentials: format!(
|
||||
"Basic {}",
|
||||
STANDARD.encode(format!("{name}:{secret}").as_bytes())
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request(&self, method: &str, query: &str, body: impl Into<String>) -> DavResponse {
|
||||
self.request_with_headers(method, query, [].into_iter(), body)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn request_with_headers(
|
||||
&self,
|
||||
method: &str,
|
||||
query: &str,
|
||||
headers: impl IntoIterator<Item = (&'static str, &str)>,
|
||||
body: impl Into<String>,
|
||||
) -> DavResponse {
|
||||
let mut request = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
.request(
|
||||
Method::from_bytes(method.as_bytes()).unwrap(),
|
||||
format!("https://127.0.0.1:8899{query}"),
|
||||
);
|
||||
|
||||
let body = body.into();
|
||||
if !body.is_empty() {
|
||||
request = request.body(body);
|
||||
}
|
||||
|
||||
let mut request_headers = HeaderMap::new();
|
||||
for (key, value) in headers {
|
||||
request_headers.insert(key, value.parse().unwrap());
|
||||
}
|
||||
request_headers.insert(AUTHORIZATION, self.credentials.parse().unwrap());
|
||||
|
||||
let response = request.headers(request_headers).send().await.unwrap();
|
||||
let status = response.status();
|
||||
let headers = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string().to_lowercase(),
|
||||
v.to_str().unwrap().to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let body = response
|
||||
.bytes()
|
||||
.await
|
||||
.map(|bytes| String::from_utf8(bytes.to_vec()).unwrap())
|
||||
.map_err(|err| err.to_string());
|
||||
let xml = match &body {
|
||||
Ok(body) if body.starts_with("<?xml") => flatten_xml(body),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
DavResponse {
|
||||
headers,
|
||||
status,
|
||||
body,
|
||||
xml,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
max_depth: usize,
|
||||
containers_per_level: usize,
|
||||
files_per_container: usize,
|
||||
) -> (String, Vec<(String, String)>) {
|
||||
let resource_type = if base_path.starts_with("/dav/card/") {
|
||||
DavResourceName::Card
|
||||
} else if base_path.starts_with("/dav/cal/") {
|
||||
DavResourceName::Cal
|
||||
} else {
|
||||
DavResourceName::File
|
||||
};
|
||||
|
||||
let mut created_resources = Vec::new();
|
||||
|
||||
self.create_hierarchy_recursive(
|
||||
resource_type,
|
||||
base_path,
|
||||
max_depth,
|
||||
containers_per_level,
|
||||
files_per_container,
|
||||
0,
|
||||
&mut created_resources,
|
||||
)
|
||||
.await;
|
||||
|
||||
let root_folder = created_resources.first().unwrap().0.clone();
|
||||
created_resources.sort_unstable_by(|a, b| a.0.cmp(&b.0));
|
||||
(root_folder, created_resources)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_hierarchy_recursive(
|
||||
&self,
|
||||
resource_type: DavResourceName,
|
||||
base_path: &str,
|
||||
max_depth: usize,
|
||||
containers_per_level: usize,
|
||||
files_per_container: usize,
|
||||
current_depth: usize,
|
||||
created_resources: &mut Vec<(String, String)>,
|
||||
) {
|
||||
let folder_name = generate_random_name(4);
|
||||
let folder_path = format!("{base_path}/Folder_{folder_name}");
|
||||
|
||||
self.mkcol("MKCOL", &folder_path, [], [])
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
|
||||
created_resources.push((format!("{folder_path}/"), "".to_string()));
|
||||
|
||||
for _ in 0..files_per_container {
|
||||
let file_name = generate_random_name(8);
|
||||
let file_path = format!(
|
||||
"{folder_path}/{file_name}.{}",
|
||||
match resource_type {
|
||||
DavResourceName::Card => "vcf",
|
||||
DavResourceName::Cal => "ics",
|
||||
DavResourceName::File => "txt",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
);
|
||||
let content = match resource_type {
|
||||
DavResourceName::Card => generate_random_vcard(),
|
||||
DavResourceName::Cal => generate_random_ical(),
|
||||
DavResourceName::File => generate_random_content(100, 500),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.request("PUT", &file_path, &content)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
|
||||
created_resources.push((file_path, content));
|
||||
}
|
||||
|
||||
if current_depth < max_depth {
|
||||
for _ in 0..containers_per_level {
|
||||
Box::pin(self.create_hierarchy_recursive(
|
||||
resource_type,
|
||||
&folder_path,
|
||||
max_depth,
|
||||
containers_per_level,
|
||||
files_per_container,
|
||||
current_depth + 1,
|
||||
created_resources,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn validate_values(&self, items: &[(String, String)]) {
|
||||
for (path, value) in items {
|
||||
if !path.ends_with('/') {
|
||||
self.request("GET", path, "")
|
||||
.await
|
||||
.with_status(StatusCode::OK)
|
||||
.with_body(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_default_containers(&self) {
|
||||
self.delete_default_containers_by_account(self.name).await;
|
||||
}
|
||||
|
||||
pub async fn delete_default_containers_by_account(&self, account: &str) {
|
||||
for col in ["card", "cal"] {
|
||||
self.request("DELETE", &format!("/dav/{col}/{account}/default"), "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DavResponse {
|
||||
pub fn with_status(self, status: StatusCode) -> Self {
|
||||
if self.status != status {
|
||||
self.dump_response();
|
||||
panic!("Expected {status} but got {}", self.status)
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_redirect_to(self, url: &str) -> Self {
|
||||
self.with_status(StatusCode::TEMPORARY_REDIRECT)
|
||||
.with_header("location", url)
|
||||
}
|
||||
|
||||
pub fn with_header(self, header: &str, value: &str) -> Self {
|
||||
if self.headers.get(header).is_some_and(|v| v == value) {
|
||||
self
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Header {header}:{value} not found.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_body(self, expect_body: impl AsRef<str>) -> Self {
|
||||
let expect_body = expect_body.as_ref();
|
||||
if self.body.is_ok() {
|
||||
let body = self.body.as_ref().unwrap();
|
||||
if body != expect_body {
|
||||
self.dump_response();
|
||||
assert_eq!(body, &expect_body);
|
||||
}
|
||||
self
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Expected body {expect_body:?} but no body was returned.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_empty_body(self) -> Self {
|
||||
if self.body.is_ok() {
|
||||
let body = self.body.as_ref().unwrap();
|
||||
if !body.is_empty() {
|
||||
self.dump_response();
|
||||
panic!("Expected empty body but got {body:?}");
|
||||
}
|
||||
self
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Expected empty body but no body was returned.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_body(&self) -> &str {
|
||||
if self.body.is_ok() {
|
||||
self.body.as_ref().unwrap()
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Expected body but no body was returned.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self, header: &str) -> &str {
|
||||
if let Some(value) = self.headers.get(header) {
|
||||
value
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Header {header} not found.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn etag(&self) -> &str {
|
||||
self.header("etag")
|
||||
}
|
||||
|
||||
pub fn sync_token(&self) -> &str {
|
||||
self.find_keys("D:multistatus.D:sync-token")
|
||||
.next()
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or_else(|| {
|
||||
self.dump_response();
|
||||
panic!("Sync token not found.")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hrefs(&self) -> Vec<&str> {
|
||||
let mut hrefs = self
|
||||
.find_keys("D:multistatus.D:response.D:href")
|
||||
.collect::<Vec<_>>();
|
||||
hrefs.sort_unstable();
|
||||
hrefs
|
||||
}
|
||||
|
||||
pub fn with_href_count(self, count: usize) -> Self {
|
||||
let href_count = self.find_keys("D:multistatus.D:response.D:href").count();
|
||||
if href_count != count {
|
||||
self.dump_response();
|
||||
panic!("Expected {} hrefs but got {}", count, href_count);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
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);
|
||||
eprintln!("Headers:");
|
||||
for (key, value) in self.headers.iter() {
|
||||
eprintln!(" {}: {:?}", key, value);
|
||||
}
|
||||
if !self.xml.is_empty() {
|
||||
eprintln!("XML: {}", xml_pretty_print(self.body.as_ref().unwrap()));
|
||||
|
||||
for (key, value) in self.xml.iter() {
|
||||
eprintln!("{} -> {:?}", key, value);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Body: {:?}", self.body);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_keys(&self, name: &str) -> impl Iterator<Item = &str> {
|
||||
self.xml
|
||||
.iter()
|
||||
.filter(move |(key, _)| name == key)
|
||||
.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 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 {
|
||||
self.dump_response();
|
||||
panic!("Expected {query} = {expect:?} but got {value:?}");
|
||||
}
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Key {query} not found.");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_any_value<'x>(
|
||||
self,
|
||||
query: &str,
|
||||
expect: impl IntoIterator<Item = &'x str>,
|
||||
) -> Self {
|
||||
let expect = expect.into_iter().collect::<AHashSet<_>>();
|
||||
if let Some(value) = self.find_keys(query).next() {
|
||||
if !expect.contains(value) {
|
||||
self.dump_response();
|
||||
panic!("Expected {query} = {expect:?} but got {value:?}");
|
||||
}
|
||||
} else {
|
||||
self.dump_response();
|
||||
panic!("Key {query} not found.");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_values<I, T>(self, query: &str, expect: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let expect_owned: Vec<T> = expect.into_iter().collect();
|
||||
let expect = expect_owned.iter().map(|s| s.as_ref()).collect::<Vec<_>>();
|
||||
let found = self.find_keys(query).collect::<Vec<_>>();
|
||||
if expect != found {
|
||||
self.dump_response();
|
||||
panic!("Expected {query} = {expect:?} but got {found:?}");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_failed_precondition(self, precondition: &str, value: &str) -> Self {
|
||||
let error = format!("D:error.{precondition}");
|
||||
if self.find_keys(&error).next().is_none_or(|v| v != value) {
|
||||
self.dump_response();
|
||||
panic!("Precondition {precondition} did not match.");
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DavResourcesTest {
|
||||
fn items(&self) -> Vec<DavResource>;
|
||||
}
|
||||
@@ -732,104 +292,6 @@ impl DavResourcesTest for DavResources {
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_xml(xml: &str) -> Vec<(String, String)> {
|
||||
let mut reader = Reader::from_str(xml);
|
||||
|
||||
let mut path: Vec<String> = Vec::new();
|
||||
let mut result: Vec<(String, String)> = Vec::new();
|
||||
let mut buf = Vec::new();
|
||||
let mut text_content: Option<String> = None;
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf).unwrap() {
|
||||
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!("{}.[{}]", base_path, key), value_str));
|
||||
}
|
||||
text_content = None;
|
||||
}
|
||||
Event::Empty(ref e) => {
|
||||
let name = str::from_utf8(e.name().as_ref()).unwrap().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.xml_content().unwrap();
|
||||
let trimmed = text.trim();
|
||||
if !trimmed.is_empty() {
|
||||
if let Some(text_content) = text_content.as_mut() {
|
||||
text_content.push_str(trimmed);
|
||||
} else {
|
||||
text_content = Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::GeneralRef(entity) => {
|
||||
let value: Cow<str> = match entity.as_ref() {
|
||||
b"lt" => "<".into(),
|
||||
b"gt" => ">".into(),
|
||||
b"amp" => "&".into(),
|
||||
b"apos" => "'".into(),
|
||||
b"quot" => "\"".into(),
|
||||
_ => {
|
||||
if let Ok(Some(gr)) = entity.resolve_char_ref() {
|
||||
gr.to_string().into()
|
||||
} else {
|
||||
std::str::from_utf8(entity.as_ref())
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(text_content) = text_content.as_mut() {
|
||||
text_content.push_str(value.as_ref());
|
||||
} else {
|
||||
text_content = Some(value.into_owned());
|
||||
}
|
||||
}
|
||||
Event::CData(e) => {
|
||||
text_content = Some(std::str::from_utf8(e.as_ref()).unwrap().to_string());
|
||||
}
|
||||
Event::End(_) => {
|
||||
if let Some(text) = text_content.take() {
|
||||
result.push((path.join("."), text));
|
||||
}
|
||||
|
||||
if !path.is_empty() {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
Event::Eof => break,
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub const TEST_VCARD_1: &str = r#"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:18F098B5-7383-4FD6-B482-48F2181D73AA
|
||||
@@ -927,126 +389,6 @@ END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
"#;
|
||||
|
||||
pub trait GenerateTestDavResource {
|
||||
fn generate(&self) -> String;
|
||||
}
|
||||
|
||||
impl GenerateTestDavResource for DavResourceName {
|
||||
fn generate(&self) -> String {
|
||||
match self {
|
||||
DavResourceName::Card => generate_random_vcard(),
|
||||
DavResourceName::Cal => generate_random_ical(),
|
||||
DavResourceName::File => generate_random_content(100, 200),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_random_vcard() -> String {
|
||||
r#"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
UID:$UID
|
||||
FN:$NAME
|
||||
END:VCARD
|
||||
"#
|
||||
.replace("$UID", &generate_random_name(8))
|
||||
.replace("$NAME", &generate_random_name(10))
|
||||
.replace('\n', "\r\n")
|
||||
}
|
||||
|
||||
fn generate_random_ical() -> String {
|
||||
r#"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
UID:$UID
|
||||
SUMMARY:$SUMMARY
|
||||
DESCRIPTION:$DESCRIPTION
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
"#
|
||||
.replace("$UID", &generate_random_name(8))
|
||||
.replace("$SUMMARY", &generate_random_name(10))
|
||||
.replace("$DESCRIPTION", &generate_random_name(20))
|
||||
.replace('\n', "\r\n")
|
||||
}
|
||||
|
||||
fn generate_random_content(min_chars: usize, max_chars: usize) -> String {
|
||||
let mut rng = rng();
|
||||
let length = rng.random_range(min_chars..=max_chars);
|
||||
|
||||
let words = [
|
||||
"lorem",
|
||||
"ipsum",
|
||||
"dolor",
|
||||
"sit",
|
||||
"amet",
|
||||
"consectetur",
|
||||
"adipiscing",
|
||||
"elit",
|
||||
"sed",
|
||||
"do",
|
||||
"eiusmod",
|
||||
"tempor",
|
||||
"incididunt",
|
||||
"ut",
|
||||
"labore",
|
||||
"et",
|
||||
"dolore",
|
||||
"magna",
|
||||
"aliqua",
|
||||
"ut",
|
||||
"enim",
|
||||
"ad",
|
||||
"minim",
|
||||
"veniam",
|
||||
"quis",
|
||||
"nostrud",
|
||||
"exercitation",
|
||||
"ullamco",
|
||||
"laboris",
|
||||
"nisi",
|
||||
"ut",
|
||||
"aliquip",
|
||||
"ex",
|
||||
"ea",
|
||||
"commodo",
|
||||
"consequat",
|
||||
];
|
||||
|
||||
let mut content = String::with_capacity(length);
|
||||
|
||||
while content.len() < length {
|
||||
let word_idx = rng.random_range(0..words.len());
|
||||
if !content.is_empty() {
|
||||
content.push(' ');
|
||||
}
|
||||
if rng.random_ratio(1, 10) {
|
||||
content.push('.');
|
||||
let word = words[word_idx];
|
||||
let mut chars = word.chars();
|
||||
if let Some(first_char) = chars.next() {
|
||||
content.push_str(&first_char.to_uppercase().to_string());
|
||||
content.push_str(chars.as_str());
|
||||
}
|
||||
} else {
|
||||
content.push_str(words[word_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
if !content.ends_with('.') {
|
||||
content.push('.');
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
fn generate_random_name(length: usize) -> String {
|
||||
let mut rng = rng();
|
||||
(0..length)
|
||||
.map(|_| rng.sample(Alphanumeric) as char)
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl WebDavTest {
|
||||
pub async fn fetch_email(&self, account_id: u32, document_id: u32) -> Vec<u8> {
|
||||
let metadata_ = self
|
||||
|
||||
@@ -10,26 +10,6 @@ use dav_proto::schema::property::{CalDavProperty, CardDavProperty, DavProperty,
|
||||
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>
|
||||
$PATH
|
||||
</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>
|
||||
$PATH
|
||||
</C:addressbook-multiget>
|
||||
"#;
|
||||
|
||||
pub async fn test(test: &WebDavTest) {
|
||||
let client = test.client("john");
|
||||
|
||||
@@ -90,33 +70,3 @@ pub async fn test(test: &WebDavTest) {
|
||||
client.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub async fn multiget_calendar(&self, path: &str, uris: &[&str]) -> DavMultiStatus {
|
||||
let mut paths = String::new();
|
||||
for uri in uris {
|
||||
paths.push_str(&format!("<D:href>{}</D:href>", uri));
|
||||
}
|
||||
|
||||
self.request("REPORT", path, &MULTIGET_CALENDAR.replace("$PATH", &paths))
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None)
|
||||
}
|
||||
|
||||
pub async fn multiget_addressbook(&self, path: &str, uris: &[&str]) -> DavMultiStatus {
|
||||
let mut paths = String::new();
|
||||
for uri in uris {
|
||||
paths.push_str(&format!("<D:href>{}</D:href>", uri));
|
||||
}
|
||||
|
||||
self.request(
|
||||
"REPORT",
|
||||
path,
|
||||
&MULTIGET_ADDRESSBOOK.replace("$PATH", &paths),
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,498 +706,6 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) {
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DavMultiStatus {
|
||||
pub response: DavResponse,
|
||||
pub hrefs: AHashMap<String, DavProperties>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct DavItem {
|
||||
#[serde(serialize_with = "serialize_status_code")]
|
||||
pub status: StatusCode,
|
||||
pub values: AHashMap<String, Vec<String>>,
|
||||
pub error: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct DavProperties {
|
||||
#[serde(skip)]
|
||||
status: StatusCode,
|
||||
props: Vec<DavItem>,
|
||||
}
|
||||
|
||||
impl DavMultiStatus {
|
||||
pub fn properties(&self, href: &str) -> DavPropertyResult<'_> {
|
||||
DavPropertyResult {
|
||||
response: &self.response,
|
||||
properties: self.hrefs.get(href).unwrap_or_else(|| {
|
||||
self.response.dump_response();
|
||||
panic!(
|
||||
"No properties found for href: {href} in {}",
|
||||
serde_json::to_string_pretty(&self.hrefs).unwrap()
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
pub response: &'x DavResponse,
|
||||
pub properties: &'x DavProperties,
|
||||
}
|
||||
|
||||
pub struct DavQueryResult<'x> {
|
||||
pub response: &'x DavResponse,
|
||||
pub prop: &'x DavItem,
|
||||
pub values: &'x [String],
|
||||
}
|
||||
|
||||
impl DavPropertyResult<'_> {
|
||||
pub fn get(&self, name: impl AsRef<str>) -> DavQueryResult<'_> {
|
||||
let name = name.as_ref();
|
||||
self.properties
|
||||
.props
|
||||
.iter()
|
||||
.find_map(|prop| {
|
||||
prop.values.get(name).map(|values| DavQueryResult {
|
||||
response: self.response,
|
||||
prop,
|
||||
values,
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
self.response.dump_response();
|
||||
panic!(
|
||||
"No property found for name: {name} in {}",
|
||||
serde_json::to_string_pretty(&self.properties.props).unwrap()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_status(&self, status: StatusCode) -> &Self {
|
||||
if self.properties.status != status {
|
||||
self.response.dump_response();
|
||||
panic!(
|
||||
"Expected status {status}, but got {}",
|
||||
self.properties.status
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_defined(&self, name: impl AsRef<str>) -> &Self {
|
||||
if self
|
||||
.properties
|
||||
.props
|
||||
.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
|
||||
.props
|
||||
.iter()
|
||||
.any(|prop| prop.values.contains_key(name.as_ref()))
|
||||
{
|
||||
self.response.dump_response();
|
||||
panic!("Expected property {} to be undefined", name.as_ref());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn calendar_data(&self) -> DavQueryResult<'_> {
|
||||
self.get(DavProperty::CalDav(CalDavProperty::CalendarData(
|
||||
Default::default(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> DavQueryResult<'x> {
|
||||
pub fn with_values(&self, expected_values: impl IntoIterator<Item = &'x str>) -> &Self {
|
||||
let expected_values = AHashSet::from_iter(expected_values);
|
||||
let values = self
|
||||
.values
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
if values != expected_values {
|
||||
self.response.dump_response();
|
||||
assert_eq!(values, expected_values,);
|
||||
}
|
||||
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 with_any_values(&self, expected_values: impl IntoIterator<Item = &'x str>) -> &Self {
|
||||
let values = self
|
||||
.values
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<AHashSet<_>>();
|
||||
let expected_values = AHashSet::from_iter(expected_values);
|
||||
|
||||
if values.is_disjoint(&expected_values) {
|
||||
self.response.dump_response();
|
||||
panic!("Expected at least one of {expected_values:?} 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
|
||||
.values
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
if !expected_values.is_disjoint(&values) {
|
||||
self.response.dump_response();
|
||||
panic!("Expected no {expected_values:?} values, but got {values:?}",);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_not_empty(&self) -> &Self {
|
||||
if self.values.is_empty() || self.values.iter().all(|s| s.is_empty()) {
|
||||
self.response.dump_response();
|
||||
panic!("Expected non-empty values, but got {:?}", self.values);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &str {
|
||||
if let Some(value) = self.values.iter().find(|s| !s.is_empty()) {
|
||||
value
|
||||
} else {
|
||||
self.response.dump_response();
|
||||
panic!("Expected a value, but got {:?}", self.values);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(&self, status: StatusCode) -> &Self {
|
||||
if self.prop.status != status {
|
||||
self.response.dump_response();
|
||||
panic!("Expected status {status}, but got {}", self.prop.status);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(&self, description: &str) -> &Self {
|
||||
if self.prop.description.as_deref() != Some(description) {
|
||||
self.response.dump_response();
|
||||
panic!(
|
||||
"Expected description {description}, but got {:?}",
|
||||
self.prop.description
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn with_error(&self, error: &str) -> &Self {
|
||||
if !self.prop.error.contains(&error.to_string()) {
|
||||
self.response.dump_response();
|
||||
panic!("Expected error {error}, but got {:?}", self.prop.error);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DavResponse {
|
||||
pub fn into_propfind_response(mut self, prop_prefix: Option<&str>) -> DavMultiStatus {
|
||||
if let Some(prop_prefix) = prop_prefix {
|
||||
for (key, _) in self.xml.iter_mut() {
|
||||
if let Some(suffix) = key.strip_prefix(prop_prefix) {
|
||||
*key = format!("D:multistatus.D:response{suffix}");
|
||||
}
|
||||
}
|
||||
self.xml.push((
|
||||
"D:multistatus.D:response.D:href".to_string(),
|
||||
"".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut result = DavMultiStatus {
|
||||
response: self,
|
||||
hrefs: AHashMap::new(),
|
||||
};
|
||||
let mut href = None;
|
||||
let mut href_status = StatusCode::OK;
|
||||
let mut props = Vec::new();
|
||||
let mut prop = DavItem::default();
|
||||
|
||||
for (key, value) in &result.response.xml {
|
||||
match key.as_str() {
|
||||
"D:multistatus.D:response.D:href" => {
|
||||
if let Some(href) = href.take() {
|
||||
if !prop.is_empty() {
|
||||
props.push(std::mem::take(&mut prop));
|
||||
}
|
||||
result.hrefs.insert(
|
||||
href,
|
||||
DavProperties {
|
||||
status: href_status,
|
||||
props: std::mem::take(&mut props),
|
||||
},
|
||||
);
|
||||
href_status = StatusCode::OK;
|
||||
}
|
||||
href = Some(value.to_string());
|
||||
}
|
||||
"D:multistatus.D:response.D:status" => {
|
||||
href_status = value
|
||||
.split_ascii_whitespace()
|
||||
.nth(1)
|
||||
.unwrap_or_default()
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
"D:multistatus.D:response.D:propstat.D:status" => {
|
||||
prop.status = value
|
||||
.split_ascii_whitespace()
|
||||
.nth(1)
|
||||
.unwrap_or_default()
|
||||
.parse()
|
||||
.unwrap();
|
||||
}
|
||||
"D:multistatus.D:response.D:propstat.D:responsedescription" => {
|
||||
prop.description = Some(value.to_string());
|
||||
}
|
||||
_ => {
|
||||
if let Some(prop_name) =
|
||||
key.strip_prefix("D:multistatus.D:response.D:propstat.D:prop.")
|
||||
{
|
||||
if prop.status != StatusCode::PROXY_AUTHENTICATION_REQUIRED {
|
||||
props.push(std::mem::take(&mut prop));
|
||||
}
|
||||
|
||||
let (prop_name, prop_value) =
|
||||
if let Some((prop_name, prop_sub_name)) = prop_name.split_once('.') {
|
||||
if value.is_empty() {
|
||||
(prop_name, prop_sub_name.to_string())
|
||||
} else {
|
||||
(prop_name, format!("{}:{}", prop_sub_name, value))
|
||||
}
|
||||
} else {
|
||||
(prop_name, value.to_string())
|
||||
};
|
||||
prop.values
|
||||
.entry(prop_name.to_string())
|
||||
.or_default()
|
||||
.push(prop_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(href) = href.take() {
|
||||
if !prop.is_empty() {
|
||||
props.push(prop);
|
||||
}
|
||||
result.hrefs.insert(
|
||||
href,
|
||||
DavProperties {
|
||||
status: href_status,
|
||||
props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub async fn patch_and_check<T>(
|
||||
&self,
|
||||
path: &str,
|
||||
properties: impl IntoIterator<Item = (T, &str)>,
|
||||
) where
|
||||
T: AsRef<str> + Clone,
|
||||
{
|
||||
let mut expect_set = Vec::new();
|
||||
let mut expect_remove = Vec::new();
|
||||
|
||||
for (key, value) in properties {
|
||||
if !value.is_empty() {
|
||||
expect_set.push((key, value));
|
||||
} else {
|
||||
expect_remove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
let response = self
|
||||
.proppatch(
|
||||
path,
|
||||
expect_set.iter().cloned(),
|
||||
expect_remove.iter().cloned(),
|
||||
[],
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None);
|
||||
let patch_prop = response.properties(path);
|
||||
for (key, _) in &expect_set {
|
||||
patch_prop.get(key.as_ref()).with_status(StatusCode::OK);
|
||||
}
|
||||
for key in &expect_remove {
|
||||
patch_prop
|
||||
.get(key.as_ref())
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.propfind(
|
||||
path,
|
||||
expect_set
|
||||
.iter()
|
||||
.map(|(k, _)| k)
|
||||
.chain(expect_remove.iter()),
|
||||
)
|
||||
.await;
|
||||
let prop = response.properties(path);
|
||||
|
||||
for (key, value) in expect_set {
|
||||
prop.get(key.as_ref())
|
||||
.with_values([value])
|
||||
.with_status(StatusCode::OK);
|
||||
}
|
||||
|
||||
for key in expect_remove {
|
||||
prop.get(key.as_ref()).with_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
{
|
||||
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\" xmlns:C=\"http://calendarserver.org/ns/\">",
|
||||
"<D:prop>"
|
||||
)
|
||||
.to_string();
|
||||
|
||||
for property in properties {
|
||||
request.push_str(&format!("<{}/>", property.as_ref()));
|
||||
}
|
||||
|
||||
request.push_str("</D:prop></D:propfind>");
|
||||
|
||||
self.request_with_headers("PROPFIND", path, headers, &request)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None)
|
||||
}
|
||||
|
||||
pub async fn proppatch<T>(
|
||||
&self,
|
||||
path: &str,
|
||||
set: impl IntoIterator<Item = (T, &str)>,
|
||||
clear: impl IntoIterator<Item = T>,
|
||||
headers: impl IntoIterator<Item = (&'static str, &str)>,
|
||||
) -> DavResponse
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let mut request = concat!(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
|
||||
"<D:propertyupdate xmlns:D=\"DAV:\" xmlns:A=\"urn:ietf:params:xml:ns:caldav\" ",
|
||||
"xmlns:B=\"urn:ietf:params:xml:ns:carddav\" xmlns:C=\"http://calendarserver.org/ns/\">",
|
||||
"<D:remove><D:prop>"
|
||||
)
|
||||
.to_string();
|
||||
|
||||
for property in clear {
|
||||
request.push_str(&format!("<{}/>", property.as_ref()));
|
||||
}
|
||||
|
||||
request.push_str("</D:prop></D:remove><D:set><D:prop>");
|
||||
|
||||
for (key, value) in set {
|
||||
let key = key.as_ref();
|
||||
request.push_str(&format!("<{key}>{value}</{key}>"));
|
||||
}
|
||||
|
||||
request.push_str("</D:prop></D:set></D:propertyupdate>");
|
||||
|
||||
self.request_with_headers("PROPPATCH", path, headers, &request)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl DavItem {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
&& self.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED
|
||||
&& self.error.is_empty()
|
||||
&& self.description.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DavItem {
|
||||
fn default() -> Self {
|
||||
DavItem {
|
||||
status: StatusCode::PROXY_AUTHENTICATION_REQUIRED,
|
||||
values: AHashMap::new(),
|
||||
error: Vec::new(),
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -1268,10 +776,3 @@ pub const ALL_DAV_PROPERTIES: &[DavProperty] = &[
|
||||
DavProperty::Principal(PrincipalProperty::ScheduleInboxURL),
|
||||
DavProperty::Principal(PrincipalProperty::ScheduleOutboxURL),
|
||||
];
|
||||
|
||||
fn serialize_status_code<S>(status_code: &StatusCode, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&status_code.to_string())
|
||||
}
|
||||
|
||||
@@ -290,47 +290,3 @@ pub async fn test(test: &WebDavTest) {
|
||||
client.delete_default_containers().await;
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
impl DummyWebDavClient {
|
||||
pub async fn sync_collection(
|
||||
&self,
|
||||
path: &str,
|
||||
sync_token: &str,
|
||||
depth: Depth,
|
||||
limit: Option<usize>,
|
||||
properties: impl IntoIterator<Item = &str>,
|
||||
) -> DavResponse {
|
||||
let mut request = concat!(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>",
|
||||
"<D:sync-collection 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:sync-token>");
|
||||
request.push_str(sync_token);
|
||||
request.push_str("</D:sync-token><D:sync-level>");
|
||||
request.push_str(match depth {
|
||||
Depth::One => "1",
|
||||
Depth::Infinity => "infinite",
|
||||
_ => "0",
|
||||
});
|
||||
request.push_str("</D:sync-level>");
|
||||
|
||||
if let Some(limit) = limit {
|
||||
request.push_str("<D:limit><D:nresults>");
|
||||
request.push_str(&limit.to_string());
|
||||
request.push_str("</D:nresults></D:limit>");
|
||||
}
|
||||
|
||||
request.push_str("</D:sync-collection>");
|
||||
|
||||
self.request("REPORT", path, &request)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user