refactoring of integration tests

This commit is contained in:
Lennart K
2026-01-28 18:38:03 +01:00
parent c763a682ed
commit af60a446ad
56 changed files with 111 additions and 237 deletions

View File

@@ -1,336 +0,0 @@
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::{CalendarMetadata, CalendarStore};
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
pub fn mkcalendar_template(
CalendarMetadata {
displayname,
order: _order,
description,
color,
}: &CalendarMetadata,
) -> String {
format!(
r#"
<?xml version='1.0' encoding='UTF-8' ?>
<CAL:mkcalendar xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<resourcetype>
<collection />
<CAL:calendar />
</resourcetype>
<displayname>{displayname}</displayname>
<CAL:calendar-description>{description}</CAL:calendar-description>
<n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">{color}</n0:calendar-color>
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT"/>
<CAL:comp name="VTODO"/>
<CAL:comp name="VJOURNAL"/>
</CAL:supported-calendar-component-set>
<CAL:calendar-timezone><![CDATA[BEGIN:VCALENDAR
PRODID:-//Example Corp.//CalDAV Client//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:US/Eastern
LAST-MODIFIED:19870101T000000Z
BEGIN:STANDARD
DTSTART:19671029T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:Eastern Standard Time (US & Canada)
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19870405T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:Eastern Daylight Time (US & Canada)
END:DAYLIGHT
END:VTIMEZONE
END:VCALENDAR
]]></CAL:calendar-timezone>
</prop>
</set>
</CAL:mkcalendar>
"#,
displayname = displayname.as_deref().unwrap_or_default(),
description = description.as_deref().unwrap_or_default(),
color = color.as_deref().unwrap_or_default(),
)
}
#[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 cal_store = context.cal_store;
let mut calendar_meta = CalendarMetadata {
displayname: Some("Calendar".to_string()),
description: Some("Description".to_string()),
color: Some("#00FF00".to_string()),
order: 0,
};
let (principal, cal_id) = ("user", "calendar");
let url = format!("/caldav/principal/{principal}/{cal_id}");
let request_template = || {
Request::builder()
.method("MKCALENDAR")
.uri(&url)
.body(Body::from(mkcalendar_template(&calendar_meta)))
.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, calendar-access, webdav-push",
"allow": "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, OPTIONS, REPORT, GET, HEAD, POST, MKCOL, MKCALENDAR, 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!("mkcalendar_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);
assert_eq!(
cal_store
.get_calendar(principal, cal_id, false)
.await
.unwrap()
.meta,
calendar_meta
);
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"<PUSH:topic>[0-9a-f-]+</PUSH:topic>", "<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>")
]
}, {
insta::assert_snapshot!("propfind_body", body);
});
let proppatch_request: &str = r#"
<propertyupdate xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<displayname>New Displayname</displayname>
<CAL:calendar-description>Test</CAL:calendar-description>
</prop>
</set>
<remove>
<prop>
<CAL:calendar-description />
</prop>
</remove>
</propertyupdate>
"#;
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);
calendar_meta.displayname = Some("New Displayname".to_string());
calendar_meta.description = None;
assert_eq!(
cal_store
.get_calendar(principal, cal_id, false)
.await
.unwrap()
.meta,
calendar_meta
);
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!(
cal_store.get_calendar(principal, cal_id, false).await,
Err(rustical_store::Error::NotFound)
));
}
#[rstest]
#[tokio::test]
async fn test_rfc4791_5_3_2(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let context = context.await;
let app = get_app(context.clone());
let calendar_meta = CalendarMetadata {
displayname: Some("Calendar".to_string()),
description: Some("Description".to_string()),
color: Some("#00FF00".to_string()),
order: 0,
};
let (principal, cal_id) = ("user", "calendar");
let url = format!("/caldav/principal/{principal}/{cal_id}");
let request_template = || {
Request::builder()
.method("MKCALENDAR")
.uri(&url)
.body(Body::from(mkcalendar_template(&calendar_meta)))
.unwrap()
};
// 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 ical = r"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:20010712T182145Z-123401@example.com
DTSTAMP:20060712T182145Z
DTSTART:20060714T170000Z
DTEND:20060715T040000Z
SUMMARY:Bastille Day Party
END:VEVENT
END:VCALENDAR";
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/qwue23489.ics"))
.header("If-None-Match", "*")
.header("Content-Type", "text/calendar")
.body(Body::from(ical))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/qwue23489.ics"))
.header("If-None-Match", "*")
.header("Content-Type", "text/calendar")
.body(Body::from(ical))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let mut request = Request::builder()
.method("REPORT")
.uri(&url)
.header("Depth", "infinity")
.header("Content-Type", "text/xml; charset=\"utf-8\"")
.body(Body::from(format!(
r#"
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
</D:prop>
<D:href>{url}/qwue23489.ics</D:href>
<D:href>/home/bernard/addressbook/vcf1.vcf</D:href>
</C:calendar-multiget>
"#
)))
.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!("multiget_body", body);
}

View File

