Everything clients, users and operators meet now carries the fork's name, with no aliases (SPEC.md §2.4, changed here from "protocol identifiers stay"): - JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside the fork's own urn:inbuxa:jmap. - WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once. - Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in; a unit test fails if Cargo.lock ever moves past the vendored copy. The trusted runtime now names itself too, rather than answering sieve-rs's default. - The web interface's OAuth client is inbuxa-webui. On every start the old stalwart-webui client is removed and any application naming it is moved over. - The spam filter's blobs are INBUXA_SPAM_*; every start moves any left under the old keys, so a trained model survives. - SQL stores and log files default to inbuxa, in the code and in the schema served to the admin (checksum regenerated). - Settings are INBUXA_* only. A STALWART_* variable that's set where its INBUXA_* one isn't stops the server at startup, naming it. - The version-upgrade messages link docs.inbuxa.org's migration page, and the OpenAPI description, smtp crate metadata and web-push test fixtures lose the name. Kept on purpose, allowlisted with reasons: the OAuth key-derivation contexts (renaming them would end every session and invalidate every sealed client id) and the hashed application prefix. Also fixes a latent start-up failure: ensure_client updated an existing first-party client with a revision of 0, which the registry's assertion never matches, so adding a redirect URI or changing the webmail secret failed start-up. And the principal session test now expects legacyProtocols (C-1, added 2026-09-21), which it had missed. Tested: the server builds without warnings; common's 106 unit tests, including the vendoring check; a new integration test for the two start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
206 lines
7.3 KiB
Rust
206 lines
7.3 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* 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("[email protected]").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::<String>()
|
|
.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;
|
|
}
|