/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; use ahash::AHashSet; use dav_proto::Depth; use groupware::DavResourceName; use hyper::StatusCode; pub async fn test(test: &TestServer) { let client = test.account("john@example.com").webdav_client(); for resource_type in [ DavResourceName::File, DavResourceName::Cal, DavResourceName::Card, ] { println!( "Running REPORT sync-collection tests ({})...", resource_type.base_path() ); let user_base_path = format!("{}/john%40example.com/", resource_type.base_path()); // Test 1: Initial sync let response = client .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"]) .await; assert_eq!( response.hrefs().len(), if resource_type == DavResourceName::File { 1 } else { 2 }, "{:?}", response.hrefs() ); let sync_token_1 = response.sync_token().to_string(); // Test 2: No changes since last sync let response = client .sync_collection( &user_base_path, &sync_token_1, Depth::Infinity, None, ["D:getetag"], ) .await; assert_eq!(response.hrefs(), Vec::::new()); // Test 3: Create a collection and make sure it is synced let new_collection = format!("{}new-collection/", user_base_path); client .mkcol("MKCOL", &new_collection, [], []) .await .with_status(StatusCode::CREATED); let response = client .sync_collection( &user_base_path, &sync_token_1, Depth::Infinity, None, ["D:getetag"], ) .await; assert_eq!(response.hrefs(), vec![new_collection.clone()]); let sync_token_2 = response.sync_token().to_string(); // Test 4: Create a file and make sure it is synced let new_file = format!("{new_collection}new-file"); let contents = resource_type.generate(); client .request("PUT", &new_file, &contents) .await .with_status(StatusCode::CREATED); let response = client .sync_collection( &user_base_path, &sync_token_1, Depth::Infinity, None, ["D:getetag"], ) .await; assert_eq!( response.hrefs(), vec![new_collection.clone(), new_file.clone()] ); let sync_token_3 = response.sync_token().to_string(); let response = client .sync_collection( &user_base_path, &sync_token_2, Depth::Infinity, None, ["D:getetag"], ) .await; assert_eq!(response.hrefs(), vec![new_file.clone()]); // Test 5: sync-token with Depth 1 let response = client .sync_collection( &user_base_path, &sync_token_1, Depth::One, None, ["D:getetag"], ) .await; assert_eq!(response.hrefs(), vec![new_collection.clone()]); // Test 6: sync-token with Depth 0 let response = client .sync_collection( &new_collection, &sync_token_1, Depth::Zero, None, ["D:getetag"], ) .await; assert_eq!(response.hrefs(), vec![new_collection.clone()]); // Test 7: Outdated sync-token in If header should fail let new_file2 = format!("{new_collection}new-file2"); let contents = resource_type.generate(); let condition = format!("(<{sync_token_2}>)"); client .request_with_headers( "PUT", &new_file2, [("if", condition.as_str())], contents.as_str(), ) .await .with_status(StatusCode::PRECONDITION_FAILED) .with_empty_body(); // Test 8: Correct sync-token in If header should work let condition = format!("(<{sync_token_3}>)"); client .request_with_headers( "PUT", &new_file2, [("if", condition.as_str())], contents.as_str(), ) .await .with_status(StatusCode::CREATED) .with_empty_body(); // Test 9: Limit let mut sync_token = client .sync_collection( &new_collection, &sync_token_3, Depth::Zero, None, ["D:getetag"], ) .await .sync_token() .to_string(); let (folder_name, files) = client .create_hierarchy(user_base_path.trim_end_matches('/'), 1, 0, 10) .await; let mut expected_changes = files .iter() .map(|x| x.0.as_str()) .chain([folder_name.as_str()]) .collect::>(); for _ in 0..10 { let response = client .sync_collection( &user_base_path, &sync_token, Depth::Infinity, 2.into(), ["D:getetag"], ) .await; sync_token = response.sync_token().to_string(); let hrefs = response.hrefs(); if hrefs.is_empty() { break; } let mut has_user_base_path = false; let mut item_count = 0; for href in hrefs { if href == user_base_path { has_user_base_path = true; } else if expected_changes.remove(href) { item_count += 1; } else { panic!("Unexpected href: {href}"); } } if has_user_base_path { assert_eq!(item_count, 2); response .with_value( "D:multistatus.D:response.D:status", "HTTP/1.1 507 Insufficient Storage", ) .with_value( "D:multistatus.D:response.D:error.D:number-of-matches-within-limits", "", ) .with_value( "D:multistatus.D:response.D:responsedescription", "The number of matches exceeds the limit of 2", ); } else { assert!(item_count <= 2); break; } } assert!(expected_changes.is_empty(), "{:?}", expected_changes); // Test 10: Expect changes after deletion client .request("DELETE", &new_file, "") .await .with_status(StatusCode::NO_CONTENT); let response = client .sync_collection( &user_base_path, &sync_token, Depth::Infinity, None, ["D:getetag"], ) .await; sync_token = response.sync_token().to_string(); response .with_href_count(1) .with_value("D:multistatus.D:response.D:href", &new_file) .with_value( "D:multistatus.D:response.D:status", "HTTP/1.1 404 Not Found", ); client .request("DELETE", &new_collection, "") .await .with_status(StatusCode::NO_CONTENT); let response = client .sync_collection( &user_base_path, &sync_token, Depth::Infinity, None, ["D:getetag"], ) .await; sync_token = response.sync_token().to_string(); response .with_href_count(1) .with_value("D:multistatus.D:response.D:href", &new_collection) .with_value( "D:multistatus.D:response.D:status", "HTTP/1.1 404 Not Found", ); client .request("DELETE", &folder_name, "") .await .with_status(StatusCode::NO_CONTENT); client .sync_collection( &user_base_path, &sync_token, Depth::Infinity, None, ["D:getetag"], ) .await .with_href_count(1) .with_value("D:multistatus.D:response.D:href", &folder_name) .with_value( "D:multistatus.D:response.D:status", "HTTP/1.1 404 Not Found", ); } client.delete_default_containers().await; test.assert_is_empty().await; }