diff --git a/src/integration_tests/carddav/addressbook.rs b/src/integration_tests/carddav/addressbook.rs new file mode 100644 index 0000000..9d7ca45 --- /dev/null +++ b/src/integration_tests/carddav/addressbook.rs @@ -0,0 +1,194 @@ +use crate::integration_tests::{ResponseExtractString, get_app}; +use axum::body::Body; +use axum::extract::Request; +use headers::{Authorization, HeaderMapExt}; +use http::{HeaderValue, StatusCode}; +use rstest::rstest; +use rustical_store::{Addressbook, AddressbookStore}; +use rustical_store_sqlite::tests::{TestStoreContext, test_store_context}; +use tower::ServiceExt; + +fn mkcol_template(displayname: &str, description: &str) -> String { + format!( + r#" + + + + + + + + + {displayname} + {description} + + + + "#, + ) +} + +#[rstest] +#[tokio::test] +async fn test_caldav_calendar( + #[from(test_store_context)] + #[future] + context: TestStoreContext, +) { + let context = context.await; + let app = get_app(context.clone()); + let addr_store = context.addr_store; + + let (mut displayname, mut description) = ( + Some("Contacts".to_owned()), + Some("Amazing contacts!".to_owned()), + ); + let (principal, addr_id) = ("user", "contacts"); + let url = format!("/carddav/principal/{principal}/{addr_id}"); + + let request_template = || { + Request::builder() + .method("MKCALENDAR") + .uri(&url) + .body(Body::from(mkcol_template( + displayname.as_ref().unwrap(), + description.as_ref().unwrap(), + ))) + .unwrap() + }; + + // Try OPTIONS without authentication + let request = Request::builder() + .method("OPTIONS") + .uri(&url) + .body(Body::empty()) + .unwrap(); + let response = app.clone().oneshot(request).await.unwrap(); + insta::assert_debug_snapshot!(response, @r#" + Response { + status: 200, + version: HTTP/1.1, + headers: { + "dav": "1, 3, access-control, addressbook, webdav-push", + "allow": "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, OPTIONS, REPORT, GET, HEAD, POST, MKCOL, IMPORT", + }, + body: Body( + UnsyncBoxBody, + ), + } + "#); + + // Try without authentication + let request = request_template(); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + // Try with correct credentials + let mut request = request_template(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + let body = response.extract_string().await; + insta::assert_snapshot!("mkcol_body", body); + + let mut request = Request::builder() + .method("GET") + .uri(&url) + .body(Body::empty()) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.extract_string().await; + insta::assert_snapshot!("get_body", body); + + let saved_addressbook = addr_store + .get_addressbook(principal, addr_id, false) + .await + .unwrap(); + assert_eq!( + (saved_addressbook.displayname, saved_addressbook.description), + (displayname, description) + ); + + let mut request = Request::builder() + .method("PROPFIND") + .uri(&url) + .body(Body::empty()) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::MULTI_STATUS); + let body = response.extract_string().await; + insta::with_settings!({ + filters => vec![ + (r"[0-9a-f-]+", "[PUSH_TOPIC]") + ] + }, { + insta::assert_snapshot!("propfind_body", body); + }); + + let proppatch_request: &str = r#" + + + + New Displayname + Test + + + + + + + + + "#; + let mut request = Request::builder() + .method("PROPPATCH") + .uri(&url) + .body(Body::from(proppatch_request)) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::MULTI_STATUS); + let body = response.extract_string().await; + insta::assert_snapshot!("proppatch_body", body); + + displayname = Some("New Displayname".to_string()); + description = None; + let saved_addressbook = addr_store + .get_addressbook(principal, addr_id, false) + .await + .unwrap(); + assert_eq!( + (saved_addressbook.displayname, saved_addressbook.description), + (displayname, description) + ); + + let mut request = Request::builder() + .method("DELETE") + .uri(&url) + .header("X-No-Trashbin", HeaderValue::from_static("1")) + .body(Body::empty()) + .unwrap(); + request + .headers_mut() + .typed_insert(Authorization::basic("user", "pass")); + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = response.extract_string().await; + insta::assert_snapshot!("delete_body", body); + + assert!(matches!( + addr_store.get_addressbook(principal, addr_id, false).await, + Err(rustical_store::Error::NotFound) + )); +} diff --git a/src/integration_tests/carddav/mod.rs b/src/integration_tests/carddav/mod.rs index d55ae82..520d11f 100644 --- a/src/integration_tests/carddav/mod.rs +++ b/src/integration_tests/carddav/mod.rs @@ -7,6 +7,8 @@ use rstest::rstest; use rustical_store_sqlite::tests::{TestStoreContext, test_store_context}; use tower::ServiceExt; +mod addressbook; + #[rstest] #[tokio::test] async fn test_carddav_root(