@@ -1,98 +0,0 @@
use crate::integration_tests::{ResponseExtractString, get_app};
use axum::body::Body;
use axum::extract::Request;
use headers::{Authorization, HeaderMapExt};
use http::StatusCode;
use rstest::rstest;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
const ICAL: &str = r"
BEGIN:VCALENDAR
PRODID:-//Example Corp.//CalDAV Client//EN
VERSION:2.0
BEGIN:VEVENT
UID:1@example.com
SUMMARY:One-off Meeting
DTSTAMP:20041210T183904Z
DTSTART:20041207T120000Z
DTEND:20041207T130000Z
END:VEVENT
BEGIN:VEVENT
UID:2@example.com
SUMMARY:Weekly Meeting
DTSTAMP:20041210T183838Z
DTSTART:20041206T120000Z
DTEND:20041206T130000Z
RRULE:FREQ=WEEKLY
END:VEVENT
BEGIN:VEVENT
UID:2@example.com
SUMMARY:Weekly Meeting
RECURRENCE-ID:20041213T120000Z
DTSTAMP:20041210T183838Z
DTSTART:20041213T130000Z
DTEND:20041213T140000Z
END:VEVENT
END:VCALENDAR
";
#[rstest]
#[case(0, ICAL)]
#[case(1, include_str!("resources/rfc4791_appb.ics"))]
#[tokio::test]
async fn test_import(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
#[case] case: usize,
#[case] ical: &'static str,
) {
let context = context.await;
let app = get_app(context.clone());
let (principal, addr_id) = ("user", "calendar");
let url = format!("/caldav/principal/{principal}/{addr_id}");
let request_template = || {
Request::builder()
.method("IMPORT")
.uri(&url)
.body(Body::from(ical))
.unwrap()
};
// 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::OK);
let body = response.extract_string().await;
insta::assert_snapshot!(format!("{case}_import_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::with_settings!({
filters => vec![
(r"UID:.+", "UID:[UID]")
]
}, {
insta::assert_snapshot!(format!("{case}_get_body"), body);
});
}

View File

@@ -1,77 +0,0 @@
use axum::body::Body;
use headers::{Authorization, HeaderMapExt};
use http::{Request, StatusCode};
use rstest::rstest;
use rustical_store::CalendarMetadata;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
use crate::integration_tests::{
ResponseExtractString, caldav::calendar::mkcalendar_template, get_app,
};
#[rstest]
#[tokio::test]
async fn test_put_invalid(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let context = context.await;
let app = get_app(context.clone());
let calendar_meta = CalendarMetadata {
displayname: Some("Calendar".to_string()),
description: Some("Description".to_string()),
color: Some("#00FF00".to_string()),
order: 0,
};
let (principal, cal_id) = ("user", "calendar");
let url = format!("/caldav/principal/{principal}/{cal_id}");
let mut request = Request::builder()
.method("MKCALENDAR")
.uri(&url)
.body(Body::from(mkcalendar_template(&calendar_meta)))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
// Invalid calendar data
let ical = r"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:20010712T182145Z-123401@example.com
DTSTAMP:20060712T182145Z
DTSTART:20060714T170000Z
RRULE:UNTIL=123
DTEND:20060715T040000Z
SUMMARY:Bastille Day Party
END:VEVENT
END:VCALENDAR";
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/qwue23489.ics"))
.header("If-None-Match", "*")
.header("Content-Type", "text/calendar")
.body(Body::from(ical))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let body = response.extract_string().await;
insta::assert_snapshot!(body, @r#"
<?xml version="1.0" encoding="utf-8"?>
<error xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<CAL:valid-calendar-data/>
</error>
"#);
}

View File

@@ -1,209 +0,0 @@
use crate::integration_tests::{ResponseExtractString, get_app};
use axum::body::Body;
use axum::extract::Request;
use headers::{Authorization, HeaderMapExt};
use http::StatusCode;
use rstest::rstest;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
const ICS_1: &str = include_str!("resources/rfc4791_appb.ics");
const REPORT_7_8_1: &str = r#"
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<C:calendar-data>
<C:comp name="VCALENDAR">
<C:prop name="VERSION"/>
<C:comp name="VEVENT">
<C:prop name="SUMMARY"/>
<C:prop name="UID"/>
<C:prop name="DTSTART"/>
<C:prop name="DTEND"/>
<C:prop name="DURATION"/>
<C:prop name="RRULE"/>
<C:prop name="RDATE"/>
<C:prop name="EXRULE"/>
<C:prop name="EXDATE"/>
<C:prop name="RECURRENCE-ID"/>
</C:comp>
<C:comp name="VTIMEZONE"/>
</C:comp>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060104T000000Z"
end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_7_8_2: &str = r#"
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:limit-recurrence-set start="20060103T000000Z"
end="20060105T000000Z"/>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060103T000000Z"
end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_7_8_3: &str = r#"
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:expand start="20060103T000000Z"
end="20060105T000000Z"/>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060103T000000Z"
end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
// Adapted from Example 7.8.3 of RFC 4791
// In the RFC the output is wrong since it returns DTSTART in UTC as local time, e.g.
// DTSTART:20060103T170000
// instead of
// DTSTART:20060103T170000Z
// In https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5
// it is clearly stated that times with timezone information MUST be returned in UTC.
// Also, the RECURRENCE-ID needs to include the TIMEZONE, which is fixed here by converting it to
// UTC
const OUTPUT_7_8_3: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar/abcd2.ics</href>
<propstat>
<prop>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART:20060103T170000Z
DURATION:PT1H
SUMMARY:Event #2
UID:abcd2
RECURRENCE-ID:20060103T170000Z
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART:20060104T190000Z
DURATION:PT1H
RECURRENCE-ID:20060104T170000Z
SUMMARY:Event #2 bis
UID:abcd2
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/caldav/principal/user/calendar/abcd3.ics</href>
<propstat>
<prop>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART:20060104T150000Z
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
#[rstest]
#[case(0, ICS_1, REPORT_7_8_1, None)]
#[case(1, ICS_1, REPORT_7_8_2, None)]
#[case(2, ICS_1, REPORT_7_8_3, Some(OUTPUT_7_8_3))]
#[tokio::test]
async fn test_report(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
#[case] case: usize,
#[case] ics: &'static str,
#[case] report: &'static str,
#[case] output: Option<&'static str>,
) {
let context = context.await;
let app = get_app(context.clone());
let (principal, addr_id) = ("user", "calendar");
let url = format!("/caldav/principal/{principal}/{addr_id}");
let request_template = || {
Request::builder()
.method("IMPORT")
.uri(&url)
.body(Body::from(ics))
.unwrap()
};
// 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::OK);
let mut request = Request::builder()
.method("REPORT")
.uri(&url)
.body(Body::from(report))
.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!(format!("{case}_report_body"), body);
if let Some(output) = output {
similar_asserts::assert_eq!(output, body.replace('\r', ""));
}
}

View File

@@ -1,115 +0,0 @@
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_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
mod calendar;
mod calendar_import;
mod calendar_put;
mod calendar_report;
#[rstest]
#[tokio::test]
async fn test_caldav_root(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let app = get_app(context.await);
let request_template = || {
Request::builder()
.method("PROPFIND")
.uri("/caldav")
.body(Body::empty())
.unwrap()
};
// Try without authentication
let request = request_template();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
// Try with wrong password
let mut request = request_template();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "wrongpass"));
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.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
let body = response.extract_string().await;
insta::assert_snapshot!("propfind_body", body);
}
#[rstest]
#[tokio::test]
async fn test_caldav_principal(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let app = get_app(context.await);
let request_template = || {
Request::builder()
.method("PROPFIND")
.uri("/caldav/principal/user")
.body(Body::empty())
.unwrap()
};
// Try without authentication
let request = request_template();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
// Try with wrong password
let mut request = request_template();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "wrongpass"));
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::MULTI_STATUS);
let body = response.extract_string().await;
insta::assert_snapshot!("propfind_depth_0", body);
// Try with Depth: 1
let mut request = request_template();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
request
.headers_mut()
.insert("Depth", HeaderValue::from_static("1"));
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"<PUSH:topic>[0-9a-f-]+</PUSH:topic>", "<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>")
]
}, {
insta::assert_snapshot!("propfind_depth_1", body);
});
}

