libdav 0.10.3

CalDAV and CardDAV client implementations.
Documentation
//! Get the etag of a resource.

use crate::{
    Depth,
    dav::{Propfind, WebDavError},
    names,
    requests::{DavRequest, ParseResponseError, PreparedRequest},
    xmlutils::validate_xml_response,
};

/// Request to get the Etag of a resource.
///
/// Sends a PROPFIND request with depth 0 to fetch only the `getetag` property.
///
/// # Example
///
/// ```
/// # use libdav::dav::GetEtag;
/// # use libdav::dav::WebDavClient;
/// # use tower_service::Service;
/// # async fn example<C>(webdav: &WebDavClient<C>) -> Result<(), Box<dyn std::error::Error>>
/// # where
/// #     C: Service<http::Request<String>, Response = http::Response<hyper::body::Incoming>> + Send + Sync,
/// #     C::Error: std::error::Error + Send + Sync,
/// # {
/// let response = webdav.request(
///     GetEtag::new("/calendar/event.ics")
/// ).await?;
///
/// println!("Etag: {}", response.etag);
/// # Ok(())
/// # }
/// ```
pub struct GetEtag<'a> {
    propfind: Propfind<'a>,
}

impl<'a> GetEtag<'a> {
    /// Create a new `GetEtag` request for the given resource href.
    ///
    /// The href should be a path relative to the server's base URL.
    #[must_use]
    pub fn new(href: &'a str) -> Self {
        Self {
            propfind: Propfind::new(href)
                .with_properties(&[&names::GETETAG])
                .with_depth(Depth::Zero),
        }
    }
}

/// Response from a `GetEtag` request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetEtagResponse {
    /// Entity tag (etag) of the resource.
    ///
    /// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag>
    pub etag: String,
}

impl DavRequest for GetEtag<'_> {
    type Response = GetEtagResponse;
    type ParseError = ParseResponseError;
    type Error<E> = WebDavError<E>;

    // FIXME: error type needs refinement
    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 etag_node = root
            .descendants()
            .find(|node| node.tag_name() == names::GETETAG)
            .ok_or_else(|| {
                ParseResponseError::InvalidResponse("missing getetag in response".into())
            })?;

        let etag = etag_node
            .text()
            .ok_or_else(|| ParseResponseError::InvalidResponse("missing text in getetag".into()))?
            .to_string();

        Ok(GetEtagResponse { etag })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::{Method, StatusCode};

    #[test]
    fn test_prepare_request() {
        let req = GetEtag::new("/calendar/event.ics");
        let prepared = req.prepare_request().unwrap();

        assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
        assert_eq!(prepared.path, "/calendar/event.ics");
        assert_eq!(
            prepared.body,
            r#"<D:propfind xmlns:D="DAV:"><D:prop><D:getetag/></D:prop></D:propfind>"#
        );
        assert_eq!(
            prepared.headers,
            vec![
                ("Depth".to_string(), "0".to_string()),
                (
                    "Content-Type".to_string(),
                    "application/xml; charset=utf-8".to_string()
                )
            ]
        );
    }

    #[test]
    fn test_parse_response() {
        let req = GetEtag::new("/calendar/event.ics");
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:">
                <response>
                    <href>/calendar/event.ics</href>
                    <propstat>
                        <prop><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, response_xml.as_bytes()).unwrap();

        assert_eq!(result.etag, "\"abc123\"");
    }

    #[test]
    fn test_parse_response_missing_etag() {
        let req = GetEtag::new("/calendar/event.ics");
        let response_xml = r#"<?xml version="1.0"?>
            <multistatus xmlns="DAV:">
                <response>
                    <href>/calendar/event.ics</href>
                    <propstat>
                        <prop></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, response_xml.as_bytes());

        assert!(result.is_err());
    }

    #[test]
    fn test_parse_response_bad_status() {
        let req = GetEtag::new("/calendar/event.ics");
        let response_xml = b"";

        let response = http::Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(())
            .unwrap();
        let (parts, ()) = response.into_parts();
        let result = req.parse_response(&parts, response_xml);

        assert!(result.is_err());
    }
}