DNS, DKIM and ACME improvements - part 4

This commit is contained in:
Maurus Decimus
2026-04-02 19:32:51 +02:00
parent c54ec2397a
commit 4a4dcfd7e3
37 changed files with 932 additions and 209 deletions

View File

@@ -0,0 +1,304 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
use common::network::dns::update::DNS_RECORDS;
use dns_update::DnsRecord;
use jmap_proto::error::set::SetErrorType;
use registry::{
schema::{
enums::{AcmeChallengeType, AcmeRenewBefore, DnsRecordType},
prelude::{ObjectType, Property},
structs::{
AcmeProvider, Certificate, CertificateManagement, CertificateManagementProperties,
DkimManagement, DnsManagement, DnsManagementProperties, DnsServer, DnsServerCloudflare,
Domain, SecretKey, SecretKeyValue, Task, TaskDomainManagement,
},
},
types::map::Map,
};
use serde_json::json;
pub async fn test(test: &TestServer) {
println!("Running ACME tests...");
let account = test.account("admin@example.org");
// Create test Pebble and In Memory DNS servers
let pebble_dns_id = account
.registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare {
email: "test@pebble.org".to_string().into(),
secret: SecretKey::Value(SecretKeyValue {
secret: "secret".into(),
}),
..Default::default()
}))
.await;
let in_memory_dns_id = account
.registry_create_object(DnsServer::Cloudflare(DnsServerCloudflare {
email: "test@memory.org".to_string().into(),
secret: SecretKey::Value(SecretKeyValue {
secret: "secret".into(),
}),
..Default::default()
}))
.await;
// ACME provider creation should fail without a contact email
account
.registry_create_object_expect_err(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
..Default::default()
})
.await
.assert_type(SetErrorType::InvalidProperties)
.assert_description_contains("At least one contact email is required");
// Create an ACME provider using TLS-ALPN-01 challenge
let tls_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:hello@tls.org".to_string()]),
challenge_type: AcmeChallengeType::TlsAlpn01,
renew_before: AcmeRenewBefore::R12,
..Default::default()
})
.await;
let provider = account.registry_get::<AcmeProvider>(tls_acme_id).await;
assert_eq!(provider.directory, "https://localhost:14000/dir");
assert_eq!(
provider.contact,
Map::new(vec!["mailto:hello@tls.org".to_string()])
);
assert!(
provider.account_uri.starts_with("https://localhost:14000"),
"Provider {:?} has invalid account URI",
provider
);
// Create a domain and trigger TLS-ALPN-01 ACME renewal
let domain_id = account
.registry_create_object(Domain {
name: "tls.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: tls_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: in_memory_dns_id,
publish_records: Map::new(vec![
DnsRecordType::Tlsa,
DnsRecordType::AutoConfig,
DnsRecordType::AutoConfigLegacy,
DnsRecordType::AutoDiscover,
DnsRecordType::MtaSts,
]),
..Default::default()
}),
..Default::default()
})
.await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(
sans,
vec![
"autoconfig.tls.org".to_string(),
"autodiscover.tls.org".to_string(),
"mta-sts.tls.org".to_string(),
"ua-auto-config.tls.org".to_string()
]
);
// Make sure the TLSA records were added to the in-memory DNS server
let records = DNS_RECORDS.lock().unwrap().clone();
for record in [
"_443._tcp.mta-sts.tls.org.",
"_443._tcp.autoconfig.tls.org.",
"_443._tcp.ua-auto-config.tls.org.",
"_443._tcp.autodiscover.tls.org.",
] {
if records
.iter()
.find(|r| r.name == record && matches!(r.record, DnsRecord::TLSA(_)))
.is_none()
{
panic!(
"Expected TLSA record for {} not found in DNS records: {:?}",
record, records
);
}
}
// Make sure a task was created to renew the certificate before it expires
let tasks = account.registry_get_all::<Task>().await;
assert_eq!(
tasks.len(),
1,
"Expected 1 task, found {}: {:?}",
tasks.len(),
tasks
);
let task = tasks.into_iter().next().unwrap().1;
if let Task::AcmeRenewal(TaskDomainManagement {
domain_id: task_domain_id,
..
}) = task
{
assert_eq!(
task_domain_id, domain_id,
"ACME renewal task has incorrect domain ID"
);
} else {
panic!("Expected ACME renewal task, found: {:?}", task);
}
let not_valid_after = certificate.not_valid_after.timestamp();
let not_valid_before = certificate.not_valid_before.timestamp();
let length = not_valid_after - not_valid_before;
assert_eq!(
not_valid_after - length / 2,
task.due_timestamp() as i64,
"ACME renewal task has incorrect due timestamp, expected around {} but found {}",
not_valid_after - length / 2,
task.due_timestamp() as i64
);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Test ACME using HTTP-01 challenge and the server domain "mail.example.org"
let http_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:hello@example.org".to_string()]),
challenge_type: AcmeChallengeType::Http01,
..Default::default()
})
.await;
let domain_id = account.find_or_create_domain("example.org").await;
account.registry_update_object(ObjectType::Domain, domain_id, json!({
Property::CertificateManagement: CertificateManagement::Automatic(CertificateManagementProperties {
acme_provider_id: http_acme_id,
subject_alternative_names: Default::default(),
}),
})).await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(
sans,
vec![
"autoconfig.example.org".to_string(),
"autodiscover.example.org".to_string(),
"imap.example.org".to_string(),
"mail.example.org".to_string(),
"mta-sts.example.org".to_string(),
"mx1.example.org".to_string(),
"mx2.example.org".to_string(),
"pop3.example.org".to_string(),
"smtp.example.org".to_string(),
"ua-auto-config.example.org".to_string()
]
);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Test ACME using DNS-01 challenge
let dns_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:hello@dns.org".to_string()]),
challenge_type: AcmeChallengeType::Dns01,
..Default::default()
})
.await;
account
.registry_create_object(Domain {
name: "dns.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: dns_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: pebble_dns_id,
publish_records: Map::new(vec![DnsRecordType::Caa]),
..Default::default()
}),
..Default::default()
})
.await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(sans, vec!["*.dns.org".to_string()]);
account.registry_destroy_all(ObjectType::Certificate).await;
account.registry_destroy_all(ObjectType::Task).await;
// Test ACME using DNS-01 challenge
let dns_acme_id = account
.registry_create_object(AcmeProvider {
directory: "https://localhost:14000/dir".to_string(),
contact: Map::new(vec!["mailto:hello@persist.org".to_string()]),
challenge_type: AcmeChallengeType::DnsPersist01,
..Default::default()
})
.await;
account
.registry_create_object(Domain {
name: "persist.org".to_string(),
certificate_management: CertificateManagement::Automatic(
CertificateManagementProperties {
acme_provider_id: dns_acme_id,
subject_alternative_names: Default::default(),
},
),
dkim_management: DkimManagement::Manual,
dns_management: DnsManagement::Automatic(DnsManagementProperties {
dns_server_id: pebble_dns_id,
publish_records: Map::new(vec![DnsRecordType::Caa]),
..Default::default()
}),
..Default::default()
})
.await;
test.wait_for_tasks_skip_not_due().await;
let certificate = account
.registry_get_all::<Certificate>()
.await
.into_iter()
.next()
.unwrap()
.1;
let mut sans = certificate.subject_alternative_names.into_inner();
sans.sort();
assert_eq!(sans, vec!["*.persist.org".to_string()]);
}

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
pub async fn test(test: &TestServer) {
println!("Running DKIM Management tests...");
}

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::TestServer;
pub async fn test(test: &TestServer) {
println!("Running DNS Management tests...");
}