View File

@@ -1,103 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20060206T001102Z
DTSTART;TZID=US/Eastern:20060102T100000
DURATION:PT1H
SUMMARY:Event #1
Description:Go Steelers!
UID:abcd1
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:abcd2
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:abcd2
END:VEVENT
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
BEGIN:VTODO
DTSTAMP:20060205T235335Z
DUE;VALUE=DATE:20060104
STATUS:NEEDS-ACTION
SUMMARY:Task #1
UID:abcd4
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
BEGIN:VTODO
DTSTAMP:20060205T235300Z
DUE;VALUE=DATE:20060106
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:NEEDS-ACTION
SUMMARY:Task #2
UID:abcd5
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
BEGIN:VTODO
COMPLETED:20051223T122322Z
DTSTAMP:20060205T235400Z
DUE;VALUE=DATE:20051225
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:COMPLETED
SUMMARY:Task #3
UID:abcd6
END:VTODO
BEGIN:VTODO
DTSTAMP:20060205T235600Z
DUE;VALUE=DATE:20060101
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:CANCELLED
SUMMARY:Task #4
UID:abcd7
END:VTODO
END:VCALENDAR

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/caldav/calendar.rs
expression: body
---

View File

@@ -1,13 +0,0 @@
---
source: src/integration_tests/caldav/calendar.rs
expression: body
---
BEGIN:VCALENDAR
VERSION:2.0
PRODID:RustiCal Export
CALSCALE:GREGORIAN
X-WR-CALNAME:Calendar
X-WR-CALDESC:Description
X-WR-CALCOLOR:#00FF00
X-WR-TIMEZONE:US/Eastern
END:VCALENDAR

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/caldav/calendar.rs
expression: body
---

View File

@@ -1,20 +0,0 @@
---
source: src/integration_tests/caldav/calendar.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar/qwue23489.ics</href>
<propstat>
<prop>
<getetag>&quot;f781224669f0db2674e9e45a9be2b01774c02136e3fb72792ef217bccf49fafa&quot;</getetag>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/home/bernard/addressbook/vcf1.vcf</href>
<status>HTTP/1.1 404 Not Found</status>
</response>
</multistatus>

