use crate::{
Depth, PropertyName,
dav::{Propfind, WebDavError},
requests::{DavRequest, ParseResponseError, PreparedRequest},
xmlutils::{check_multistatus, validate_xml_response},
};
pub struct GetProperty<'a> {
property: &'a PropertyName<'a, 'a>,
propfind: Propfind<'a>,
}
impl<'a> GetProperty<'a> {
#[must_use]
pub fn new(href: &'a str, property: &'a PropertyName<'a, 'a>) -> Self {
Self {
property,
propfind: Propfind::new(href)
.with_properties(&[property])
.with_depth(Depth::Zero),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetPropertyResponse {
pub value: Option<String>,
}
impl DavRequest for GetProperty<'_> {
type Response = GetPropertyResponse;
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 prop = root
.descendants()
.find(|node| node.tag_name() == *self.property)
.or_else(|| {
root.descendants()
.find(|node| node.tag_name().name() == self.property.name())
});
check_multistatus(root)?;
if let Some(prop) = prop {
return Ok(GetPropertyResponse {
value: prop.text().map(str::to_string),
});
}
Err(ParseResponseError::InvalidResponse(
"Property is missing from response, but response is non-error.".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::names;
use http::{Method, StatusCode};
#[test]
fn test_prepare_request() {
let req = GetProperty::new("/calendars/personal/", &names::DISPLAY_NAME);
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(r#"xmlns:D="DAV:""#));
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "0".to_string()))
);
}
#[test]
fn test_parse_response_success_with_text() {
let req = GetProperty::new("/calendars/personal/", &names::DISPLAY_NAME);
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.value, Some("Personal Calendar".to_string()));
}
#[test]
fn test_parse_response_success_with_cdata() {
let req = GetProperty::new("/calendars/personal/", &names::DISPLAY_NAME);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname><![CDATA[My 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.value, Some("My Calendar".to_string()));
}
#[test]
fn test_parse_response_empty_property() {
let req = GetProperty::new("/calendars/personal/", &names::DISPLAY_NAME);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<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.value, None);
}
#[test]
fn test_parse_response_property_not_found_404() {
let req = GetProperty::new("/calendars/personal/", &names::DISPLAY_NAME);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname/>
</prop>
<status>HTTP/1.1 404 Not Found</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);
assert!(matches!(
result,
Err(ParseResponseError::BadStatusCode(StatusCode::NOT_FOUND))
));
}
#[test]
fn test_parse_response_bad_http_status() {
let req = GetProperty::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 = GetProperty::new("/calendars/personal/", &names::CALENDAR_COLOUR);
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>
</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.value, Some("#ff0000".to_string()));
}
}