From f9fab60e5ee77c0cf57744e39b5685902189a38b Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 29 May 2024 08:47:56 +0200 Subject: [PATCH] test report sync-collection --- aero-collections/src/calendar/mod.rs | 4 + aero-dav/src/syncencoder.rs | 1 - aero-proto/src/dav/controller.rs | 2 +- aero-proto/src/dav/resource.rs | 36 ++++- aerogramme/tests/behavior.rs | 188 ++++++++++++++++++++++++++- aerogramme/tests/common/mod.rs | 19 ++- 6 files changed, 240 insertions(+), 10 deletions(-) diff --git a/aero-collections/src/calendar/mod.rs b/aero-collections/src/calendar/mod.rs index 414426a..ac07842 100644 --- a/aero-collections/src/calendar/mod.rs +++ b/aero-collections/src/calendar/mod.rs @@ -177,6 +177,10 @@ impl CalendarInternal { .iter() .filter_map(|t: &Token| davstate.change.get(t)) .map(|s| s.clone()) + .filter(|s| match s { + SyncChange::Ok((filename, _)) => davstate.idx_by_filename.get(filename).is_some(), + SyncChange::NotFound(filename) => davstate.idx_by_filename.get(filename).is_none(), + }) .collect(); let token = self.current_token().await?; diff --git a/aero-dav/src/syncencoder.rs b/aero-dav/src/syncencoder.rs index 2dd50eb..55f7ad6 100644 --- a/aero-dav/src/syncencoder.rs +++ b/aero-dav/src/syncencoder.rs @@ -128,7 +128,6 @@ mod tests { src.qwrite(&mut writer).await.expect("xml serialization"); tokio_buffer.flush().await.expect("tokio buffer flush"); let got = std::str::from_utf8(buffer.as_slice()).unwrap(); - println!("{:?}", got); // deserialize let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes())) diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs index 7e1f416..76dd15d 100644 --- a/aero-proto/src/dav/controller.rs +++ b/aero-proto/src/dav/controller.rs @@ -184,7 +184,7 @@ impl Controller { }, }; extension = Some(realization::Multistatus::Sync(sync::Multistatus { - sync_token: sync::SyncToken(new_token.to_string()), + sync_token: sync::SyncToken(format!("{}{}", BASE_TOKEN_URI, new_token)), })); } _ => { diff --git a/aero-proto/src/dav/resource.rs b/aero-proto/src/dav/resource.rs index 297a1c1..b6c0ed5 100644 --- a/aero-proto/src/dav/resource.rs +++ b/aero-proto/src/dav/resource.rs @@ -21,6 +21,16 @@ use aero_dav::versioningtypes as vers; use super::node::PropertyStream; use crate::dav::node::{Content, DavNode, PutPolicy}; +/// Why "https://aerogramme.0"? +/// Because tokens must be valid URI. +/// And numeric TLD are ~mostly valid in URI (check the .42 TLD experience) +/// and at the same time, they are not used sold by the ICANN and there is no plan to use them. +/// So I am sure that the URL remains invalid, avoiding leaking requests to an hardcoded URL in the +/// future. +/// The best option would be to make it configurable ofc, so someone can put a domain name +/// that they control, it would probably improve compatibility (maybe some WebDAV spec tells us +/// how to handle/resolve this URI but I am not aware of that...). But that's not the plan for +/// now. So here we are: https://aerogramme.0. pub const BASE_TOKEN_URI: &str = "https://aerogramme.0/sync/"; #[derive(Clone)] @@ -575,7 +585,31 @@ impl DavNode for CalendarNode { let col = self.col.clone(); let calname = self.calname.clone(); async move { - let sync_token = sync_token.unwrap(); + let sync_token = match sync_token { + Some(v) => v, + None => { + let token = col + .token() + .await + .or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)))?; + let ok_nodes = col + .dag() + .await + .idx_by_filename + .iter() + .map(|(filename, blob_id)| { + Box::new(EventNode { + col: col.clone(), + calname: calname.clone(), + filename: filename.to_string(), + blob_id: *blob_id, + }) as Box + }) + .collect(); + + return Ok((token, ok_nodes, vec![])); + } + }; let (new_token, listed_changes) = match col.diff(sync_token).await { Ok(v) => v, Err(e) => { diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs index 1846c92..d7fb6e9 100644 --- a/aerogramme/tests/behavior.rs +++ b/aerogramme/tests/behavior.rs @@ -370,7 +370,7 @@ use aero_dav::synctypes as sync; use aero_dav::types as dav; use aero_dav::versioningtypes as vers; -use crate::common::dav_deserialize; +use crate::common::{dav_deserialize, dav_serialize}; fn rfc4918_webdav_core() { println!("🧪 rfc4918_webdav_core"); @@ -435,6 +435,7 @@ fn rfc4918_webdav_core() { assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Value(dav::Property::GetContentType(_)))).is_none()); assert!(root_not_found.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::GetContentLength))).is_some()); + // -- HIERARCHY EXPLORATION WITH THE DEPTH: X HEADER FIELD -- // depth 1 / -> /alice/ let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").header("Depth", "1").send()?.text()?; let multistatus = dav_deserialize::>(&body); @@ -470,7 +471,7 @@ fn rfc4918_webdav_core() { let multistatus = dav_deserialize::>(&body); assert_eq!(multistatus.responses.len(), 1); - // --- PUT --- + // --- PUT (add objets) --- // first object let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-None-Match", "*").body(ICAL_RFC2).send()?; let obj1_etag = resp.headers().get("etag").expect("etag must be set"); @@ -496,14 +497,14 @@ fn rfc4918_webdav_core() { let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-Match", obj1_etag).body(ICAL_RFC1).send()?; assert_eq!(resp.status(), 201); - // --- GET --- + // --- GET (fetch objects) --- let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?.text()?; assert_eq!(body.as_bytes(), ICAL_RFC1); let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc3.ics").send()?.text()?; assert_eq!(body.as_bytes(), ICAL_RFC3); - // --- DELETE --- + // --- DELETE (delete objects) --- // delete 1st object let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?; assert_eq!(resp.status(), 204); @@ -528,7 +529,7 @@ fn rfc4918_webdav_core() { fn rfc5397_webdav_principal() { println!("🧪 rfc5397_webdav_principal"); common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { - // Find principal + // -- AUTODISCOVERY: FIND "PRINCIPAL" AS DEFINED IN WEBDAV ACL (~USER'S HOME) -- let propfind_req = r#""#; let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?; let multistatus = dav_deserialize::>(&body); @@ -1017,7 +1018,8 @@ fn rfc4791_webdav_caldav() { fn rfc6578_webdav_sync() { println!("🧪 rfc6578_webdav_sync"); common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { - // propname on a calendar node must return + (2nd element is theoretically from versioning) + // -- PROPFIND -- + // propname must return sync-token & supported-report-set (from webdav versioning) let propfind_req = r#""#; let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?; let multistatus = dav_deserialize::>(&body); @@ -1110,6 +1112,180 @@ fn rfc6578_webdav_sync() { assert!(init_sync_token != del_sync_token); assert!(rfc1_sync_token != del_sync_token); + // -- TEST SYNC CUSTOM REPORT: SYNC-COLLECTION -- + // 3.8. Example: Initial DAV:sync-collection Report + // Part 1: check the empty case + let sync_query = r#" + + + 1 + + + + + "#; + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(sync_query) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + assert_eq!(multistatus.responses.len(), 0); + let empty_token = match &multistatus.extension { + Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x, + _ => anyhow::bail!("wrong content"), + }; + + // Part 2: check with one file + let resp = http + .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics") + .header("If-None-Match", "*") + .body(ICAL_RFC1) + .send()?; + assert_eq!(resp.status(), 201); + + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(sync_query) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + assert_eq!(multistatus.responses.len(), 1); + let initial_one_file_token = match &multistatus.extension { + Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x, + _ => anyhow::bail!("wrong content"), + }; + assert!(empty_token != initial_one_file_token); + + // 3.9. Example: DAV:sync-collection Report with Token + // Part 1: nothing changed, response must be empty + let sync_query = |token: &str| vers::Report::::Extension(realization::ReportType::Sync(sync::SyncCollection { + sync_token: sync::SyncTokenRequest::IncrementalSync(token.into()), + sync_level: sync::SyncLevel::One, + limit: None, + prop: dav::PropName(vec![ + dav::PropertyRequest::GetEtag, + ]), + })); + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(dav_serialize(&sync_query(initial_one_file_token))) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + assert_eq!(multistatus.responses.len(), 0); + let no_change = match &multistatus.extension { + Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x, + _ => anyhow::bail!("wrong content"), + }; + assert_eq!(initial_one_file_token, no_change); + + // Part 2: add a new node (rfc2) + remove a node (rfc1) + // add rfc2 + let resp = http + .put("http://localhost:8087/alice/calendar/Personal/rfc2.ics") + .header("If-None-Match", "*") + .body(ICAL_RFC2) + .send()?; + assert_eq!(resp.status(), 201); + + // delete rfc1 + let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc1.ics").send()?; + assert_eq!(resp.status(), 204); + + // call REPORT + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(dav_serialize(&sync_query(initial_one_file_token))) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + assert_eq!(multistatus.responses.len(), 2); + let token_addrm = match &multistatus.extension { + Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x, + _ => anyhow::bail!("wrong content"), + }; + assert!(initial_one_file_token != token_addrm); + + // Part 3: remove a node (rfc2) and add it again with new content + // delete rfc2 + let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?; + assert_eq!(resp.status(), 204); + + // add rfc2 with ICAL_RFC3 content + let resp = http + .put("http://localhost:8087/alice/calendar/Personal/rfc2.ics") + .header("If-None-Match", "*") + .body(ICAL_RFC3) + .send()?; + let rfc2_etag = resp.headers().get("etag").expect("etag must be set"); + assert_eq!(resp.status(), 201); + + // call REPORT + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(dav_serialize(&sync_query(token_addrm))) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + assert_eq!(multistatus.responses.len(), 1); + let token_addrm_same = match &multistatus.extension { + Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x, + _ => anyhow::bail!("wrong content"), + }; + assert!(token_addrm_same != token_addrm); + + // Part 4: overwrite an event (rfc1) with new content + let resp = http + .put("http://localhost:8087/alice/calendar/Personal/rfc1.ics") + .header("If-Match", rfc2_etag) + .body(ICAL_RFC4) + .send()?; + assert_eq!(resp.status(), 201); + + // call REPORT + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(dav_serialize(&sync_query(token_addrm_same))) + .send()?; + assert_eq!(resp.status(), 207); + let multistatus = dav_deserialize::>(&resp.text()?); + assert_eq!(multistatus.responses.len(), 1); + let token_addrm_same = match &multistatus.extension { + Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x, + _ => anyhow::bail!("wrong content"), + }; + assert!(token_addrm_same != token_addrm); + + // Unknown token must return 410 GONE. + // Token can be forgotten as we garbage collect the DAG. + let resp = http + .request( + reqwest::Method::from_bytes(b"REPORT")?, + "http://localhost:8087/alice/calendar/Personal/", + ) + .body(dav_serialize(&sync_query("https://aerogramme.0/sync/000000000000000000000000000000000000000000000000"))) + .send()?; + assert_eq!(resp.status(), 410); + Ok(()) }) .expect("test fully run") diff --git a/aerogramme/tests/common/mod.rs b/aerogramme/tests/common/mod.rs index 12f2764..bc65305 100644 --- a/aerogramme/tests/common/mod.rs +++ b/aerogramme/tests/common/mod.rs @@ -108,7 +108,8 @@ pub fn read_first_u32(inp: &str) -> Result { .parse::()?) } -use aero_dav::xml::{Node, Reader}; +use aero_dav::xml::{Node, Reader, Writer}; +use tokio::io::AsyncWriteExt; pub fn dav_deserialize>(src: &str) -> T { futures::executor::block_on(async { let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes())) @@ -117,3 +118,19 @@ pub fn dav_deserialize>(src: &str) -> T { rdr.find().await.expect("parse XML") }) } +pub fn dav_serialize>(src: &T) -> String { + futures::executor::block_on(async { + let mut buffer = Vec::new(); + let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); + let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); + let ns_to_apply = vec![ + ("xmlns:D".into(), "DAV:".into()), + ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()), + ]; + let mut writer = Writer { q, ns_to_apply }; + + src.qwrite(&mut writer).await.expect("xml serialization"); + tokio_buffer.flush().await.expect("tokio buffer flush"); + std::str::from_utf8(buffer.as_slice()).unwrap().into() + }) +}