View File

@@ -1,205 +0,0 @@
---
source: src/integration_tests/caldav/calendar.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar/</href>
<propstat>
<prop>
<calendar-color xmlns="http://apple.com/ns/ical/">#00FF00</calendar-color>
<CAL:calendar-description>Description</CAL:calendar-description>
<CAL:calendar-timezone>BEGIN:VCALENDAR
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:US/Eastern
TZID-ALIAS-OF:America/New_York
LAST-MODIFIED:20260124T185655Z
X-LIC-LOCATION:US/Eastern
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-045602
TZOFFSETTO:-0500
DTSTART:18831118T120358
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19180331T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19200328T070000Z
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
DTSTART:19181027T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19201031T060000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19210424T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19410427T070000Z
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
DTSTART:19210925T020000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19410928T060000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:EWT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19420209T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:EPT
TZOFFSETFROM:-0400
TZOFFSETTO:-0400
DTSTART:19450814T190000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
DTSTART:19450930T020000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19460428T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
DTSTART:19460929T020000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19540926T060000Z
END:STANDARD
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
DTSTART:19551030T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19740106T020000
RDATE:19750223T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19760425T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:19870405T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
</CAL:calendar-timezone>
<CAL:timezone-service-set>
<href>https://www.iana.org/time-zones</href>
</CAL:timezone-service-set>
<CAL:calendar-timezone-id>US/Eastern</CAL:calendar-timezone-id>
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT"/>
<CAL:comp name="VTODO"/>
<CAL:comp name="VJOURNAL"/>
</CAL:supported-calendar-component-set>
<CAL:supported-calendar-data>
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
</CAL:supported-calendar-data>
<CAL:supported-collation-set>
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
<CAL:supported-collation>i;unicode-casemap</CAL:supported-collation>
<CAL:supported-collation>i;octet</CAL:supported-collation>
</CAL:supported-collation-set>
<CAL:max-resource-size>10000000</CAL:max-resource-size>
<supported-report-set>
<supported-report>
<report>
<CAL:calendar-query/>
</report>
</supported-report>
<supported-report>
<report>
<CAL:calendar-multiget/>
</report>
</supported-report>
<supported-report>
<report>
<sync-collection/>
</report>
</supported-report>
</supported-report-set>
<CAL:min-date-time>-2621430101T000000Z</CAL:min-date-time>
<CAL:max-date-time>+2621421231T235959Z</CAL:max-date-time>
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
<PUSH:transports>
<PUSH:web-push/>
</PUSH:transports>
<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>
<PUSH:supported-triggers>
<PUSH:content-update>
<depth>1</depth>
</PUSH:content-update>
<PUSH:property-update>
<depth>1</depth>
</PUSH:property-update>
</PUSH:supported-triggers>
<resourcetype>
<collection/>
<CAL:calendar/>
</resourcetype>
<displayname>Calendar</displayname>
<current-user-principal>
<href>/caldav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
<owner>
<href>/caldav/principal/user/</href>
</owner>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,28 +0,0 @@
---
source: src/integration_tests/caldav/calendar.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar</href>
<propstat>
<prop>
<displayname xmlns="DAV:"/>
<calendar-description xmlns="urn:ietf:params:xml:ns:caldav"/>
<calendar-description/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
<propstat>
<prop>
</prop>
<status>HTTP/1.1 404 Not Found</status>
</propstat>
<propstat>
<prop>
</prop>
<status>HTTP/1.1 409 Conflict</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,32 +0,0 @@
---
source: src/integration_tests/caldav/calendar_import.rs
expression: body
---
BEGIN:VCALENDAR
VERSION:2.0
PRODID:RustiCal Export
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:[UID]
SUMMARY:One-off Meeting
DTSTAMP:20041210T183904Z
DTSTART:20041207T120000Z
DTEND:20041207T130000Z
END:VEVENT
BEGIN:VEVENT
UID:[UID]
SUMMARY:Weekly Meeting
DTSTAMP:20041210T183838Z
DTSTART:20041206T120000Z
DTEND:20041206T130000Z
RRULE:FREQ=WEEKLY
END:VEVENT
BEGIN:VEVENT
UID:[UID]
SUMMARY:Weekly Meeting
RECURRENCE-ID:20041213T120000Z
DTSTAMP:20041210T183838Z
DTSTART:20041213T130000Z
DTEND:20041213T140000Z
END:VEVENT
END:VCALENDAR

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/caldav/calendar_import.rs
expression: body
---

View File

