//! 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}"
);
}