aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 08:47:56 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 08:47:56 +0200
commitf9fab60e5ee77c0cf57744e39b5685902189a38b (patch)
tree8434d66f7b38658edb4db7648527930f3f26aab0
parenta2f5b451bd32780d60be69c6412cb351a54b765b (diff)
downloadaerogramme-f9fab60e5ee77c0cf57744e39b5685902189a38b.tar.gz
aerogramme-f9fab60e5ee77c0cf57744e39b5685902189a38b.zip
test report sync-collection
-rw-r--r--aero-collections/src/calendar/mod.rs4
-rw-r--r--aero-dav/src/syncencoder.rs1
-rw-r--r--aero-proto/src/dav/controller.rs2
-rw-r--r--aero-proto/src/dav/resource.rs36
-rw-r--r--aerogramme/tests/behavior.rs188
-rw-r--r--aerogramme/tests/common/mod.rs19
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<dyn DavNode>
+ })
+ .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::<dav::Multistatus<All>>(&body);
@@ -470,7 +471,7 @@ fn rfc4918_webdav_core() {
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&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#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><current-user-principal/></prop></propfind>"#;
let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?;
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&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 <sync-token/> + <supported-report-set/> (2nd element is theoretically from versioning)
+ // -- PROPFIND --
+ // propname must return sync-token & supported-report-set (from webdav versioning)
let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>"#;
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::<dav::Multistatus<All>>(&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#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:sync-collection xmlns:D="DAV:">
+ <D:sync-token/>
+ <D:sync-level>1</D:sync-level>
+ <D:prop>
+ <D:getetag/>
+ </D:prop>
+ </D:sync-collection>
+ "#;
+ 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::<dav::Multistatus<All>>(&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::<dav::Multistatus<All>>(&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::<realization::All>::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::<dav::Multistatus<All>>(&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 <sync-collection>
+ 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::<dav::Multistatus<All>>(&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 <sync-collection>
+ 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::<dav::Multistatus<All>>(&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 <sync-collection>
+ 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::<dav::Multistatus<All>>(&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<u32> {
.parse::<u32>()?)
}
-use aero_dav::xml::{Node, Reader};
+use aero_dav::xml::{Node, Reader, Writer};
+use tokio::io::AsyncWriteExt;
pub fn dav_deserialize<T: Node<T>>(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<T: Node<T>>(src: &str) -> T {
rdr.find().await.expect("parse XML")
})
}
+pub fn dav_serialize<T: Node<T>>(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()
+ })
+}