@@ -1,108 +0,0 @@
---
source: src/integration_tests/caldav/calendar_import.rs
expression: body
---
BEGIN:VCALENDAR
VERSION:2.0
PRODID:RustiCal Export
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20060206T001102Z
DTSTART;TZID=US/Eastern:20060102T100000
DURATION:PT1H
SUMMARY:Event #1
DESCRIPTION:Go Steelers!
UID:[UID]
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:[UID]
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:[UID]
END:VEVENT
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:[UID]
X-ABC-GUID:[UID]
END:VEVENT
BEGIN:VTODO
DTSTAMP:20060205T235335Z
DUE;VALUE=DATE:20060104
STATUS:NEEDS-ACTION
SUMMARY:Task #1
UID:[UID]
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
BEGIN:VTODO
DTSTAMP:20060205T235300Z
DUE;VALUE=DATE:20060106
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:NEEDS-ACTION
SUMMARY:Task #2
UID:[UID]
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
BEGIN:VTODO
COMPLETED:20051223T122322Z
DTSTAMP:20060205T235400Z
DUE;VALUE=DATE:20051225
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:COMPLETED
SUMMARY:Task #3
UID:[UID]
END:VTODO
BEGIN:VTODO
DTSTAMP:20060205T235600Z
DUE;VALUE=DATE:20060101
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:CANCELLED
SUMMARY:Task #4
UID:[UID]
END:VTODO
END:VCALENDAR

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/caldav/calendar_import.rs
expression: body
---

View File

@@ -1,107 +0,0 @@
---
source: src/integration_tests/caldav/calendar_import.rs
expression: body
---
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:RustiCal
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:[UID]
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001102Z
DTSTART;TZID=US/Eastern:20060102T100000
DURATION:PT1H
SUMMARY:Event #1
Description:Go Steelers!
UID:[UID]
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:[UID]
END:VEVENT
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:[UID]
END:VEVENT
BEGIN:VTODO
DTSTAMP:20060205T235335Z
DUE;VALUE=DATE:20060104
STATUS:NEEDS-ACTION
SUMMARY:Task #1
UID:[UID]
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
BEGIN:VTODO
DTSTAMP:20060205T235300Z
DUE;VALUE=DATE:20060106
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:NEEDS-ACTION
SUMMARY:Task #2
UID:[UID]
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
BEGIN:VTODO
COMPLETED:20051223T122322Z
DTSTAMP:20060205T235400Z
DUE;VALUE=DATE:20051225
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:COMPLETED
SUMMARY:Task #3
UID:[UID]
END:VTODO
BEGIN:VTODO
DTSTAMP:20060205T235600Z
DUE;VALUE=DATE:20060101
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:CANCELLED
SUMMARY:Task #4
UID:[UID]
END:VTODO
END:VCALENDAR

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/caldav/calendar_import.rs
expression: body
---

View File

