use crate::{
Depth, PropertyName,
dav::{Propfind, WebDavError},
requests::{DavRequest, ParseResponseError, PreparedRequest},
xmlutils::validate_xml_response,
};
pub struct GetProperties<'p> {
propfind: Propfind<'p>,
}
impl<'p> GetProperties<'p> {
#[must_use]
pub fn new(href: &'p str, properties: &[&'p PropertyName<'p, 'p>]) -> Self {
Self {
propfind: Propfind::new(href)
.with_properties(properties)
.with_depth(Depth::Zero),
}
}
}
#[derive(Debug, Clone)]
pub struct GetPropertiesResponse<'p> {
pub values: Vec<(&'p PropertyName<'p, 'p>, Option<String>)>,
}
impl<'p> DavRequest for GetProperties<'p> {
type Response = GetPropertiesResponse<'p>;
type ParseError = ParseResponseError;
type Error<E> = WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
self.propfind.prepare_request()
}
fn parse_response(
&self,
parts: &http::response::Parts,
body: &[u8],
) -> Result<Self::Response, ParseResponseError> {
let doc = validate_xml_response(parts, body)?;
let root = doc.root_element();
let mut results = Vec::with_capacity(self.propfind.properties().len());
for property in self.propfind.properties() {
let prop = root
.descendants()
.find(|node| node.tag_name() == **property)
.or_else(|| {
root.descendants()
.find(|node| node.tag_name().name() == property.name())
})
.and_then(|p| p.text())
.map(str::to_owned);
results.push((*property, prop));
}
Ok(GetPropertiesResponse { values: results })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::names;
use http::{Method, StatusCode};
#[test]
fn test_prepare_request() {
let req = GetProperties::new(
"/calendars/personal/",
&[&names::DISPLAY_NAME, &names::GETETAG],
);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert_eq!(prepared.path, "/calendars/personal/");
assert!(prepared.body.contains("displayname"));
assert!(prepared.body.contains("getetag"));
assert!(prepared.body.contains(r#"xmlns:D="DAV:""#));
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "0".to_string()))
);
}
#[test]
fn test_parse_response_success() {
let req = GetProperties::new(
"/calendars/personal/",
&[&names::DISPLAY_NAME, &names::GETETAG],
);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname>Personal Calendar</displayname>
<getetag>"abc123"</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, body).unwrap();
assert_eq!(result.values.len(), 2);
assert_eq!(result.values[0].0, &names::DISPLAY_NAME);
assert_eq!(result.values[0].1, Some("Personal Calendar".to_string()));
assert_eq!(result.values[1].0, &names::GETETAG);
assert_eq!(result.values[1].1, Some("\"abc123\"".to_string()));
}
#[test]
fn test_parse_response_partial() {
let req = GetProperties::new(
"/calendars/personal/",
&[&names::DISPLAY_NAME, &names::GETETAG],
);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname>Personal Calendar</displayname>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, body).unwrap();
assert_eq!(result.values.len(), 2);
assert_eq!(result.values[0].1, Some("Personal Calendar".to_string()));
assert_eq!(result.values[1].1, None); }
#[test]
fn test_parse_response_bad_http_status() {
let req = GetProperties::new("/calendars/personal/", &[&names::DISPLAY_NAME]);
let response = http::Response::builder()
.status(StatusCode::FORBIDDEN)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"");
assert!(matches!(
result,
Err(ParseResponseError::BadStatusCode(StatusCode::FORBIDDEN))
));
}
#[test]
fn test_parse_response_cyrus_quirk() {
let req = GetProperties::new(
"/calendars/personal/",
&[&names::CALENDAR_COLOUR, &names::DISPLAY_NAME],
);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:X="http://wrong.namespace/">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<X:calendar-color>#ff0000</X:calendar-color>
<displayname>Test</displayname>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, body).unwrap();
assert_eq!(result.values.len(), 2);
assert_eq!(result.values[0].1, Some("#ff0000".to_string()));
assert_eq!(result.values[1].1, Some("Test".to_string()));
}
}