117
tests/src/automation/mod.rs Normal file
View File

@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acme;
pub mod dkim;
pub mod dns;
use registry::{
schema::{
enums::{NetworkListenerProtocol, ServiceProtocol},
prelude::Property,
structs::{MailExchanger, Service, SystemSettings},
},
types::list::List,
};
use utils::map::vec_map::VecMap;
use crate::utils::server::TestServerBuilder;
#[tokio::test(flavor = "multi_thread")]
async fn automation_tests() {
let mut test = TestServerBuilder::new("automation_tests")
.await
.with_listener(NetworkListenerProtocol::Http, "http", 8898, false)
.await
.with_default_listeners()
.await
.build()
.await;
// Create admin account
let account = test.create_admin_account("admin@example.org").await;
account
.registry_update_setting(
SystemSettings {
mail_exchangers: List::from_iter([
MailExchanger {
priority: 10u64,
hostname: "mx1.example.org".to_string().into(),
},
MailExchanger {
priority: 20u64,
hostname: "mx2.example.org".to_string().into(),
},
]),
services: VecMap::from_iter([
(
ServiceProtocol::Caldav,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Carddav,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Imap,
Service {
cleartext: false,
hostname: "imap.example.org".to_string().into(),
},
),
(
ServiceProtocol::Jmap,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Managesieve,
Service {
cleartext: false,
..Default::default()
},
),
(
ServiceProtocol::Pop3,
Service {
cleartext: false,
hostname: "pop3.example.org".to_string().into(),
},
),
(
ServiceProtocol::Smtp,
Service {
cleartext: false,
hostname: "smtp.example.org".to_string().into(),
},
),
(
ServiceProtocol::Webdav,
Service {
cleartext: false,
..Default::default()
},
),
]),
..Default::default()
},
&[Property::MailExchangers, Property::Services],
)
.await;
account.reload_settings().await;
test.insert_account(account);
acme::test(&test).await;
}