@@ -1,101 +0,0 @@
---
source: src/integration_tests/caldav/calendar_report.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar/abcd2.ics</href>
<propstat>
<prop>
<getetag>&quot;7d80077c5655339885a36b6dbe97336767fb85e6b12c94668bcac100ed971fac&quot;</getetag>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:abcd2
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:abcd2
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/caldav/principal/user/calendar/abcd3.ics</href>
<propstat>
<prop>
<getetag>&quot;a84fd022dfc742bf8f17ac04fca3aad687e9ae724180185e8e0df11e432dae30&quot;</getetag>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,99 +0,0 @@
---
source: src/integration_tests/caldav/calendar_report.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar/abcd2.ics</href>
<propstat>
<prop>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:abcd2
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:abcd2
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/caldav/principal/user/calendar/abcd3.ics</href>
<propstat>
<prop>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,63 +0,0 @@
---
source: src/integration_tests/caldav/calendar_report.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/calendar/abcd2.ics</href>
<propstat>
<prop>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART:20060103T170000Z
DURATION:PT1H
SUMMARY:Event #2
UID:abcd2
RECURRENCE-ID:20060103T170000Z
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART:20060104T190000Z
DURATION:PT1H
RECURRENCE-ID:20060104T170000Z
SUMMARY:Event #2 bis
UID:abcd2
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/caldav/principal/user/calendar/abcd3.ics</href>
<propstat>
<prop>
<CAL:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DTSTART:20060104T150000Z
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:abcd3
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
</CAL:calendar-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,27 +0,0 @@
---
source: src/integration_tests/caldav/mod.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
</resourcetype>
<displayname>RustiCal DAV root</displayname>
<current-user-principal>
<href>/caldav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,53 +0,0 @@
---
source: src/integration_tests/caldav/mod.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/</href>
<propstat>
<prop>
<CAL:calendar-user-type>INDIVIDUAL</CAL:calendar-user-type>
<CAL:calendar-user-address-set>
<href>/caldav/principal/user/</href>
</CAL:calendar-user-address-set>
<principal-URL>
<href>/caldav/principal/user/</href>
</principal-URL>
<group-membership>
</group-membership>
<group-member-set>
</group-member-set>
<alternate-URI-set/>
<supported-report-set>
<supported-report>
<report>
<principal-match/>
</report>
</supported-report>
</supported-report-set>
<CAL:calendar-home-set>
<href>/caldav/principal/user/</href>
</CAL:calendar-home-set>
<resourcetype>
<collection/>
<principal/>
</resourcetype>
<displayname>user</displayname>
<current-user-principal>
<href>/caldav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
<owner>
<href>/caldav/principal/user/</href>
</owner>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,53 +0,0 @@
---
source: src/integration_tests/caldav/mod.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/caldav/principal/user/</href>
<propstat>
<prop>
<CAL:calendar-user-type>INDIVIDUAL</CAL:calendar-user-type>
<CAL:calendar-user-address-set>
<href>/caldav/principal/user/</href>
</CAL:calendar-user-address-set>
<principal-URL>
<href>/caldav/principal/user/</href>
</principal-URL>
<group-membership>
</group-membership>
<group-member-set>
</group-member-set>
<alternate-URI-set/>
<supported-report-set>
<supported-report>
<report>
<principal-match/>
</report>
</supported-report>
</supported-report-set>
<CAL:calendar-home-set>
<href>/caldav/principal/user/</href>
</CAL:calendar-home-set>
<resourcetype>
<collection/>
<principal/>
</resourcetype>
<displayname>user</displayname>
<current-user-principal>
<href>/caldav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
<owner>
<href>/caldav/principal/user/</href>
</owner>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,447 +0,0 @@
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::AddressbookStore;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
fn mkcol_template(displayname: &str, description: &str) -> String {
format!(
r#"
<?xml version='1.0' encoding='UTF-8' ?>
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<resourcetype>
<collection />
<CARD:addressbook />
</resourcetype>
<displayname>{displayname}</displayname>
<CARD:addressbook-description>{description}</CARD:addressbook-description>
</prop>
</set>
</mkcol>
"#,
)
}
#[rstest]
#[tokio::test]
async fn test_carddav_addressbook(
#[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("MKCOL")
.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"<PUSH:topic>[0-9a-f-]+</PUSH:topic>", "<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>")
]
}, {
insta::assert_snapshot!("propfind_body", body);
});
let proppatch_request: &str = r#"
<propertyupdate xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<displayname>New Displayname</displayname>
<CARD:addressbook-description>Test</CARD:addressbook-description>
</prop>
</set>
<remove>
<prop>
<CARD:addressbook-description />
</prop>
</remove>
</propertyupdate>
"#;
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)
));
}
#[rstest]
#[tokio::test]
async fn test_mkcol_rfc6352_6_3_1_1(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let context = context.await;
let app = get_app(context.clone());
let addr_store = context.addr_store;
let (displayname, description) = (
"Lisa's Contacts".to_owned(),
"My primary address book.".to_owned(),
);
let (principal, addr_id) = ("user", "contacts");
let url = format!("/carddav/principal/{principal}/{addr_id}");
let mut request = Request::builder()
.method("MKCOL")
.uri(&url)
.body(Body::from(format!(
r#"<?xml version="1.0" encoding="utf-8" ?>
<D:mkcol xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:set>
<D:prop>
<D:resourcetype>
<D:collection/>
<C:addressbook/>
</D:resourcetype>
<D:displayname>{displayname}</D:displayname>
<C:addressbook-description xml:lang="en"
>{description}</C:addressbook-description>
</D:prop>
</D:set>
</D:mkcol>"#
)))
.unwrap();
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 saved_addressbook = addr_store
.get_addressbook(principal, addr_id, false)
.await
.unwrap();
assert_eq!(
(
saved_addressbook.displayname.unwrap(),
saved_addressbook.description.unwrap()
),
(displayname, description)
);
let vcard = r"BEGIN:VCARD
VERSION:3.0
FN:Cyrus Daboo
N:Daboo;Cyrus
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
NICKNAME:me
NOTE:Example VCard.
ORG:Self Employed
TEL;TYPE=WORK,VOICE:412 605 0499
TEL;TYPE=FAX:412 605 0705
URL:http://www.example.com
UID:1234-5678-9000-1
END:VCARD
";
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/newcard.vcf"))
.header("If-None-Match", "*")
.header("Content-Type", "text/vcard")
.body(Body::from(vcard))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let etag = response.headers().get("ETag").unwrap();
// This should overwrite
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/newcard.vcf"))
.header("If-None-Match", "\"somearbitraryetag\"")
.header("Content-Type", "text/vcard")
.body(Body::from(vcard))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/newcard.vcf"))
.header("If-None-Match", etag)
.header("Content-Type", "text/vcard")
.body(Body::from(vcard))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/newcard.vcf"))
.header("If-None-Match", "*")
.header("Content-Type", "text/vcard")
.body(Body::from(vcard))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[rstest]
#[tokio::test]
async fn test_rfc6352_8_7_1(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let context = context.await;
let app = get_app(context.clone());
let addr_store = context.addr_store;
let (displayname, description) = (
"Lisa's Contacts".to_owned(),
"My primary address book.".to_owned(),
);
let (principal, addr_id) = ("user", "contacts");
let url = format!("/carddav/principal/{principal}/{addr_id}");
let mut request = Request::builder()
.method("MKCOL")
.uri(&url)
.body(Body::from(format!(
r#"<?xml version="1.0" encoding="utf-8" ?>
<D:mkcol xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:set>
<D:prop>
<D:resourcetype>
<D:collection/>
<C:addressbook/>
</D:resourcetype>
<D:displayname>{displayname}</D:displayname>
<C:addressbook-description xml:lang="en"
>{description}</C:addressbook-description>
</D:prop>
</D:set>
</D:mkcol>"#
)))
.unwrap();
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 saved_addressbook = addr_store
.get_addressbook(principal, addr_id, false)
.await
.unwrap();
assert_eq!(
(
saved_addressbook.displayname.unwrap(),
saved_addressbook.description.unwrap()
),
(displayname, description)
);
let vcard = r"BEGIN:VCARD
VERSION:3.0
FN:Cyrus Daboo
N:Daboo;Cyrus
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
NICKNAME:me
NOTE:Example VCard.
ORG:Self Employed
TEL;TYPE=WORK,VOICE:412 605 0499
TEL;TYPE=FAX:412 605 0705
URL:http://www.example.com
UID:1234-5678-9000-1
END:VCARD
";
let mut request = Request::builder()
.method("PUT")
.uri(format!("{url}/newcard.vcf"))
.header("If-None-Match", "*")
.header("Content-Type", "text/vcard")
.body(Body::from(vcard))
.unwrap();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let mut request = Request::builder()
.method("REPORT")
.uri(&url)
.header("Depth", "infinity")
.header("Content-Type", "text/xml; charset=\"utf-8\"")
.body(Body::from(format!(
r#"
<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-multiget xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<C:address-data>
<C:prop name="VERSION"/>
<C:prop name="UID"/>
<C:prop name="NICKNAME"/>
<C:prop name="EMAIL"/>
<C:prop name="FN"/>
</C:address-data>
</D:prop>
<D:href>{url}/newcard.vcf</D:href>
<D:href>/home/bernard/addressbook/vcf1.vcf</D:href>
</C:addressbook-multiget>
"#
)))
.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!("multiget_body", body);
}

View File

@@ -1,73 +0,0 @@
use crate::integration_tests::{ResponseExtractString, get_app};
use axum::body::Body;
use axum::extract::Request;
use headers::{Authorization, HeaderMapExt};
use http::StatusCode;
use rstest::rstest;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
#[rstest]
#[tokio::test]
async fn test_import(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let context = context.await;
let app = get_app(context.clone());
let (principal, addr_id) = ("user", "contacts");
let url = format!("/carddav/principal/{principal}/{addr_id}");
let request_template = || {
Request::builder()
.method("IMPORT")
.uri(&url)
.body(Body::from(
r"BEGIN:VCARD
VERSION:4.0
FN:Simon Perreault
N:Perreault;Simon;;;ing. jr,M.Sc.
BDAY:--0203
GENDER:M
EMAIL;TYPE=work:simon.perreault@viagenie.ca
END:VCARD",
))
.unwrap()
};
// 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::OK);
let body = response.extract_string().await;
insta::assert_snapshot!("import_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::with_settings!({
filters => vec![
(r"UID:.+", "UID:[UID]")
]
}, {
insta::assert_snapshot!("get_body", body);
});
}

