~whynothugo/vdirsyncer-rs

1f05f0fec76f42302a30c9ded4c48a71fa2c7ce8 — Hugo Osvaldo Barrera 2 months ago 66e67bf
Add WebDavClient::get_properties

Allows fetching multiple properties in a single network around trip.
Tested against Xandikos and Fastmail.
3 files changed, 104 insertions(+), 0 deletions(-)

M libdav/src/dav.rs
M live_tests/src/caldav.rs
M live_tests/src/main.rs
M libdav/src/dav.rs => libdav/src/dav.rs +48 -0
@@ 296,6 296,54 @@ where
        parse_prop(body, property)
    }

    /// Fetch multiple properties for a single resource.
    ///
    /// # Quirks
    ///
    /// Same as [`WebDavClient::get_property`].
    ///
    /// # Errors
    ///
    /// - If there are any network errors or the response could not be parsed.
    /// - If the requested property is missing in the response.
    ///
    /// # See also
    ///
    /// - [`WebDavClient::get_property`]
    /// - [`WebDavClient::set_property`]
    pub async fn get_properties<'p>(
        &self,
        href: &str,
        properties: &[&PropertyName<'p, 'p>],
    ) -> Result<Vec<(PropertyName<'p, 'p>, Option<String>)>, WebDavError> {
        let url = self.relative_uri(href)?;

        let (head, body) = self.propfind(&url, properties, 0).await?;
        check_status(head.status)?;

        let body = std::str::from_utf8(body.as_ref())?;
        let doc = roxmltree::Document::parse(body)?;
        let root = doc.root_element();

        let mut results = Vec::with_capacity(properties.len());
        for property in properties {
            let prop = root
                .descendants()
                .find(|node| node.tag_name() == **property)
                // Hack to work around: https://github.com/cyrusimap/cyrus-imapd/issues/4489
                .or_else(|| {
                    root.descendants()
                        .find(|node| node.tag_name().name() == property.name())
                })
                // End hack
                .and_then(|p| p.text())
                .map(str::to_owned);

            results.push((**property, prop));
        }
        Ok(results)
    }

    /// Sends a `PROPUPDATE` query to the server.
    ///
    /// Setting the value to `None` will remove the property. Returns the new value as returned by

M live_tests/src/caldav.rs => live_tests/src/caldav.rs +55 -0
@@ 158,6 158,61 @@ pub(crate) async fn test_setting_and_getting_colour(test_data: &TestData) -> any
    Ok(())
}

pub(crate) async fn test_get_properties(test_data: &TestData) -> anyhow::Result<()> {
    let new_collection = format!(
        "{}{}/",
        test_data.calendar_home_set.path(),
        &random_string(16)
    );
    test_data.caldav.create_calendar(&new_collection).await?;

    let colour = "#ff00ff";
    let colour_alpha = "#FF00FFFF"; // Some servers normalise to this value.
    test_data
        .caldav
        .set_property(&new_collection, &names::CALENDAR_COLOUR, Some(colour))
        .await
        .context("setting collection colour")?;

    let name = "panda-events";
    test_data
        .caldav
        .set_property(&new_collection, &names::DISPLAY_NAME, Some(name))
        .await
        .context("setting collection displayname")?;

    let values = test_data
        .caldav
        .get_properties(
            &new_collection,
            &[
                &names::CALENDAR_COLOUR,
                &names::DISPLAY_NAME,
                &names::CALENDAR_ORDER,
            ],
        )
        .await
        .context("getting collection properties")?;

    for value in values {
        match value.0 {
            names::CALENDAR_COLOUR => match value.1 {
                Some(c) => {
                    ensure!(c.eq_ignore_ascii_case(colour) || c.eq_ignore_ascii_case(colour_alpha));
                }
                None => bail!("Set a colour but then got colour None"),
            },
            names::DISPLAY_NAME => ensure!(value.1 == Some("panda-events".into())),
            names::CALENDAR_ORDER => ensure!(value.1 == None),
            _ => bail!("got unexpected property"),
        }
    }

    test_data.caldav.force_delete(&new_collection).await?;

    Ok(())
}

fn minimal_icalendar() -> anyhow::Result<Vec<u8>> {
    let mut entry = String::new();
    let uid = random_string(12);

M live_tests/src/main.rs => live_tests/src/main.rs +1 -0
@@ 252,6 252,7 @@ async fn main() -> anyhow::Result<()> {

    let (total, passed) = run_tests!(
        &test_data,
        caldav::test_get_properties,
        caldav::test_create_and_delete_collection,
        caldav::test_create_and_force_delete_collection,
        caldav::test_setting_and_getting_displayname,