View File

@@ -18,7 +18,7 @@ use registry::{
prelude::{ObjectType, Property, SocketAddr},
structs::{
ClusterListenerGroup, ClusterListenerGroupProperties, ClusterRole, ClusterTaskGroup,
Coordinator, Expression, Http, NatsCoordinator, NetworkListener, RedisStore,
Coordinator, NatsCoordinator, NetworkListener, RedisStore,
},
},
types::map::Map,
@@ -60,14 +60,6 @@ fn cluster_tests() {
// Create initial server
let test = TestServerBuilder::new("cluster_test_0")
.await
.with_object(Http {
base_url: Expression {
else_: "'https://127.0.0.1:' + local_port".to_string(),
..Default::default()
},
..Default::default()
})
.await
.with_object(coordinator)
.await

View File

@@ -13,6 +13,8 @@ use jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
#[cfg(test)]
pub mod automation;
#[cfg(test)]
pub mod cluster;
#[cfg(test)]

View File

@@ -21,8 +21,8 @@ use mail_auth::{
};
use registry::{
schema::structs::{
DkimReportSettings, DmarcReportSettings, Domain, Expression, ExpressionMatch, SenderAuth,
SpfReportSettings,
CertificateManagement, DkimManagement, DkimReportSettings, DmarcReportSettings,
DnsManagement, Domain, Expression, ExpressionMatch, SenderAuth, SpfReportSettings,
},
types::list::List,
};
@@ -45,6 +45,9 @@ async fn dmarc() {
let domain_id = admin
.registry_create_object(Domain {
name: "localdomain.org".into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
allow_relaying: true,
..Default::default()
})

View File

@@ -13,9 +13,9 @@ use crate::{
};
use core::panic;
use registry::schema::structs::{
Domain, Expression, LookupStore, MtaStageConnect, MtaStageData, MtaStageEhlo,
MtaStageMail, MtaStageRcpt, SieveSystemInterpreter, SieveSystemScript, SqliteStore,
StoreLookup,
CertificateManagement, DkimManagement, DnsManagement, Domain, Expression, LookupStore,
MtaStageConnect, MtaStageData, MtaStageEhlo, MtaStageMail, MtaStageRcpt,
SieveSystemInterpreter, SieveSystemScript, SqliteStore, StoreLookup,
};
use smtp::scripts::{ScriptResult, event_loop::RunScript};
use std::{fs, path::PathBuf};
@@ -36,6 +36,9 @@ async fn sieve_scripts() {
let domain_id = admin
.registry_create_object(Domain {
name: "foobar.org".into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
allow_relaying: true,
..Default::default()
})

View File

@@ -18,7 +18,8 @@ use mail_auth::{
use registry::schema::{
enums::{DkimCanonicalization, DkimRotationStage},
structs::{
Dkim1Signature, DkimSignature, Domain, Expression, SecretText, SecretTextValue, SenderAuth,
CertificateManagement, Dkim1Signature, DkimManagement, DkimSignature, DnsManagement,
Domain, Expression, SecretText, SecretTextValue, SenderAuth,
},
};
use std::time::{Duration, Instant};
@@ -40,6 +41,9 @@ async fn sign_and_seal() {
let domain_id = admin
.registry_create_object(Domain {
name: "example.com".into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
allow_relaying: true,
..Default::default()
})

View File

@@ -12,8 +12,9 @@ use registry::{
enums::{AccountType, Locale, Permission, StorageQuota},
prelude::{Object, ObjectType, Property},
structs::{
Account, Credential, CredentialPermissions, CredentialPermissionsList, CustomRoles,
Domain, EmailAlias, EncryptionAtRest, EncryptionSettings, GroupAccount, MailingList,
Account, CertificateManagement, Credential, CredentialPermissions,
CredentialPermissionsList, CustomRoles, DkimManagement, DnsManagement, Domain,
EmailAlias, EncryptionAtRest, EncryptionSettings, GroupAccount, MailingList,
PasswordCredential, Permissions, PermissionsList, PublicKey, Roles,
SecondaryCredential, UserAccount, UserRoles,
},
@@ -114,6 +115,9 @@ pub async fn test(test: &TestServer) {
.write(RegistryWrite::insert(
&Domain {
name: "test.org".into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
is_enabled: true,
..Default::default()
}
@@ -126,6 +130,9 @@ pub async fn test(test: &TestServer) {
.write(RegistryWrite::insert(
&Domain {
name: "test.net".into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
is_enabled: true,
..Default::default()
}
@@ -243,6 +250,9 @@ pub async fn test(test: &TestServer) {
Domain {
name: "test.org".into(),
is_enabled: true,
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
},
RegistryWriteResult::PrimaryKeyConflict {

View File

@@ -15,8 +15,9 @@ use registry::{
enums::{AccountType, StorageQuota},
prelude::{ObjectType, Property},
structs::{
Account, Credential, Domain, EmailAlias, Expression, ExpressionMatch, GroupAccount,
MailingList, PasswordCredential, SubAddressing, SubAddressingCustom, UserAccount,
Account, CertificateManagement, Credential, DkimManagement, DnsManagement, Domain,
EmailAlias, Expression, ExpressionMatch, GroupAccount, MailingList, PasswordCredential,
SubAddressing, SubAddressingCustom, UserAccount,
},
},
types::{EnumImpl, list::List, map::Map},
@@ -33,6 +34,9 @@ pub async fn test(test: &TestServer) {
let domain_id = account
.registry_create_object(Domain {
name: "example.com".to_string(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
aliases: Map::new(vec!["beispiel.de".to_string()]),
is_enabled: true,
catch_all_address: Some("catchy@example.com".to_string()),
@@ -60,6 +64,9 @@ pub async fn test(test: &TestServer) {
account
.registry_create_object_expect_err(Domain {
name: "example.com".to_string(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
})
.await
@@ -371,6 +378,9 @@ pub async fn test(test: &TestServer) {
.registry_create_object(Domain {
name: "another-example.com".to_string(),
is_enabled: true,
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
sub_addressing: SubAddressing::Custom(SubAddressingCustom {
custom_rule: Expression {
else_: "false".to_string(),

View File

@@ -18,10 +18,10 @@ use registry::{
enums::{AccountType, Permission, TenantStorageQuota},
prelude::{ObjectType, Property},
structs::{
Account, Credential, Dkim1Signature, DkimSignature, DnsServer, DnsServerCloudflare,
Domain, GroupAccount, MailingList, OAuthClient, PasswordCredential, Permissions,
PermissionsList, Role, SecretKey, SecretKeyValue, SecretText, SecretTextValue, Tenant,
UserAccount, UserRoles,
Account, CertificateManagement, Credential, Dkim1Signature, DkimManagement,
DkimSignature, DnsManagement, DnsServer, DnsServerCloudflare, Domain, GroupAccount,
MailingList, OAuthClient, PasswordCredential, Permissions, PermissionsList, Role,
SecretKey, SecretKeyValue, SecretText, SecretTextValue, Tenant, UserAccount, UserRoles,
},
},
types::{EnumImpl, ObjectImpl, list::List, map::Map},
@@ -60,6 +60,9 @@ pub async fn test(test: &mut TestServer) {
.registry_create_object(Domain {
name: format!("tenant{name}.org"),
member_tenant_id: tenant_id.into(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
})
.await;
@@ -176,6 +179,9 @@ pub async fn test(test: &mut TestServer) {
Domain {
name: format!("tenant{tenant_id_pos}.org"),
member_tenant_id,
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
},
)

View File

@@ -12,8 +12,9 @@ use registry::{
enums::Permission,
prelude::{ObjectType, Property},
structs::{
self, Credential, CustomRoles, Domain, EmailAlias, GroupAccount, PasswordCredential,
Permissions, PermissionsList, Roles, UserAccount,
self, CertificateManagement, Credential, CustomRoles, DkimManagement, DnsManagement,
Domain, EmailAlias, GroupAccount, PasswordCredential, Permissions, PermissionsList,
Roles, UserAccount,
},
},
types::{list::List, map::Map},
@@ -244,6 +245,9 @@ impl Account {
self.registry_create_object(Domain {
is_enabled: true,
name: name.to_string(),
certificate_management: CertificateManagement::Manual,
dns_management: DnsManagement::Manual,
dkim_management: DkimManagement::Manual,
..Default::default()
})
.await

View File

@@ -407,7 +407,14 @@ fn remove_server_set_props(value: &mut serde_json::Value) {
.and_then(|v| v.as_str())
.is_some_and(|t| ["AppPassword", "ApiKey"].contains(&t));
obj.retain(|k, v| {
!(["createdAt", "credentialId", "retireAt"].contains(&k.as_str())
!([
"createdAt",
"credentialId",
"retireAt",
"accountKey",
"accountUri",
]
.contains(&k.as_str())
|| (is_app_pass && k == "secret")
|| (k == "memberTenantId" && v.is_null()))
});

View File

@@ -42,8 +42,8 @@ use registry::{
enums::{DataStoreType, EventPolicy, NetworkListenerProtocol, TracingLevel},
prelude::{Object, ObjectType, SocketAddr},
structs::{
Authentication, Certificate, Expression, Http, NetworkListener, PublicText,
SecretKeyFile, SecretText, Tracer, TracerStdout,
Authentication, Certificate, NetworkListener, PublicText, SecretKeyFile, SecretText,
Tracer, TracerStdout,
},
},
types::{EnumImpl, map::Map},
@@ -149,28 +149,12 @@ impl TestServerBuilder {
] {
this = this.with_listener(protocol, name, port, use_tls).await;
}
this.with_object(Http {
base_url: Expression {
else_: "'https://127.0.0.1:8899'".to_string(),
..Default::default()
},
..Default::default()
})
.await
this
}
pub async fn with_http_listener(self, port: u16) -> Self {
self.with_listener(NetworkListenerProtocol::Http, "jmap", port, true)
.await
.with_object(Http {
base_url: Expression {
else_: format!("'https://127.0.0.1:{}'", port),
..Default::default()
},
..Default::default()
})
.await
}
pub async fn with_smtp_listener(self, port: u16) -> Self {
@@ -216,7 +200,7 @@ impl TestServerBuilder {
}
self.insert_object(NetworkListener {
bind: Map::new(vec![
SocketAddr::from_str(&format!("127.0.0.1:{port}")).unwrap(),
SocketAddr::from_str(&format!("0.0.0.0:{port}")).unwrap(),
]),
name: name.to_string(),
protocol,
@@ -469,11 +453,15 @@ impl TestServer {
}
pub async fn wait_for_tasks(&self) {
wait_for_tasks(&self.server, false).await;
wait_for_tasks(&self.server, false, false).await;
}
pub async fn wait_for_tasks_skip_failures(&self) {
wait_for_tasks(&self.server, true).await;
wait_for_tasks(&self.server, false, true).await;
}
pub async fn wait_for_tasks_skip_not_due(&self) {
wait_for_tasks(&self.server, true, false).await;
}
pub async fn blob_expire_all(&self) {

View File

@@ -21,6 +21,7 @@ use registry::{
},
types::{EnumImpl, duration::Duration},
};
use store::write::now;
use store::{
Deserialize, IterateParams, ValueKey,
write::{TaskQueueClass, ValueClass},
@@ -153,7 +154,7 @@ fn build_search_store(typ: SearchStoreType, _path: &str) -> SearchStore {
}
}
pub async fn wait_for_tasks(server: &Server, skip_permanent_failures: bool) {
pub async fn wait_for_tasks(server: &Server, skip_not_due: bool, skip_permanent_failures: bool) {
let mut count = 0;
loop {
let mut has_index_tasks = None;
@@ -169,7 +170,9 @@ pub async fn wait_for_tasks(server: &Server, skip_permanent_failures: bool) {
.ascending(),
|_, value| {
let task = Task::deserialize(value)?;
if skip_permanent_failures && matches!(task.status(), TaskStatus::Failed(_)) {
if (skip_permanent_failures && matches!(task.status(), TaskStatus::Failed(_)))
|| (skip_not_due && task.due_timestamp() > now())
{
Ok(true)
} else {
has_index_tasks = Some(task);
@@ -195,7 +198,7 @@ pub async fn wait_for_tasks(server: &Server, skip_permanent_failures: bool) {
pub async fn assert_is_empty(server: &Server, include_registry: bool) {
// Wait for pending index tasks
wait_for_tasks(server, false).await;
wait_for_tasks(server, false, false).await;
// Assert is empty
store_assert_is_empty(

View File

@@ -1174,7 +1174,8 @@ fn flatten_xml(xml: &str) -> Vec<(String, String)> {
}
}
Event::GeneralRef(entity) => {
let value: Cow<str> = match entity.as_ref() {
let entity_slice: &[u8] = entity.as_ref();
let value: Cow<str> = match entity_slice {
b"lt" => "<".into(),
b"gt" => ">".into(),
b"amp" => "&".into(),