View File

@@ -1,57 +0,0 @@
use crate::integration_tests::{ResponseExtractString, get_app};
use axum::body::Body;
use axum::extract::Request;
use headers::{Authorization, HeaderMapExt};
use http::StatusCode;
use rstest::rstest;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use tower::ServiceExt;
mod addressbook;
mod addressbook_import;
#[rstest]
#[tokio::test]
async fn test_carddav_root(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let app = get_app(context.await);
let request_template = || {
Request::builder()
.method("PROPFIND")
.uri("/carddav")
.body(Body::empty())
.unwrap()
};
// Try without authentication
let request = request_template();
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let body = response.extract_string().await;
insta::assert_snapshot!(body);
// Try with wrong password
let mut request = request_template();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "wrongpass"));
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let body = response.extract_string().await;
insta::assert_snapshot!(body);
// Try with correct credentials
let mut request = request_template();
request
.headers_mut()
.typed_insert(Authorization::basic("user", "pass"));
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
let body = response.extract_string().await;
insta::assert_snapshot!(body);
}

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/carddav/addressbook.rs
expression: body
---

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/carddav/addressbook.rs
expression: body
---

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/carddav/addressbook.rs
expression: body
---

View File

@@ -1,35 +0,0 @@
---
source: src/integration_tests/carddav/addressbook.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/carddav/principal/user/contacts/newcard.vcf</href>
<propstat>
<prop>
<getetag>&quot;ea0bf4a2ce7ef84606a4cf9235776dbc11b3e7ce351ddf35f27cbc0088acca7e&quot;</getetag>
<CARD:address-data>BEGIN:VCARD
VERSION:3.0
FN:Cyrus Daboo
N:Daboo;Cyrus
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
NICKNAME:me
NOTE:Example VCard.
ORG:Self Employed
TEL;TYPE=WORK,VOICE:412 605 0499
TEL;TYPE=FAX:412 605 0705
URL:http://www.example.com
UID:1234-5678-9000-1
END:VCARD
</CARD:address-data>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response>
<href>/home/bernard/addressbook/vcf1.vcf</href>
<status>HTTP/1.1 404 Not Found</status>
</response>
</multistatus>

View File

