/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; use dav_proto::schema::property::{DavProperty, WebDavProperty}; 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 LOCK/UNLOCK tests ({})...", resource_type.base_path() ); let base_path = format!("{}/john%40example.com", resource_type.base_path()); // Test 1: Creating a collection under an unmapped resource without providing a lock token should fail let path = format!("{base_path}/do-not-write"); let response = client .lock_create(&path, "super-owner", true, "infinity", "Second-123") .await .with_status(StatusCode::CREATED); let lock_token = response .with_value( "D:prop.D:lockdiscovery.D:activelock.D:owner.href", "super-owner", ) .with_value("D:prop.D:lockdiscovery.D:activelock.D:depth", "infinity") .with_value( "D:prop.D:lockdiscovery.D:activelock.D:timeout", "Second-123", ) .lock_token() .to_string(); // Test 2: Refreshing a lock token with an invalid a lock token should fail client .lock_refresh(&path, "urn:inbuxa:davlock:1234", "infinity", "Second-456") .await .with_status(StatusCode::PRECONDITION_FAILED); // Test 3: Refreshing a lock token with valid a lock token should succeed client .lock_refresh(&path, &lock_token, "infinity", "Second-456") .await .with_status(StatusCode::OK) .with_value( "D:prop.D:lockdiscovery.D:activelock.D:owner.href", "super-owner", ) .with_any_value( "D:prop.D:lockdiscovery.D:activelock.D:timeout", ["Second-456", "Second-455"], ); // Test 3: Creating a collection under an unmapped resource with a lock token should fail client .request_with_headers("MKCOL", &path, [], "") .await .with_status(StatusCode::LOCKED) .with_value("D:error.D:lock-token-submitted.D:href", &path); // Test 4: Creating a collection under a mapped resource with a lock token should succeed client .request_with_headers( "MKCOL", &path, [("if", format!("(<{lock_token}>)").as_str())], "", ) .await .with_status(StatusCode::CREATED); // Test 5: Creating a lock under an infinity locked resource should fail let file_path = format!("{path}/file.txt"); client .lock_create(&file_path, "super-owner", true, "0", "Second-123") .await .with_status(StatusCode::LOCKED) .with_value("D:error.D:lock-token-submitted.D:href", &path); // Test 6: Creating a file under a locked resource without a lock token should fail let contents = resource_type.generate(); client .request("PUT", &file_path, &contents) .await .with_status(StatusCode::LOCKED) .with_value("D:error.D:lock-token-submitted.D:href", &path); // Test 7: Creating a file under a locked resource with a lock token should succeed client .request_with_headers( "PUT", &file_path, [("if", format!("(<{lock_token}>)").as_str())], &contents, ) .await .with_status(StatusCode::CREATED); // Test 8: Locks should be included in propfind responses let response = client .propfind(&path, [DavProperty::WebDav(WebDavProperty::LockDiscovery)]) .await; for href in [path.clone() + "/", file_path] { let props = response.properties(&href); props .get(DavProperty::WebDav(WebDavProperty::LockDiscovery)) .with_some_values([ "D:activelock.D:owner.href:super-owner", "D:activelock.D:depth:infinity", format!("D:activelock.D:locktoken.D:href:{lock_token}").as_str(), format!("D:activelock.D:lockroot.D:href:{path}").as_str(), "D:activelock.D:locktype.D:write", "D:activelock.D:lockscope.D:exclusive", ]) .with_any_values([ "D:activelock.D:timeout:Second-456", "D:activelock.D:timeout:Second-455", ]); } // Test 9: Delete with and without a lock token client .request("DELETE", &path, "") .await .with_status(StatusCode::LOCKED) .with_value("D:error.D:lock-token-submitted.D:href", &path); client .request_with_headers( "DELETE", &path, [("if", format!("(<{lock_token}>)").as_str())], "", ) .await .with_status(StatusCode::NO_CONTENT); // Test 10: Unlock with and without a lock token client .unlock(&path, "urn:inbuxa:davlock:1234") .await .with_status(StatusCode::CONFLICT) .with_value("D:error.D:lock-token-matches-request-uri", ""); client .unlock(&path, &lock_token) .await .with_status(StatusCode::NO_CONTENT); // Test 11: Locking with a large dead property should fail let path = format!("{base_path}/invalid-lock"); client .lock_create( &path, (0..=test.server.core.groupware.dead_property_size.unwrap() + 1) .map(|_| "a") .collect::() .as_str(), true, "infinity", "Second-123", ) .await .with_status(StatusCode::PAYLOAD_TOO_LARGE); // Test 12: Too many locks should fail for i in 0..test.server.core.groupware.max_locks_per_user { client .lock_create( &format!("{base_path}/invalid-lock-{i}"), "super-owner", true, "infinity", "Second-123", ) .await .with_status(StatusCode::CREATED); } client .lock_create( &format!("{base_path}/invalid-lock-greedy"), "super-owner", true, "infinity", "Second-123", ) .await .with_status(StatusCode::TOO_MANY_REQUESTS); } client.delete_default_containers().await; test.assert_is_empty().await; }