//! RFC 4918 §9.2 PROPPATCH compliance — dead property storage and retrieval. use reqwest::Method; use super::harness::{get_server, unique_name}; fn propfind() -> Method { Method::from_bytes(b"PROPFIND").unwrap() } fn proppatch() -> Method { Method::from_bytes(b"PROPPATCH").unwrap() } /// PROPPATCH set a custom property → 207 with 200 propstat. #[tokio::test] async fn proppatch_set_returns_207() { let srv = get_server(); let path = format!("/webdav/{}", unique_name("pp_set")); let (k, v) = srv.auth(); srv.client() .put(srv.url(&path)) .header(k, v.clone()) .body("x") .send() .await .unwrap(); let xml = r#" Alice "#; let res = srv .client() .request(proppatch(), srv.url(&path)) .header(k, v) .header("Content-Type", "application/xml") .body(xml) .send() .await .unwrap(); assert_eq!(res.status(), 207, "PROPPATCH must return 207"); let body = res.text().await.unwrap(); assert!( body.contains("200") || body.contains("HTTP/1.1 200"), "PROPPATCH 207 must contain 200 propstat; body: {body}" ); } /// PROPPATCH set → PROPFIND retrieves the stored value. #[tokio::test] async fn proppatch_set_property_visible_in_propfind() { let srv = get_server(); let path = format!("/webdav/{}", unique_name("pp_roundtrip")); let (k, v) = srv.auth(); srv.client() .put(srv.url(&path)) .header(k, v.clone()) .body("data") .send() .await .unwrap(); // Set dead property let set_xml = r#" blue "#; let pp_res = srv .client() .request(proppatch(), srv.url(&path)) .header(k, v.clone()) .header("Content-Type", "application/xml") .body(set_xml) .send() .await .unwrap(); assert_eq!(pp_res.status(), 207, "PROPPATCH set must return 207"); // Retrieve via PROPFIND allprop let pf_res = srv .client() .request(propfind(), srv.url(&path)) .header(k, v) .header("Depth", "0") .send() .await .unwrap(); assert_eq!(pf_res.status(), 207); let body = pf_res.text().await.unwrap(); assert!( body.contains("color") || body.contains("blue"), "PROPFIND allprop must include dead property set by PROPPATCH; body: {body}" ); } /// PROPPATCH remove → property absent from subsequent PROPFIND. #[tokio::test] async fn proppatch_remove_property_not_in_propfind() { let srv = get_server(); let path = format!("/webdav/{}", unique_name("pp_remove")); let (k, v) = srv.auth(); srv.client() .put(srv.url(&path)) .header(k, v.clone()) .body("data") .send() .await .unwrap(); // First set let set_xml = r#" removeme "#; srv.client() .request(proppatch(), srv.url(&path)) .header(k, v.clone()) .header("Content-Type", "application/xml") .body(set_xml) .send() .await .unwrap(); // Then remove let remove_xml = r#" "#; let rem_res = srv .client() .request(proppatch(), srv.url(&path)) .header(k, v.clone()) .header("Content-Type", "application/xml") .body(remove_xml) .send() .await .unwrap(); assert_eq!(rem_res.status(), 207, "PROPPATCH remove must return 207"); // Verify gone — request the specific prop, expect 404 propstat let pf_xml = r#" "#; let pf_res = srv .client() .request(propfind(), srv.url(&path)) .header(k, v) .header("Depth", "0") .header("Content-Type", "application/xml") .body(pf_xml) .send() .await .unwrap(); assert_eq!(pf_res.status(), 207); let body = pf_res.text().await.unwrap(); assert!( body.contains("404"), "Removed dead property must appear in 404 propstat; body: {body}" ); } /// PROPPATCH set + remove in same request → both applied atomically. #[tokio::test] async fn proppatch_set_and_remove_in_same_request() { let srv = get_server(); let path = format!("/webdav/{}", unique_name("pp_setrem")); let (k, v) = srv.auth(); srv.client() .put(srv.url(&path)) .header(k, v.clone()) .body("x") .send() .await .unwrap(); // Pre-seed a property to remove let seed_xml = r#" gone "#; srv.client() .request(proppatch(), srv.url(&path)) .header(k, v.clone()) .header("Content-Type", "application/xml") .body(seed_xml) .send() .await .unwrap(); // Set new + remove old in one request let xml = r#" here "#; let res = srv .client() .request(proppatch(), srv.url(&path)) .header(k, v) .header("Content-Type", "application/xml") .body(xml) .send() .await .unwrap(); assert_eq!(res.status(), 207, "combined set+remove must return 207"); let body = res.text().await.unwrap(); // Both ops should succeed assert!( !body.contains("409") && !body.contains("403"), "combined PROPPATCH must not fail; body: {body}" ); } /// PROPPATCH on non-existent resource → 404. #[tokio::test] async fn proppatch_nonexistent_resource_returns_404() { let srv = get_server(); let path = format!("/webdav/{}", unique_name("pp_ghost")); let (k, v) = srv.auth(); let xml = r#" y "#; let res = srv .client() .request(proppatch(), srv.url(&path)) .header(k, v) .header("Content-Type", "application/xml") .body(xml) .send() .await .unwrap(); assert_eq!( res.status(), 404, "PROPPATCH on non-existent resource must return 404" ); } /// PROPPATCH on collection (folder) → 207. #[tokio::test] async fn proppatch_on_collection_returns_207() { let srv = get_server(); let col = format!("/webdav/{}", unique_name("pp_col")); let (k, v) = srv.auth(); srv.client() .request(Method::from_bytes(b"MKCOL").unwrap(), srv.url(&col)) .header(k, v.clone()) .send() .await .unwrap(); let xml = r#" my folder "#; let res = srv .client() .request(proppatch(), srv.url(&col)) .header(k, v) .header("Content-Type", "application/xml") .body(xml) .send() .await .unwrap(); assert_eq!(res.status(), 207, "PROPPATCH on collection must return 207"); } /// PROPFIND specific dead property returns value in 200 propstat (not 404). #[tokio::test] async fn propfind_specific_dead_property_returns_200_propstat() { let srv = get_server(); let path = format!("/webdav/{}", unique_name("pp_specific")); let (k, v) = srv.auth(); srv.client() .put(srv.url(&path)) .header(k, v.clone()) .body("x") .send() .await .unwrap(); // Set let set_xml = r#" 5 "#; srv.client() .request(proppatch(), srv.url(&path)) .header(k, v.clone()) .header("Content-Type", "application/xml") .body(set_xml) .send() .await .unwrap(); // PROPFIND for that exact property let pf_xml = r#" "#; let pf_res = srv .client() .request(propfind(), srv.url(&path)) .header(k, v) .header("Depth", "0") .header("Content-Type", "application/xml") .body(pf_xml) .send() .await .unwrap(); assert_eq!(pf_res.status(), 207); let body = pf_res.text().await.unwrap(); assert!( !body.contains("404"), "Known dead property must not be in 404 propstat; body: {body}" ); assert!( body.contains("rating") || body.contains("5"), "Response must include the dead property value; body: {body}" ); }