@@ -1,68 +0,0 @@
---
source: src/integration_tests/carddav/addressbook.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/carddav/principal/user/contacts/</href>
<propstat>
<prop>
<CARD:addressbook-description>Amazing contacts!</CARD:addressbook-description>
<CARD:supported-address-data>
<CARD:address-data-type content-type="text/vcard" version="3.0"/>
<CARD:address-data-type content-type="text/vcard" version="4.0"/>
</CARD:supported-address-data>
<CARD:supported-collation-set>
<CARD:supported-collation>i;ascii-casemap</CARD:supported-collation>
<CARD:supported-collation>i;unicode-casemap</CARD:supported-collation>
<CARD:supported-collation>i;octet</CARD:supported-collation>
</CARD:supported-collation-set>
<supported-report-set>
<supported-report>
<report>
<CARD:addressbook-multiget/>
</report>
</supported-report>
<supported-report>
<report>
<sync-collection/>
</report>
</supported-report>
</supported-report-set>
<CARD:max-resource-size>10000000</CARD:max-resource-size>
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
<PUSH:transports>
<PUSH:web-push/>
</PUSH:transports>
<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>
<PUSH:supported-triggers>
<PUSH:content-update>
<depth>1</depth>
</PUSH:content-update>
<PUSH:property-update>
<depth>1</depth>
</PUSH:property-update>
</PUSH:supported-triggers>
<resourcetype>
<collection/>
<CARD:addressbook/>
</resourcetype>
<displayname>Contacts</displayname>
<current-user-principal>
<href>/carddav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
<owner>
<href>/carddav/principal/user/</href>
</owner>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,28 +0,0 @@
---
source: src/integration_tests/carddav/addressbook.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/carddav/principal/user/contacts</href>
<propstat>
<prop>
<displayname xmlns="DAV:"/>
<addressbook-description xmlns="urn:ietf:params:xml:ns:carddav"/>
<addressbook-description/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
<propstat>
<prop>
</prop>
<status>HTTP/1.1 404 Not Found</status>
</propstat>
<propstat>
<prop>
</prop>
<status>HTTP/1.1 409 Conflict</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,13 +0,0 @@
---
source: src/integration_tests/carddav/addressbook_import.rs
expression: body
---
BEGIN:VCARD
VERSION:4.0
FN:Simon Perreault
N:Perreault;Simon;;;ing. jr,M.Sc.
BDAY:--0203
GENDER:M
EMAIL;TYPE=work:simon.perreault@viagenie.ca
UID:[UID]
END:VCARD

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/carddav/addressbook_import.rs
expression: body
---

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/carddav/mod.rs
expression: body
---

View File

@@ -1,27 +0,0 @@
---
source: src/integration_tests/carddav/mod.rs
expression: body
---
<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<response>
<href>/carddav/</href>
<propstat>
<prop>
<resourcetype>
<collection/>
</resourcetype>
<displayname>RustiCal DAV root</displayname>
<current-user-principal>
<href>/carddav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>

View File

@@ -1,5 +0,0 @@
---
source: src/integration_tests/carddav/mod.rs
expression: body
---

View File

@@ -1,69 +0,0 @@
use crate::{app::make_app, config::NextcloudLoginConfig};
use axum::extract::Request;
use axum::{body::Body, response::Response};
use rstest::rstest;
use rustical_caldav::CalDavConfig;
use rustical_frontend::FrontendConfig;
use rustical_store_sqlite::tests::{TestStoreContext, test_store_context};
use std::sync::Arc;
use tower::ServiceExt;
pub fn get_app(context: TestStoreContext) -> axum::Router {
let TestStoreContext {
addr_store,
cal_store,
principal_store,
sub_store,
..
} = context;
make_app(
Arc::new(addr_store),
Arc::new(cal_store),
Arc::new(sub_store),
Arc::new(principal_store),
FrontendConfig {
enabled: true,
allow_password_login: true,
},
None,
CalDavConfig::default(),
&NextcloudLoginConfig { enabled: false },
false,
true,
20,
)
}
pub trait ResponseExtractString {
#[allow(async_fn_in_trait)]
async fn extract_string(self) -> String;
}
impl ResponseExtractString for Response {
async fn extract_string(self) -> String {
let bytes = axum::body::to_bytes(self.into_body(), usize::MAX)
.await
.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
}
#[rstest]
#[tokio::test]
async fn test_ping(
#[from(test_store_context)]
#[future]
context: TestStoreContext,
) {
let app = get_app(context.await);
let response = app
.oneshot(Request::builder().uri("/ping").body(Body::empty()).unwrap())
.await
.unwrap();
assert!(response.status().is_success());
}
mod caldav;
mod carddav;

View File

@@ -26,8 +26,6 @@ pub mod app;
mod commands;
pub use commands::*;
pub mod config;
#[cfg(test)]
pub mod integration_tests;
mod setup_tracing;
#[derive(Parser, Debug)]
@@ -70,6 +68,7 @@ pub async fn get_data_stores(
skip_broken,
}) => {
let db = create_db_pool(db_url, migrate).await?;
// Channel to watch for changes (for DAV Push)
let (send, recv) = tokio::sync::mpsc::channel(1000);