use http::Method;
use crate::{
PropertyName, names,
requests::{DavRequest, ParseResponseError, PreparedRequest, xml_content_type_header},
xmlutils::{XmlNode, check_multistatus, validate_xml_response},
};
pub struct SetProperty<'a> {
href: &'a str,
property: &'a PropertyName<'a, 'a>,
value: Option<&'a str>,
}
impl<'a> SetProperty<'a> {
#[must_use]
pub fn new(href: &'a str, property: &'a PropertyName<'a, 'a>, value: Option<&'a str>) -> Self {
Self {
href,
property,
value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetPropertyResponse {
pub value: Option<String>,
}
impl DavRequest for SetProperty<'_> {
type Response = SetPropertyResponse;
type ParseError = ParseResponseError;
type Error<E> = crate::dav::WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
let mut property_node = XmlNode::new(self.property);
if let Some(value) = self.value {
property_node.characters = value;
}
let mut prop = XmlNode::new(&names::PROP);
prop.children = vec![property_node];
let action_name = if self.value.is_some() {
&names::SET
} else {
&names::REMOVE
};
let mut action = XmlNode::new(action_name);
action.children = vec![prop];
let mut propertyupdate = XmlNode::new(&names::PROPERTYUPDATE);
propertyupdate.children = vec![action];
Ok(PreparedRequest {
method: Method::from_bytes(b"PROPPATCH")?,
path: self.href.to_string(),
body: propertyupdate.render_node(),
headers: vec![xml_content_type_header()],
})
}
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(SetPropertyResponse {
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::StatusCode;
#[test]
fn test_prepare_request_set() {
let req = SetProperty::new(
"/calendars/personal/",
&names::DISPLAY_NAME,
Some("My Calendar"),
);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPPATCH").unwrap());
assert_eq!(prepared.path, "/calendars/personal/");
assert!(prepared.body.contains("D:set"));
assert!(prepared.body.contains("displayname"));
assert!(prepared.body.contains("My Calendar"));
assert!(prepared.body.contains(r#"xmlns:D="DAV:""#));
}
#[test]
fn test_prepare_request_remove() {
let req = SetProperty::new("/calendars/personal/", &names::DISPLAY_NAME, None);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPPATCH").unwrap());
assert!(prepared.body.contains("D:remove"));
assert!(prepared.body.contains("displayname"));
assert!(prepared.body.contains("/>"));
}
#[test]
fn test_prepare_request_escapes_value() {
let req = SetProperty::new(
"/calendars/personal/",
&names::DISPLAY_NAME,
Some("<Test & \"Calendar\">"),
);
let prepared = req.prepare_request().unwrap();
assert!(
prepared
.body
.contains("<Test & "Calendar">")
);
}
#[test]
fn test_parse_response_success() {
let req = SetProperty::new(
"/calendars/personal/",
&names::DISPLAY_NAME,
Some("My Calendar"),
);
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname>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_after_remove() {
let req = SetProperty::new("/calendars/personal/", &names::DISPLAY_NAME, None);
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_forbidden() {
let req = SetProperty::new(
"/calendars/personal/",
&names::DISPLAY_NAME,
Some("My Calendar"),
);
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 403 Forbidden</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::FORBIDDEN))
));
}
#[test]
fn test_parse_response_bad_http_status() {
let req = SetProperty::new(
"/calendars/personal/",
&names::DISPLAY_NAME,
Some("My Calendar"),
);
let response = http::Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"");
assert!(matches!(
result,
Err(ParseResponseError::BadStatusCode(StatusCode::UNAUTHORIZED))
));
}
#[test]
fn test_parse_response_cyrus_quirk() {
let req = SetProperty::new(
"/calendars/personal/",
&names::CALENDAR_COLOUR,
Some("#ff0000"),
);
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()));
}
}