From 9b118bceef3423e28070181993dd2f7cf124e014 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:31:36 +0100 Subject: [PATCH] Registry testing - part 10 --- Cargo.lock | 41 +- crates/common/src/cache/invalidate.rs | 9 +- crates/common/src/config/server/listener.rs | 2 +- crates/common/src/config/smtp/queue.rs | 17 +- crates/common/src/enterprise/alerts.rs | 6 - crates/common/src/ipc.rs | 1 + crates/common/src/telemetry/metrics/store.rs | 35 +- crates/common/src/telemetry/tracers/store.rs | 41 +- crates/common/src/telemetry/webhooks/mod.rs | 9 +- crates/http/src/management/diagnose.rs | 18 +- crates/jmap/src/registry/mapping/action.rs | 7 + .../src/registry/mapping/archived_item.rs | 1 - crates/jmap/src/registry/mapping/task.rs | 10 +- crates/jmap/src/registry/mapping/telemetry.rs | 9 +- crates/registry/src/utils/task.rs | 1 + crates/services/src/broadcast/mod.rs | 12 +- crates/services/src/broadcast/subscriber.rs | 4 + crates/services/src/task_manager/index.rs | 18 +- .../services/src/task_manager/maintenance.rs | 27 + crates/services/src/task_manager/manager.rs | 143 ++- crates/services/src/task_manager/mod.rs | 6 +- crates/smtp/src/outbound/lookup.rs | 65 +- crates/smtp/src/outbound/mod.rs | 21 +- crates/store/src/registry/mod.rs | 18 +- crates/trc/event-macro/src/lib.rs | 2 +- crates/trc/src/event/enums.rs | 7 +- crates/trc/src/event/enums_impl.rs | 60 +- crates/trc/src/ipc/metrics.rs | 2 +- tests/Cargo.toml | 2 +- tests/src/jmap/calendar/acl.rs | 6 +- tests/src/jmap/calendar/alarm.rs | 12 +- tests/src/jmap/calendar/calendars.rs | 9 +- tests/src/jmap/calendar/event.rs | 28 +- tests/src/jmap/calendar/identity.rs | 8 +- tests/src/jmap/calendar/notification.rs | 9 +- tests/src/jmap/contacts/acl.rs | 6 +- tests/src/jmap/contacts/addressbook.rs | 10 +- tests/src/jmap/contacts/contact.rs | 26 +- tests/src/jmap/core/blob.rs | 13 +- tests/src/jmap/core/event_source.rs | 5 +- tests/src/jmap/core/push_subscription.rs | 101 +- tests/src/jmap/core/websocket.rs | 4 +- tests/src/jmap/files/acl.rs | 4 +- tests/src/jmap/files/node.rs | 7 +- tests/src/jmap/mail/acl.rs | 60 +- tests/src/jmap/mail/changes.rs | 8 +- tests/src/jmap/mail/copy.rs | 14 +- tests/src/jmap/mail/get.rs | 10 +- tests/src/jmap/mail/mailbox.rs | 15 +- tests/src/jmap/mail/mod.rs | 3 - tests/src/jmap/mail/parse.rs | 9 +- tests/src/jmap/mail/query.rs | 36 +- tests/src/jmap/mail/query_changes.rs | 12 +- tests/src/jmap/mail/search_snippet.rs | 11 +- tests/src/jmap/mail/set.rs | 17 +- tests/src/jmap/mail/sieve_script.rs | 16 +- tests/src/jmap/mail/submission.rs | 12 +- tests/src/jmap/mail/thread_get.rs | 4 +- tests/src/jmap/mail/thread_merge.rs | 35 +- tests/src/jmap/mail/vacation_response.rs | 18 +- tests/src/jmap/mod.rs | 941 +++--------------- tests/src/jmap/principal/availability.rs | 12 +- tests/src/jmap/principal/get.rs | 11 +- tests/src/jmap/server/enterprise.rs | 412 -------- tests/src/jmap/server/mod.rs | 16 - tests/src/lib.rs | 35 +- tests/src/smtp/mod.rs | 114 --- tests/src/system/antispam.rs | 2 +- tests/src/system/archiving.rs | 40 +- tests/src/system/authentication.rs | 4 +- tests/src/system/authorization.rs | 6 +- tests/src/system/crypto.rs | 2 +- tests/src/system/delivery.rs | 4 +- tests/src/system/directory.rs | 3 +- tests/src/system/mod.rs | 30 +- tests/src/system/oidc.rs | 2 +- tests/src/system/purge.rs | 3 +- tests/src/system/quota.rs | 3 +- tests/src/system/security.rs | 2 +- tests/src/system/task.rs | 223 +++++ tests/src/system/tenant.rs | 5 +- tests/src/telemetry/alerts.rs | 115 +++ tests/src/telemetry/metrics.rs | 164 +++ tests/src/telemetry/mod.rs | 65 ++ tests/src/telemetry/tracing.rs | 170 ++++ .../{jmap/server => telemetry}/webhooks.rs | 133 ++- tests/src/utils/account.rs | 161 ++- tests/src/utils/cleanup.rs | 26 +- tests/src/utils/dns.rs | 85 ++ tests/src/utils/http_server.rs | 87 +- tests/src/utils/jmap.rs | 2 +- tests/src/utils/mod.rs | 2 + tests/src/utils/registry.rs | 4 +- tests/src/utils/server.rs | 45 +- tests/src/utils/storage.rs | 15 +- 95 files changed, 2003 insertions(+), 2063 deletions(-) delete mode 100644 tests/src/jmap/server/enterprise.rs delete mode 100644 tests/src/jmap/server/mod.rs create mode 100644 tests/src/system/task.rs create mode 100644 tests/src/telemetry/alerts.rs create mode 100644 tests/src/telemetry/metrics.rs create mode 100644 tests/src/telemetry/mod.rs create mode 100644 tests/src/telemetry/tracing.rs rename tests/src/{jmap/server => telemetry}/webhooks.rs (62%) create mode 100644 tests/src/utils/dns.rs diff --git a/Cargo.lock b/Cargo.lock index 41de729a..7c2b5e1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,7 +1089,7 @@ dependencies = [ "jmap_proto", "libc", "lz4_flex 0.12.0", - "mail-auth 0.8.0", + "mail-auth", "mail-builder", "mail-parser", "mail-send", @@ -2969,7 +2969,7 @@ dependencies = [ "hyper-util", "jmap", "jmap_proto", - "mail-auth 0.8.0", + "mail-auth", "mail-builder", "mail-parser", "mail-send", @@ -3635,7 +3635,7 @@ dependencies = [ "jmap-tools", "jmap_proto", "lz4_flex 0.12.0", - "mail-auth 0.8.0", + "mail-auth", "mail-builder", "mail-parser", "mail-send", @@ -4095,27 +4095,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "mail-auth" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7da45f78cc525d3750b623c967ae21c0cd28b2e6a9a2ee4b536a7cce3b21ce" -dependencies = [ - "ahash", - "flate2", - "hashify", - "hickory-resolver", - "mail-builder", - "mail-parser", - "quick-xml 0.38.4", - "quick_cache", - "ring", - "rustls-pki-types", - "serde", - "serde_json", - "zip", -] - [[package]] name = "mail-auth" version = "0.8.0" @@ -4302,7 +4281,7 @@ dependencies = [ "futures", "groupware", "lz4_flex 0.12.0", - "mail-auth 0.8.0", + "mail-auth", "mail-parser", "nlp", "num_cpus", @@ -6084,7 +6063,7 @@ dependencies = [ "ahash", "hashify", "jmap-tools", - "mail-auth 0.8.0", + "mail-auth", "serde", "serde_json", "tokio", @@ -7236,7 +7215,7 @@ dependencies = [ "hyper 1.8.1", "hyper-util", "lru-cache", - "mail-auth 0.8.0", + "mail-auth", "mail-builder", "mail-parser", "mail-send", @@ -7333,7 +7312,7 @@ dependencies = [ "hyper 1.8.1", "idna", "infer 0.19.0", - "mail-auth 0.8.0", + "mail-auth", "mail-builder", "mail-parser", "mail-send", @@ -7653,7 +7632,7 @@ dependencies = [ "jmap-client", "jmap-tools", "jmap_proto", - "mail-auth 0.7.5", + "mail-auth", "mail-builder", "mail-parser", "mail-send", @@ -8185,7 +8164,7 @@ dependencies = [ "compact_str", "event_macro", "hashify", - "mail-auth 0.8.0", + "mail-auth", "mail-parser", "parking_lot", "reqwest", @@ -8459,7 +8438,7 @@ dependencies = [ "form_urlencoded", "futures", "http-body-util", - "mail-auth 0.8.0", + "mail-auth", "mail-send", "nohash-hasher", "parking_lot", diff --git a/crates/common/src/cache/invalidate.rs b/crates/common/src/cache/invalidate.rs index 473c99f7..6218fe98 100644 --- a/crates/common/src/cache/invalidate.rs +++ b/crates/common/src/cache/invalidate.rs @@ -200,7 +200,6 @@ impl Server { if changes.is_empty() { return Ok(()); } - let c = println!("Invalidating caches for changes: {:?}", changes); // Invalidate objects linking roles let mut role_ids = changes @@ -262,12 +261,11 @@ impl Server { } pub fn invalidate_all_local_caches(&self) { + self.invalidate_all_local_negative_caches(); self.inner.cache.access_tokens.clear(); self.inner.cache.domains.clear(); self.inner.cache.domain_names.clear(); - self.inner.cache.domain_names_negative.clear(); self.inner.cache.emails.clear(); - self.inner.cache.emails_negative.clear(); self.inner.cache.tenants.clear(); self.inner.cache.files.clear(); self.inner.cache.contacts.clear(); @@ -280,6 +278,11 @@ impl Server { self.inner.data.logos.lock().clear(); } + pub fn invalidate_all_local_negative_caches(&self) { + self.inner.cache.domain_names_negative.clear(); + self.inner.cache.emails_negative.clear(); + } + pub async fn invalidate_local_caches(&self, changes: &[CacheInvalidation]) { let cache = &self.inner.cache; diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index fdd8185e..a966c825 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -54,7 +54,7 @@ impl Listeners { servers } - fn parse_server( + pub fn parse_server( &mut self, bp: &mut Bootstrap, listener: RegistryObject, diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 99925f23..c2e4d7b4 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -179,7 +179,7 @@ pub struct QueueQuota { #[derive(Clone, Hash, PartialEq, Eq)] pub struct RelayConfig { - pub address: String, + pub address: HostOrIp, pub port: u16, pub protocol: ServerProtocol, pub auth: Option>, @@ -187,6 +187,12 @@ pub struct RelayConfig { pub tls_allow_invalid_certs: bool, } +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub enum HostOrIp { + Host(T), + Ip { ip: IpAddr, ip_str: String }, +} + #[derive(Debug, Clone, Copy, Default)] pub enum RequireOptional { #[default] @@ -368,7 +374,14 @@ impl QueueConfig { queue.routing_strategy.insert( route.name, RoutingStrategy::Relay(RelayConfig { - address: route.address, + address: if let Ok(ip) = route.address.parse() { + HostOrIp::Ip { + ip, + ip_str: route.address, + } + } else { + HostOrIp::Host(route.address) + }, port: route.port as u16, protocol: match route.protocol { enums::MtaProtocol::Smtp => ServerProtocol::Smtp, diff --git a/crates/common/src/enterprise/alerts.rs b/crates/common/src/enterprise/alerts.rs index cbcd3fa0..79a8e120 100644 --- a/crates/common/src/enterprise/alerts.rs +++ b/crates/common/src/enterprise/alerts.rs @@ -105,12 +105,6 @@ impl Server { Id = alert.id.id().id(), Details = message.as_ref().map(|m| m.build()) ); - - #[cfg(feature = "test_mode")] - Collector::update_event_counter( - trc::EventType::Telemetry(TelemetryEvent::AlertEvent), - 1, - ); } } } diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs index cc129f79..a1b9a0ca 100644 --- a/crates/common/src/ipc.rs +++ b/crates/common/src/ipc.rs @@ -76,6 +76,7 @@ pub enum BroadcastEvent { RegistryChange(RegistryChange), CacheInvalidate(Vec), CacheInvalidateAll, + CacheInvalidateNegative, MtaQueueStatus { is_running: bool }, } diff --git a/crates/common/src/telemetry/metrics/store.rs b/crates/common/src/telemetry/metrics/store.rs index f09b400d..03d372b8 100644 --- a/crates/common/src/telemetry/metrics/store.rs +++ b/crates/common/src/telemetry/metrics/store.rs @@ -83,24 +83,25 @@ impl MetricsStore for Store { let history = history.events.entry(event).or_insert(0); let diff = reading - *history; - #[cfg(not(feature = "test_mode"))] - let metric_id = SnowflakeIdGenerator::from_sequence_id(event.to_id() as u64) - .unwrap_or_default(); - - #[cfg(feature = "test_mode")] - let metric_id = _timestamp - .map(|timestamp| { - SnowflakeIdGenerator::from_timestamp_and_sequence_id( - timestamp, - event.to_id() as u64, - ) - }) - .unwrap_or_else(|| { - SnowflakeIdGenerator::from_sequence_id(event.to_id() as u64) - }) - .unwrap_or_default(); - if diff > 0 { + #[cfg(not(feature = "test_mode"))] + let metric_id = + SnowflakeIdGenerator::from_sequence_id(event.to_id() as u64) + .unwrap_or_default(); + + #[cfg(feature = "test_mode")] + let metric_id = _timestamp + .map(|timestamp| { + SnowflakeIdGenerator::from_timestamp_and_sequence_id( + timestamp, + event.to_id() as u64, + ) + }) + .unwrap_or_else(|| { + SnowflakeIdGenerator::from_sequence_id(event.to_id() as u64) + }) + .unwrap_or_default(); + batch.set( ValueClass::Telemetry(TelemetryClass::Metric(metric_id)), Metric::Counter(MetricCount { diff --git a/crates/common/src/telemetry/tracers/store.rs b/crates/common/src/telemetry/tracers/store.rs index 46682ea7..92db1625 100644 --- a/crates/common/src/telemetry/tracers/store.rs +++ b/crates/common/src/telemetry/tracers/store.rs @@ -9,7 +9,7 @@ */ use crate::config::telemetry::StoreTracer; -use ahash::{AHashMap, AHashSet}; +use ahash::AHashMap; use registry::{ pickle::Pickle, schema::structs::{ @@ -267,58 +267,35 @@ impl StoreTracer { } } -pub fn build_span_document( - span_id: u64, - trace: Trace, - index_fields: &AHashSet, -) -> IndexDocument { +pub fn build_span_document(span_id: u64, trace: Trace) -> IndexDocument { let mut document = IndexDocument::new(SearchIndex::Tracing).with_id(span_id); let mut keywords = HashSet::new(); for (idx, event) in trace.events.into_iter().enumerate() { - if idx == 0 - && (index_fields.is_empty() - || index_fields.contains(&TracingSearchField::EventType.into())) - { + if idx == 0 { document.index_unsigned(TracingSearchField::EventType, event.event.to_id()); } for TraceKeyValue { key, value } in event.key_values { match (key, value) { (Key::QueueId, TraceValue::UnsignedInt(TraceValueUnsignedInt { value })) => { - if index_fields.is_empty() - || index_fields.contains(&TracingSearchField::QueueId.into()) - { - document.index_unsigned(TracingSearchField::QueueId, value); - } + document.index_unsigned(TracingSearchField::QueueId, value); } ( Key::From | Key::To | Key::Domain | Key::Hostname, TraceValue::String(TraceValueString { value }), ) => { - if index_fields.is_empty() - || index_fields.contains(&TracingSearchField::Keywords.into()) - { - keywords.insert(value); - } + keywords.insert(value); } (Key::To, TraceValue::List(TraceValueList { value })) => { - if index_fields.is_empty() - || index_fields.contains(&TracingSearchField::Keywords.into()) - { - for value in value { - if let TraceValue::String(TraceValueString { value }) = value { - keywords.insert(value); - } + for value in value { + if let TraceValue::String(TraceValueString { value }) = value { + keywords.insert(value); } } } (Key::RemoteIp, TraceValue::IpAddr(TraceValueIpAddr { value })) => { - if index_fields.is_empty() - || index_fields.contains(&TracingSearchField::Keywords.into()) - { - keywords.insert(value.to_string()); - } + keywords.insert(value.to_string()); } _ => {} diff --git a/crates/common/src/telemetry/webhooks/mod.rs b/crates/common/src/telemetry/webhooks/mod.rs index 3660efb7..430fecd8 100644 --- a/crates/common/src/telemetry/webhooks/mod.rs +++ b/crates/common/src/telemetry/webhooks/mod.rs @@ -4,6 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{LONG_1Y_SLUMBER, config::telemetry::WebhookTracer}; +use base64::{Engine, engine::general_purpose::STANDARD}; +use ring::hmac; +use serde::Serialize; use std::{ sync::{ Arc, @@ -11,11 +15,6 @@ use std::{ }, time::Instant, }; - -use crate::{LONG_1Y_SLUMBER, config::telemetry::WebhookTracer}; -use base64::{Engine, engine::general_purpose::STANDARD}; -use ring::hmac; -use serde::Serialize; use store::write::now; use tokio::sync::mpsc; use trc::{ diff --git a/crates/http/src/management/diagnose.rs b/crates/http/src/management/diagnose.rs index a972ccd6..2a5014eb 100644 --- a/crates/http/src/management/diagnose.rs +++ b/crates/http/src/management/diagnose.rs @@ -7,7 +7,7 @@ use common::{ Server, config::smtp::{ - queue::MxConfig, + queue::{HostOrIp, MxConfig}, resolver::{Policy, Tlsa}, }, }; @@ -429,12 +429,12 @@ async fn delivery_diagnose( tx.send(DeliveryStage::IpLookupStart).await?; let now = Instant::now(); + let hostname = match host.fqdn_hostname() { + HostOrIp::Host(host) => host.into_owned(), + HostOrIp::Ip { ip_str, .. } => ip_str, + }; match server - .ip_lookup( - host.fqdn_hostname().as_ref(), - IpLookupStrategy::Ipv4thenIpv6, - usize::MAX, - ) + .ip_lookup(&hostname, IpLookupStrategy::Ipv4thenIpv6, usize::MAX) .await { Ok(remote_ips) if !remote_ips.is_empty() => { @@ -461,7 +461,7 @@ async fn delivery_diagnose( tx.send(DeliveryStage::ReadGreetingStart).await?; let now = Instant::now(); - if let Err(status) = client.read_greeting(hostname).await { + if let Err(status) = client.read_greeting(&hostname).await { tx.send(DeliveryStage::ReadGreetingError { elapsed: now.elapsed_ms(), reason: status.to_string(), @@ -524,7 +524,7 @@ async fn delivery_diagnose( let mut client = match client .try_start_tls( &server.inner.data.smtp_connectors.pki_verify, - hostname, + &hostname, &capabilities, ) .await @@ -563,7 +563,7 @@ async fn delivery_diagnose( if let Some(dane_policy) = &dane_policy { if let Err(err) = dane_policy.verify( 0, - hostname, + &hostname, client.tls_connection().peer_certificates(), ) { tx.send(DeliveryStage::DaneVerifyError { diff --git a/crates/jmap/src/registry/mapping/action.rs b/crates/jmap/src/registry/mapping/action.rs index 15e98cc0..f5279a29 100644 --- a/crates/jmap/src/registry/mapping/action.rs +++ b/crates/jmap/src/registry/mapping/action.rs @@ -109,6 +109,13 @@ pub(crate) async fn action_set( .await; set.response.created(id, now()); } + Action::InvalidateNegativeCaches => { + set.server.invalidate_all_local_negative_caches(); + set.server + .cluster_broadcast(BroadcastEvent::CacheInvalidateNegative) + .await; + set.response.created(id, now()); + } Action::PauseMtaQueue => { let _ = set .server diff --git a/crates/jmap/src/registry/mapping/archived_item.rs b/crates/jmap/src/registry/mapping/archived_item.rs index 83f0a3cc..343c84be 100644 --- a/crates/jmap/src/registry/mapping/archived_item.rs +++ b/crates/jmap/src/registry/mapping/archived_item.rs @@ -296,7 +296,6 @@ pub(crate) async fn archived_item_query( false } } - _ => false, })?; diff --git a/crates/jmap/src/registry/mapping/task.rs b/crates/jmap/src/registry/mapping/task.rs index c02d74fc..9ddbee6b 100644 --- a/crates/jmap/src/registry/mapping/task.rs +++ b/crates/jmap/src/registry/mapping/task.rs @@ -226,6 +226,8 @@ pub(crate) async fn task_set( ) .commit_point(); } + + set.response.updated.append(id, None); } // Process destructions @@ -343,19 +345,23 @@ pub(crate) async fn task_set( set.response.destroyed.push(id); } - if !batch.is_empty() { + let has_changes = !batch.is_empty(); + if has_changes { set.server .store() .write(batch.build_all()) .await .caused_by(trc::location!())?; - set.server.notify_task_queue(); } for task_id in locked_tasks { set.server.remove_index_lock(task_id).await; } + if has_changes { + set.server.notify_task_queue(); + } + Ok(set) } diff --git a/crates/jmap/src/registry/mapping/telemetry.rs b/crates/jmap/src/registry/mapping/telemetry.rs index c7bdf8c2..0b24dc72 100644 --- a/crates/jmap/src/registry/mapping/telemetry.rs +++ b/crates/jmap/src/registry/mapping/telemetry.rs @@ -19,7 +19,10 @@ use common::Server; use jmap_proto::types::state::State; use registry::{ jmap::IntoValue, - schema::prelude::{Object, Property}, + schema::{ + prelude::Property, + structs::{Metric, Trace}, + }, types::datetime::UTCDateTime, }; use std::str::FromStr; @@ -67,7 +70,7 @@ pub(crate) async fn trace_get( if let Some(trace) = get .server .tracing_store() - .get_value::(ValueKey::from(ValueClass::Telemetry(TelemetryClass::Span( + .get_value::(ValueKey::from(ValueClass::Telemetry(TelemetryClass::Span( item_id, )))) .await? @@ -95,7 +98,7 @@ pub(crate) async fn metric_get( if let Some(metric) = get .server .metrics_store() - .get_value::(ValueKey::from(ValueClass::Telemetry( + .get_value::(ValueKey::from(ValueClass::Telemetry( TelemetryClass::Metric(item_id), ))) .await? diff --git a/crates/registry/src/utils/task.rs b/crates/registry/src/utils/task.rs index 57740c20..f7d354df 100644 --- a/crates/registry/src/utils/task.rs +++ b/crates/registry/src/utils/task.rs @@ -94,6 +94,7 @@ impl Action { Action::TroubleshootDmarc(_) => Permission::ActionTroubleshootDmarc, Action::ClassifySpam(_) => Permission::ActionClassifySpam, Action::InvalidateCaches => Permission::ActionInvalidateCaches, + Action::InvalidateNegativeCaches => Permission::ActionInvalidateNegativeCaches, Action::PauseMtaQueue => Permission::ActionPauseMtaQueue, Action::ResumeMtaQueue => Permission::ActionResumeMtaQueue, } diff --git a/crates/services/src/broadcast/mod.rs b/crates/services/src/broadcast/mod.rs index 86b8cdc8..f245efc3 100644 --- a/crates/services/src/broadcast/mod.rs +++ b/crates/services/src/broadcast/mod.rs @@ -116,11 +116,14 @@ impl BroadcastBatch> { BroadcastEvent::CacheInvalidateAll => { serialized.push(8u8); } + BroadcastEvent::CacheInvalidateNegative => { + serialized.push(9u8); + } BroadcastEvent::MtaQueueStatus { is_running } => { if *is_running { - serialized.push(9u8); - } else { serialized.push(10u8); + } else { + serialized.push(11u8); } } } @@ -236,8 +239,9 @@ where Ok(Some(BroadcastEvent::CacheInvalidate(items))) } 8 => Ok(Some(BroadcastEvent::CacheInvalidateAll)), - 9 => Ok(Some(BroadcastEvent::MtaQueueStatus { is_running: true })), - 10 => Ok(Some(BroadcastEvent::MtaQueueStatus { is_running: false })), + 9 => Ok(Some(BroadcastEvent::CacheInvalidateNegative)), + 10 => Ok(Some(BroadcastEvent::MtaQueueStatus { is_running: true })), + 11 => Ok(Some(BroadcastEvent::MtaQueueStatus { is_running: false })), _ => Err(()), } } else { diff --git a/crates/services/src/broadcast/subscriber.rs b/crates/services/src/broadcast/subscriber.rs index 84c82181..84baa34a 100644 --- a/crates/services/src/broadcast/subscriber.rs +++ b/crates/services/src/broadcast/subscriber.rs @@ -143,6 +143,9 @@ pub fn spawn_broadcast_subscriber(inner: Arc, mut shutdown_rx: watch::Rec BroadcastEvent::CacheInvalidateAll => { inner.build_server().invalidate_all_local_caches(); } + BroadcastEvent::CacheInvalidateNegative => { + inner.build_server().invalidate_all_local_negative_caches(); + } BroadcastEvent::MtaQueueStatus { is_running } => { let _ = inner .ipc @@ -244,6 +247,7 @@ fn log_event(event: &BroadcastEvent) -> trc::Value { trc::Value::Array(array) } BroadcastEvent::CacheInvalidateAll => "CacheInvalidateAll".into(), + BroadcastEvent::CacheInvalidateNegative => "CacheInvalidateNegative".into(), BroadcastEvent::MtaQueueStatus { is_running } => { if *is_running { "MtaQueueRunning".into() diff --git a/crates/services/src/task_manager/index.rs b/crates/services/src/task_manager/index.rs index 63e4b449..f8b58f08 100644 --- a/crates/services/src/task_manager/index.rs +++ b/crates/services/src/task_manager/index.rs @@ -519,17 +519,13 @@ async fn build_tracing_span_document( use common::telemetry::tracers::store::build_span_document; use registry::schema::structs::Trace; - if let Some(index_fields) = server.core.email.index_fields.get(&SearchIndex::Tracing) { - server - .tracing_store() - .get_value::(ValueKey::from(ValueClass::Telemetry(TelemetryClass::Span( - span_id, - )))) - .await - .map(|trace| trace.map(|trace| build_span_document(span_id, trace, index_fields))) - } else { - Ok(None) - } + server + .tracing_store() + .get_value::(ValueKey::from(ValueClass::Telemetry(TelemetryClass::Span( + span_id, + )))) + .await + .map(|trace| trace.map(|trace| build_span_document(span_id, trace))) } // SPDX-SnippetEnd diff --git a/crates/services/src/task_manager/maintenance.rs b/crates/services/src/task_manager/maintenance.rs index 9ec7a8cf..71b3e1bb 100644 --- a/crates/services/src/task_manager/maintenance.rs +++ b/crates/services/src/task_manager/maintenance.rs @@ -280,6 +280,33 @@ async fn store_maintenance( | TaskStoreMaintenanceType::ResetRateLimiters | TaskStoreMaintenanceType::ResetBlobQuotas | TaskStoreMaintenanceType::RemoveAuthTokens => { + #[cfg(feature = "test_mode")] + if let Some(test_var) = task.shard_index { + use crate::task_manager::TaskFailureType; + + // Simulate success for testing purposes + match test_var { + 0 => { + return Ok(TaskResult::Success); + } + 1 => { + return Ok(TaskResult::temporary( + "Simulated temporary failure".to_string(), + )); + } + 2 => { + return Ok(TaskResult::permanent("Simulated permanent failure")); + } + + retry => { + return Ok(TaskResult::Failure { + typ: TaskFailureType::Retry(retry), + message: "Simulated retry failure".to_string(), + }); + } + } + } + let prefixes = match task.maintenance_type { TaskStoreMaintenanceType::RemoveGreylist => &[KV_GREYLIST][..], TaskStoreMaintenanceType::RemoveLockQueueMessage => &[KV_LOCK_QUEUE_MESSAGE][..], diff --git a/crates/services/src/task_manager/manager.rs b/crates/services/src/task_manager/manager.rs index d35c175a..1d25010d 100644 --- a/crates/services/src/task_manager/manager.rs +++ b/crates/services/src/task_manager/manager.rs @@ -18,10 +18,10 @@ use crate::task_manager::{ DEFAULT_LOCK_EXPIRY, Locked, QUEUE_REFRESH_INTERVAL, TaskDetails, TaskFailureType, TaskInfo, TaskJob, TaskManagerIpc, TaskResult, }; +use common::BuildServer; use common::config::server::ServerProtocol; use common::network::limiter::ConcurrencyLimiter; use common::network::{ServerInstance, TcpAcceptor}; -use common::{BuildServer, IPC_CHANNEL_BUFFER}; use common::{Inner, Server}; use registry::pickle::Pickle; use registry::schema::enums::TaskType; @@ -45,8 +45,10 @@ use tokio::sync::{mpsc, watch}; use trc::TaskManagerEvent; use utils::snowflake::SnowflakeIdGenerator; +const TASK_QUEUE_BUFFER: usize = 10; + pub fn spawn_task_manager(inner: Arc) { - { + let is_clustered = { let server = inner.build_server(); let roles = &server.core.network.roles; @@ -58,7 +60,9 @@ pub fn spawn_task_manager(inner: Arc) { { return; } - } + + server.core.storage.coordinator.is_enabled() + }; trc::event!(TaskManager(TaskManagerEvent::ManagerStarted)); @@ -76,19 +80,39 @@ pub fn spawn_task_manager(inner: Arc) { // Spawn workers for each task type let mut txs = Vec::with_capacity(TaskType::COUNT); for idx in 0..TaskType::COUNT { - let (tx, mut rx) = mpsc::channel::(IPC_CHANNEL_BUFFER); + let task_type = TaskType::from_id(idx as u16).unwrap(); + let channel_capacity = match task_type { + TaskType::IndexDocument | TaskType::UnindexDocument | TaskType::IndexTrace => { + std::cmp::max( + inner.build_server().core.email.index_batch_size, + TASK_QUEUE_BUFFER, + ) + } + TaskType::DestroyAccount + | TaskType::AccountMaintenance + | TaskType::StoreMaintenance => 1, + TaskType::SpamFilterMaintenance => 2, + TaskType::CalendarAlarmEmail + | TaskType::CalendarAlarmNotification + | TaskType::CalendarItipMessage + | TaskType::MergeThreads + | TaskType::DmarcReport + | TaskType::TlsReport + | TaskType::RestoreArchivedItem => TASK_QUEUE_BUFFER, + }; + + let (tx, mut rx) = mpsc::channel::(channel_capacity); txs.push(tx); let inner = inner.clone(); let server_instance = server_instance.clone(); if matches!( - TaskType::from_id(idx as u16).unwrap(), - TaskType::IndexDocument | TaskType::UnindexDocument | TaskType::IndexTrace + task_type, + TaskType::IndexDocument | TaskType::UnindexDocument | TaskType::IndexTrace, ) { tokio::spawn(async move { while let Some(job) = rx.recv().await { let server = inner.build_server(); - let batch_size = server.core.email.index_batch_size; let mut batch = Vec::with_capacity(batch_size); match server @@ -151,8 +175,16 @@ pub fn spawn_task_manager(inner: Arc) { } // Dispatch - let results = server.index(&batch).await.into_iter().map(|r| r.result); + let mut refresh_queue = false; + let results = server.index(&batch).await.into_iter().map(|r| { + refresh_queue |= r.result.is_retry(); + r.result + }); update_tasks(&server, &mut batch, results).await; + + if refresh_queue || rx.is_empty() { + server.notify_task_queue(); + } } }); } else { @@ -160,6 +192,7 @@ pub fn spawn_task_manager(inner: Arc) { tokio::spawn(async move { while let Some(job) = rx.recv().await { let server = inner.build_server(); + let mut refresh_queue = false; match server .store() @@ -206,6 +239,8 @@ pub fn spawn_task_manager(inner: Arc) { | Task::IndexTrace(_) => unreachable!(), }; + refresh_queue = result.is_retry(); + update_tasks( &server, &mut [TaskDetails { task, info: job }], @@ -228,11 +263,16 @@ pub fn spawn_task_manager(inner: Arc) { ); } } + + if refresh_queue || rx.is_empty() { + server.notify_task_queue(); + } } }); } } + const REFRESH_INTERVAL: Duration = Duration::from_secs(60); tokio::spawn(async move { let mut ipc = TaskManagerIpc { txs: txs.try_into().expect("Incorrect number of task channels"), @@ -242,7 +282,10 @@ pub fn spawn_task_manager(inner: Arc) { let rx = inner.ipc.task_tx.clone(); loop { // Index any queued tasks - let sleep_for = inner.build_server().process_tasks(&mut ipc).await; + let mut sleep_for = inner.build_server().process_tasks(&mut ipc).await; + if is_clustered && sleep_for > REFRESH_INTERVAL { + sleep_for = REFRESH_INTERVAL; + } // Wait for a signal or sleep until the next task is due let _ = tokio::time::timeout(sleep_for, rx.notified()).await; @@ -277,6 +320,7 @@ impl TaskQueueManager for Server { let mut tasks = Vec::new(); let now = Instant::now(); let mut next_event = None; + let roles = &self.core.network.roles; ipc.revision += 1; let _ = self .store() @@ -294,14 +338,43 @@ impl TaskQueueManager for Server { .caused_by(trc::location!()) .ctx(trc::Key::Value, value) })?; + let enabled = match task_type { + TaskType::IndexDocument + | TaskType::UnindexDocument + | TaskType::IndexTrace => roles.search_indexing, + TaskType::AccountMaintenance | TaskType::DestroyAccount => { + roles.account_maintenance + } + TaskType::StoreMaintenance => roles.store_maintenance, + TaskType::SpamFilterMaintenance => roles.spam_training, + TaskType::CalendarAlarmEmail + | TaskType::CalendarAlarmNotification + | TaskType::CalendarItipMessage + | TaskType::MergeThreads + | TaskType::DmarcReport + | TaskType::TlsReport + | TaskType::RestoreArchivedItem => true, + }; + + if !enabled { + trc::event!( + TaskManager(TaskManagerEvent::TaskIgnored), + Id = task_id, + Details = task_type.as_str(), + Reason = "Task type is disabled by cluster roles.", + ); + return Ok(true); + } + match ipc.locked.entry(task_id) { Entry::Occupied(mut entry) => { let locked = entry.get_mut(); - if locked.expires <= now { + if locked.expires <= now || locked.due < task_due { locked.expires = Instant::now() + std::time::Duration::from_secs( DEFAULT_LOCK_EXPIRY + 1, ); + locked.due = task_due; tasks.push(( TaskJob { id: task_id, @@ -319,6 +392,7 @@ impl TaskQueueManager for Server { + std::time::Duration::from_secs( DEFAULT_LOCK_EXPIRY + 1, ), + due: task_due, revision: ipc.revision, }); tasks.push(( @@ -364,33 +438,11 @@ impl TaskQueueManager for Server { } // Dispatch tasks - let roles = &self.core.network.roles; for (task_job, task_type_idx) in tasks { - let enabled = match task_job.typ { - TaskType::IndexDocument | TaskType::UnindexDocument | TaskType::IndexTrace => { - roles.search_indexing - } - TaskType::AccountMaintenance | TaskType::DestroyAccount => { - roles.account_maintenance - } - TaskType::StoreMaintenance => roles.store_maintenance, - TaskType::SpamFilterMaintenance => roles.spam_training, - TaskType::CalendarAlarmEmail - | TaskType::CalendarAlarmNotification - | TaskType::CalendarItipMessage - | TaskType::MergeThreads - | TaskType::DmarcReport - | TaskType::TlsReport - | TaskType::RestoreArchivedItem => true, - }; + let tx = &ipc.txs[task_type_idx as usize]; - if enabled { - if self.try_lock_task(task_job.id).await - && ipc.txs[task_type_idx as usize] - .send(task_job) - .await - .is_err() - { + if tx.capacity() > 0 { + if self.try_lock_task(task_job.id).await && tx.send(task_job).await.is_err() { trc::event!( Server(trc::ServerEvent::ThreadError), Details = "Error sending task.", @@ -398,12 +450,8 @@ impl TaskQueueManager for Server { ); } } else { - trc::event!( - TaskManager(TaskManagerEvent::TaskIgnored), - Id = task_job.id, - Details = task_job.typ.as_str(), - Reason = "Task type is disabled by cluster roles.", - ); + // If the channel is full, release the lock so it can be picked up in the next iteration + ipc.locked.remove(&task_job.id); } } @@ -551,3 +599,16 @@ pub fn next_retry_time( Some(next_time) } + +impl TaskResult { + pub fn is_retry(&self) -> bool { + matches!( + self, + TaskResult::Update(_) + | TaskResult::Failure { + typ: TaskFailureType::Temporary | TaskFailureType::Retry(_), + .. + } + ) + } +} diff --git a/crates/services/src/task_manager/mod.rs b/crates/services/src/task_manager/mod.rs index a67a3039..e8d2b0e0 100644 --- a/crates/services/src/task_manager/mod.rs +++ b/crates/services/src/task_manager/mod.rs @@ -29,7 +29,7 @@ pub mod scheduler; pub mod spam_classifier; const QUEUE_REFRESH_INTERVAL: u64 = 60 * 5; // 5 minutes -const DEFAULT_LOCK_EXPIRY: u64 = 60 * 5; // 5 minutes +const DEFAULT_LOCK_EXPIRY: u64 = 60 * 60; // 1 hour pub(crate) struct TaskManagerIpc { txs: [mpsc::Sender; TaskType::COUNT], @@ -37,16 +37,20 @@ pub(crate) struct TaskManagerIpc { revision: u64, } +#[derive(Debug)] pub(crate) struct Locked { expires: Instant, + due: u64, revision: u64, } +#[derive(Debug)] pub(crate) struct TaskDetails { task: Task, info: TaskJob, } +#[derive(Debug)] pub(crate) struct TaskJob { id: u64, due: u64, diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index 2cab02c2..411aeecf 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -8,7 +8,7 @@ use super::NextHop; use crate::queue::{Error, ErrorDetails, HostResponse, Status}; use common::{ Server, - config::smtp::queue::{ConnectionStrategy, IpAndHost, MxConfig}, + config::smtp::queue::{ConnectionStrategy, HostOrIp, IpAndHost, MxConfig}, expr::functions::ResolveVariable, }; use mail_auth::{IpLookupStrategy, MX}; @@ -111,41 +111,44 @@ impl DnsLookup for Server { remote_host: &NextHop<'_>, envelope: &impl ResolveVariable, ) -> Result>, ErrorDetails>> { - let mut remote_ips = self - .ip_lookup( - remote_host.fqdn_hostname().as_ref(), - remote_host.ip_lookup_strategy(), - remote_host.max_multi_homed(), - ) - .await - .map_err(|err| { - if let mail_auth::Error::DnsRecordNotFound(_) = &err { - if matches!( - remote_host, - NextHop::MX { - is_implicit: true, - .. + let mut remote_ips = match remote_host.fqdn_hostname() { + HostOrIp::Host(hostname) => self + .ip_lookup( + hostname.as_ref(), + remote_host.ip_lookup_strategy(), + remote_host.max_multi_homed(), + ) + .await + .map_err(|err| { + if let mail_auth::Error::DnsRecordNotFound(_) = &err { + if matches!( + remote_host, + NextHop::MX { + is_implicit: true, + .. + } + ) { + Status::PermanentFailure(ErrorDetails { + entity: remote_host.hostname().into(), + details: Error::DnsError("no MX record found.".into()), + }) + } else { + Status::PermanentFailure(ErrorDetails { + entity: remote_host.hostname().into(), + details: Error::ConnectionError("record not found for MX".into()), + }) } - ) { - Status::PermanentFailure(ErrorDetails { - entity: remote_host.hostname().into(), - details: Error::DnsError("no MX record found.".into()), - }) } else { - Status::PermanentFailure(ErrorDetails { + Status::TemporaryFailure(ErrorDetails { entity: remote_host.hostname().into(), - details: Error::ConnectionError("record not found for MX".into()), + details: Error::ConnectionError( + format!("lookup error: {err}").into_boxed_str(), + ), }) } - } else { - Status::TemporaryFailure(ErrorDetails { - entity: remote_host.hostname().into(), - details: Error::ConnectionError( - format!("lookup error: {err}").into_boxed_str(), - ), - }) - } - })?; + })?, + HostOrIp::Ip { ip, .. } => vec![ip], + }; if !remote_ips.is_empty() { #[cfg(not(feature = "test_mode"))] diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs index 978c2f4b..1bd98ad4 100644 --- a/crates/smtp/src/outbound/mod.rs +++ b/crates/smtp/src/outbound/mod.rs @@ -10,7 +10,7 @@ use crate::{ }; use common::config::{ server::ServerProtocol, - smtp::queue::{MxConfig, RelayConfig}, + smtp::queue::{HostOrIp, MxConfig, RelayConfig}, }; use mail_auth::IpLookupStrategy; use mail_send::Credentials; @@ -246,21 +246,30 @@ impl NextHop<'_> { host } } - NextHop::Relay(host) => host.address.as_str(), + NextHop::Relay(host) => match &host.address { + HostOrIp::Host(host) => host.as_str(), + HostOrIp::Ip { ip_str, .. } => ip_str.as_str(), + }, } } #[inline(always)] - pub fn fqdn_hostname(&self) -> Cow<'_, str> { + pub fn fqdn_hostname(&self) -> HostOrIp> { match self { NextHop::MX { host, .. } => { if !host.ends_with('.') { - format!("{host}.").into() + HostOrIp::Host(format!("{host}.").into()) } else { - (*host).into() + HostOrIp::Host((*host).into()) } } - NextHop::Relay(host) => host.address.as_str().into(), + NextHop::Relay(host) => match &host.address { + HostOrIp::Host(host) => HostOrIp::Host(host.as_str().into()), + HostOrIp::Ip { ip, ip_str } => HostOrIp::Ip { + ip: *ip, + ip_str: ip_str.as_str().into(), + }, + }, } } diff --git a/crates/store/src/registry/mod.rs b/crates/store/src/registry/mod.rs index b48c8a25..e17126c6 100644 --- a/crates/store/src/registry/mod.rs +++ b/crates/store/src/registry/mod.rs @@ -19,7 +19,8 @@ use registry::{ schema::{ prelude::{Object, ObjectInner, ObjectType, Property}, structs::{ - ArchivedItem, DmarcInternalReport, SpamTrainingSample, Task, TlsInternalReport, Trace, + ArchivedItem, DmarcInternalReport, Metric, SpamTrainingSample, Task, TlsInternalReport, + Trace, }, }, types::{EnumImpl, ObjectImpl, id::ObjectId}, @@ -32,6 +33,7 @@ pub struct RegistryObject { pub revision: u64, } +#[derive(Debug)] pub struct RegistryQuery { pub(crate) object_type: ObjectType, pub filters: Vec, @@ -49,6 +51,7 @@ pub(crate) enum RegistryQueryStart { None, } +#[derive(Debug)] pub struct RegistryFilter { pub property: Property, pub op: RegistryFilterOp, @@ -72,6 +75,7 @@ pub enum RegistryFilterOp { TextMatch, } +#[derive(Debug)] pub enum RegistryFilterValue { String(String), Bytes(Vec), @@ -213,3 +217,15 @@ impl Deserialize for Trace { }) } } + +impl Deserialize for Metric { + fn deserialize(bytes: &[u8]) -> trc::Result { + let mut stream = PickledStream::new(bytes); + Metric::unpickle(&mut stream).ok_or_else(|| { + trc::EventType::Registry(trc::RegistryEvent::DeserializationError) + .into_err() + .caused_by(trc::location!()) + .ctx(trc::Key::Value, bytes) + }) + } +} diff --git a/crates/trc/event-macro/src/lib.rs b/crates/trc/event-macro/src/lib.rs index 2b12ba94..2f032d7c 100644 --- a/crates/trc/event-macro/src/lib.rs +++ b/crates/trc/event-macro/src/lib.rs @@ -56,7 +56,7 @@ pub fn key_names(_attr: TokenStream, item: TokenStream) -> TokenStream { let parse_fn = quote! { pub fn try_parse(name: &str) -> Option { match name { - #(#snake_case_names => Some(Self::#variant_names),)* + #(#camel_case_names => Some(Self::#variant_names),)* _ => None, } } diff --git a/crates/trc/src/event/enums.rs b/crates/trc/src/event/enums.rs index 104cdc48..350b23be 100644 --- a/crates/trc/src/event/enums.rs +++ b/crates/trc/src/event/enums.rs @@ -7,7 +7,7 @@ // This file is auto-generated. Do not edit directly. pub const TOTAL_EVENT_COUNT: usize = 596; -pub const TOTAL_METRIC_COUNT: usize = 338; +pub const TOTAL_METRIC_COUNT: usize = 339; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EventType { @@ -439,7 +439,7 @@ pub enum MessageIngestEvent { JmapAppend = 285, Duplicate = 281, Error = 282, - FtsIndex = 142, + SearchIndex = 142, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -1042,7 +1042,7 @@ pub enum MetricType { MessageIngestJmapAppend = 176, MessageIngestDuplicate = 177, MessageIngestError = 178, - MessageIngestFtsIndex = 179, + MessageIngestSearchIndex = 179, MilterActionAccept = 180, MilterActionDiscard = 181, MilterActionReject = 182, @@ -1205,6 +1205,7 @@ pub enum MetricType { StoreBlobDelete = 327, TaskManagerBlobNotFound = 328, TaskManagerMetadataNotFound = 329, + TelemetryAlertEvent = 338, TelemetryAlertMessage = 212, TelemetryLogError = 330, TelemetryWebhookError = 331, diff --git a/crates/trc/src/event/enums_impl.rs b/crates/trc/src/event/enums_impl.rs index 6ee7525a..ca68ed0b 100644 --- a/crates/trc/src/event/enums_impl.rs +++ b/crates/trc/src/event/enums_impl.rs @@ -287,7 +287,7 @@ impl EventType { b"message-ingest.jmap-append" => EventType::MessageIngest(MessageIngestEvent::JmapAppend), b"message-ingest.duplicate" => EventType::MessageIngest(MessageIngestEvent::Duplicate), b"message-ingest.error" => EventType::MessageIngest(MessageIngestEvent::Error), - b"message-ingest.fts-index" => EventType::MessageIngest(MessageIngestEvent::FtsIndex), + b"message-ingest.search-index" => EventType::MessageIngest(MessageIngestEvent::SearchIndex), b"milter.read" => EventType::Milter(MilterEvent::Read), b"milter.write" => EventType::Milter(MilterEvent::Write), b"milter.action-accept" => EventType::Milter(MilterEvent::ActionAccept), @@ -955,7 +955,9 @@ impl EventType { } EventType::MessageIngest(MessageIngestEvent::Duplicate) => "message-ingest.duplicate", EventType::MessageIngest(MessageIngestEvent::Error) => "message-ingest.error", - EventType::MessageIngest(MessageIngestEvent::FtsIndex) => "message-ingest.fts-index", + EventType::MessageIngest(MessageIngestEvent::SearchIndex) => { + "message-ingest.search-index" + } EventType::Milter(MilterEvent::Read) => "milter.read", EventType::Milter(MilterEvent::Write) => "milter.write", EventType::Milter(MilterEvent::ActionAccept) => "milter.action-accept", @@ -1616,7 +1618,7 @@ impl EventType { EventType::MessageIngest(MessageIngestEvent::JmapAppend) => 285, EventType::MessageIngest(MessageIngestEvent::Duplicate) => 281, EventType::MessageIngest(MessageIngestEvent::Error) => 282, - EventType::MessageIngest(MessageIngestEvent::FtsIndex) => 142, + EventType::MessageIngest(MessageIngestEvent::SearchIndex) => 142, EventType::Milter(MilterEvent::Read) => 299, EventType::Milter(MilterEvent::Write) => 303, EventType::Milter(MilterEvent::ActionAccept) => 287, @@ -2235,7 +2237,7 @@ impl EventType { 285 => Some(EventType::MessageIngest(MessageIngestEvent::JmapAppend)), 281 => Some(EventType::MessageIngest(MessageIngestEvent::Duplicate)), 282 => Some(EventType::MessageIngest(MessageIngestEvent::Error)), - 142 => Some(EventType::MessageIngest(MessageIngestEvent::FtsIndex)), + 142 => Some(EventType::MessageIngest(MessageIngestEvent::SearchIndex)), 299 => Some(EventType::Milter(MilterEvent::Read)), 303 => Some(EventType::Milter(MilterEvent::Write)), 287 => Some(EventType::Milter(MilterEvent::ActionAccept)), @@ -2713,7 +2715,7 @@ impl EventType { EventType::MessageIngest(MessageIngestEvent::ImapAppend) => Level::Info, EventType::MessageIngest(MessageIngestEvent::JmapAppend) => Level::Info, EventType::MessageIngest(MessageIngestEvent::Duplicate) => Level::Info, - EventType::MessageIngest(MessageIngestEvent::FtsIndex) => Level::Info, + EventType::MessageIngest(MessageIngestEvent::SearchIndex) => Level::Info, EventType::Milter(MilterEvent::ActionAccept) => Level::Info, EventType::Milter(MilterEvent::ActionDiscard) => Level::Info, EventType::Milter(MilterEvent::ActionReject) => Level::Info, @@ -3273,9 +3275,7 @@ impl EventType { EventType::MessageIngest(MessageIngestEvent::JmapAppend) => "Message appended via JMAP", EventType::MessageIngest(MessageIngestEvent::Duplicate) => "Skipping duplicate message", EventType::MessageIngest(MessageIngestEvent::Error) => "Message ingestion error", - EventType::MessageIngest(MessageIngestEvent::FtsIndex) => { - "Full-text search index updated" - } + EventType::MessageIngest(MessageIngestEvent::SearchIndex) => "Search index updated", EventType::Milter(MilterEvent::Read) => "Reading from Milter", EventType::Milter(MilterEvent::Write) => "Writing to Milter", EventType::Milter(MilterEvent::ActionAccept) => "Milter action: Accept", @@ -3629,9 +3629,9 @@ impl EventType { "Prometheus exporter error" } EventType::Telemetry(TelemetryEvent::JournalError) => "Journal collector error", - EventType::Telemetry(TelemetryEvent::MetricsCollected) => "Housekeeper process stopped", - EventType::Telemetry(TelemetryEvent::MetricsStored) => "Purge finished", - EventType::Telemetry(TelemetryEvent::MetricsPushed) => "Housekeeper task run", + EventType::Telemetry(TelemetryEvent::MetricsCollected) => "Metrics collected", + EventType::Telemetry(TelemetryEvent::MetricsStored) => "Metric store", + EventType::Telemetry(TelemetryEvent::MetricsPushed) => "Metrics pushed", EventType::Tls(TlsEvent::Handshake) => "TLS handshake", EventType::Tls(TlsEvent::HandshakeError) => "TLS handshake error", EventType::Tls(TlsEvent::NotConfigured) => "TLS not configured", @@ -4142,8 +4142,8 @@ impl EventType { EventType::MessageIngest(MessageIngestEvent::Error) => { "An error occurred while ingesting the message" } - EventType::MessageIngest(MessageIngestEvent::FtsIndex) => { - "The full-text search index has been updated" + EventType::MessageIngest(MessageIngestEvent::SearchIndex) => { + "The search index has been updated" } EventType::Milter(MilterEvent::Read) => "Reading from the Milter", EventType::Milter(MilterEvent::Write) => "Writing to the Milter", @@ -4726,11 +4726,11 @@ impl EventType { EventType::Telemetry(TelemetryEvent::JournalError) => { "An error occurred with the journal collector" } - EventType::Telemetry(TelemetryEvent::MetricsCollected) => { - "The housekeeper process has stopped" + EventType::Telemetry(TelemetryEvent::MetricsCollected) => "Metrics have been collected", + EventType::Telemetry(TelemetryEvent::MetricsStored) => "Metrics have been stored", + EventType::Telemetry(TelemetryEvent::MetricsPushed) => { + "Metrics have been pushed to the configured exporters" } - EventType::Telemetry(TelemetryEvent::MetricsStored) => "The purge has finished", - EventType::Telemetry(TelemetryEvent::MetricsPushed) => "A housekeeper task is running", EventType::Tls(TlsEvent::Handshake) => "Successful TLS handshake", EventType::Tls(TlsEvent::HandshakeError) => { "An error occurred during the TLS handshake" @@ -5326,7 +5326,7 @@ impl EventType { EventType::MessageIngest(MessageIngestEvent::JmapAppend), EventType::MessageIngest(MessageIngestEvent::Duplicate), EventType::MessageIngest(MessageIngestEvent::Error), - EventType::MessageIngest(MessageIngestEvent::FtsIndex), + EventType::MessageIngest(MessageIngestEvent::SearchIndex), EventType::Milter(MilterEvent::Read), EventType::Milter(MilterEvent::Write), EventType::Milter(MilterEvent::ActionAccept), @@ -5823,7 +5823,7 @@ impl MetricType { b"message-ingest.jmap-append" => MetricType::MessageIngestJmapAppend, b"message-ingest.duplicate" => MetricType::MessageIngestDuplicate, b"message-ingest.error" => MetricType::MessageIngestError, - b"message-ingest.fts-index" => MetricType::MessageIngestFtsIndex, + b"message-ingest.search-index" => MetricType::MessageIngestSearchIndex, b"milter.action-accept" => MetricType::MilterActionAccept, b"milter.action-discard" => MetricType::MilterActionDiscard, b"milter.action-reject" => MetricType::MilterActionReject, @@ -5986,6 +5986,7 @@ impl MetricType { b"store.blob-delete" => MetricType::StoreBlobDelete, b"task-manager.blob-not-found" => MetricType::TaskManagerBlobNotFound, b"task-manager.metadata-not-found" => MetricType::TaskManagerMetadataNotFound, + b"telemetry.alert-event" => MetricType::TelemetryAlertEvent, b"telemetry.alert-message" => MetricType::TelemetryAlertMessage, b"telemetry.log-error" => MetricType::TelemetryLogError, b"telemetry.webhook-error" => MetricType::TelemetryWebhookError, @@ -6171,7 +6172,7 @@ impl MetricType { MetricType::MessageIngestJmapAppend => "message-ingest.jmap-append", MetricType::MessageIngestDuplicate => "message-ingest.duplicate", MetricType::MessageIngestError => "message-ingest.error", - MetricType::MessageIngestFtsIndex => "message-ingest.fts-index", + MetricType::MessageIngestSearchIndex => "message-ingest.search-index", MetricType::MilterActionAccept => "milter.action-accept", MetricType::MilterActionDiscard => "milter.action-discard", MetricType::MilterActionReject => "milter.action-reject", @@ -6340,6 +6341,7 @@ impl MetricType { MetricType::StoreBlobDelete => "store.blob-delete", MetricType::TaskManagerBlobNotFound => "task-manager.blob-not-found", MetricType::TaskManagerMetadataNotFound => "task-manager.metadata-not-found", + MetricType::TelemetryAlertEvent => "telemetry.alert-event", MetricType::TelemetryAlertMessage => "telemetry.alert-message", MetricType::TelemetryLogError => "telemetry.log-error", MetricType::TelemetryWebhookError => "telemetry.webhook-error", @@ -6522,7 +6524,7 @@ impl MetricType { MetricType::MessageIngestJmapAppend => 176, MetricType::MessageIngestDuplicate => 177, MetricType::MessageIngestError => 178, - MetricType::MessageIngestFtsIndex => 179, + MetricType::MessageIngestSearchIndex => 179, MetricType::MilterActionAccept => 180, MetricType::MilterActionDiscard => 181, MetricType::MilterActionReject => 182, @@ -6685,6 +6687,7 @@ impl MetricType { MetricType::StoreBlobDelete => 327, MetricType::TaskManagerBlobNotFound => 328, MetricType::TaskManagerMetadataNotFound => 329, + MetricType::TelemetryAlertEvent => 338, MetricType::TelemetryAlertMessage => 212, MetricType::TelemetryLogError => 330, MetricType::TelemetryWebhookError => 331, @@ -6865,7 +6868,7 @@ impl MetricType { 176 => Some(MetricType::MessageIngestJmapAppend), 177 => Some(MetricType::MessageIngestDuplicate), 178 => Some(MetricType::MessageIngestError), - 179 => Some(MetricType::MessageIngestFtsIndex), + 179 => Some(MetricType::MessageIngestSearchIndex), 180 => Some(MetricType::MilterActionAccept), 181 => Some(MetricType::MilterActionDiscard), 182 => Some(MetricType::MilterActionReject), @@ -7028,6 +7031,7 @@ impl MetricType { 327 => Some(MetricType::StoreBlobDelete), 328 => Some(MetricType::TaskManagerBlobNotFound), 329 => Some(MetricType::TaskManagerMetadataNotFound), + 338 => Some(MetricType::TelemetryAlertEvent), 212 => Some(MetricType::TelemetryAlertMessage), 330 => Some(MetricType::TelemetryLogError), 331 => Some(MetricType::TelemetryWebhookError), @@ -7196,7 +7200,7 @@ impl MetricType { MetricType::MessageIngestJmapAppend => 285, MetricType::MessageIngestDuplicate => 281, MetricType::MessageIngestError => 282, - MetricType::MessageIngestFtsIndex => 142, + MetricType::MessageIngestSearchIndex => 142, MetricType::MilterActionAccept => 287, MetricType::MilterActionDiscard => 289, MetricType::MilterActionReject => 290, @@ -7346,6 +7350,7 @@ impl MetricType { MetricType::StoreBlobDelete => 506, MetricType::TaskManagerBlobNotFound => 141, MetricType::TaskManagerMetadataNotFound => 145, + MetricType::TelemetryAlertEvent => 548, MetricType::TelemetryAlertMessage => 365, MetricType::TelemetryLogError => 535, MetricType::TelemetryWebhookError => 539, @@ -7534,7 +7539,7 @@ impl MetricType { MetricType::MessageIngestJmapAppend => "Message appended via JMAP", MetricType::MessageIngestDuplicate => "Skipping duplicate message", MetricType::MessageIngestError => "Message ingestion error", - MetricType::MessageIngestFtsIndex => "Full-text search index updated", + MetricType::MessageIngestSearchIndex => "Search index updated", MetricType::MilterActionAccept => "Milter action: Accept", MetricType::MilterActionDiscard => "Milter action: Discard", MetricType::MilterActionReject => "Milter action: Reject", @@ -7701,6 +7706,7 @@ impl MetricType { MetricType::StoreBlobDelete => "Blob delete operation", MetricType::TaskManagerBlobNotFound => "Blob not found for task", MetricType::TaskManagerMetadataNotFound => "Metadata not found for task", + MetricType::TelemetryAlertEvent => "Alert event triggered", MetricType::TelemetryAlertMessage => "Alert message sent", MetricType::TelemetryLogError => "Log collector error", MetricType::TelemetryWebhookError => "Webhook collector error", @@ -7878,7 +7884,7 @@ impl MetricType { | MetricType::MessageIngestJmapAppend | MetricType::MessageIngestDuplicate | MetricType::MessageIngestError - | MetricType::MessageIngestFtsIndex + | MetricType::MessageIngestSearchIndex | MetricType::MilterActionAccept | MetricType::MilterActionDiscard | MetricType::MilterActionReject @@ -8028,6 +8034,7 @@ impl MetricType { | MetricType::StoreBlobDelete | MetricType::TaskManagerBlobNotFound | MetricType::TaskManagerMetadataNotFound + | MetricType::TelemetryAlertEvent | MetricType::TelemetryAlertMessage | MetricType::TelemetryLogError | MetricType::TelemetryWebhookError @@ -8224,7 +8231,7 @@ impl MetricType { MetricType::MessageIngestJmapAppend, MetricType::MessageIngestDuplicate, MetricType::MessageIngestError, - MetricType::MessageIngestFtsIndex, + MetricType::MessageIngestSearchIndex, MetricType::MilterActionAccept, MetricType::MilterActionDiscard, MetricType::MilterActionReject, @@ -8387,6 +8394,7 @@ impl MetricType { MetricType::StoreBlobDelete, MetricType::TaskManagerBlobNotFound, MetricType::TaskManagerMetadataNotFound, + MetricType::TelemetryAlertEvent, MetricType::TelemetryAlertMessage, MetricType::TelemetryLogError, MetricType::TelemetryWebhookError, diff --git a/crates/trc/src/ipc/metrics.rs b/crates/trc/src/ipc/metrics.rs index a0057f42..c16c5090 100644 --- a/crates/trc/src/ipc/metrics.rs +++ b/crates/trc/src/ipc/metrics.rs @@ -178,7 +178,7 @@ impl Collector { EventType::Queue(QueueEvent::AutogeneratedQueued | QueueEvent::DsnQueued) => { QUEUE_COUNT.increment(); } - EventType::MessageIngest(MessageIngestEvent::FtsIndex) => { + EventType::MessageIngest(MessageIngestEvent::SearchIndex) => { MESSAGE_INDEX_TIME.observe(elapsed); } EventType::Store(StoreEvent::BlobWrite) => { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index eb90db60..37e6159a 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -45,7 +45,7 @@ trc = { path = "../crates/trc", features = ["enterprise"] } managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] } smtp-proto = { version = "0.2" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } -mail-auth = { version = "0.7.1", features = ["test"] } +mail-auth = { path = "/Users/me/code/mail-auth", features = ["test"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = "0.4.4" sieve-rs = { version = "0.7", features = ["rkyv"] } diff --git a/tests/src/jmap/calendar/acl.rs b/tests/src/jmap/calendar/acl.rs index 7b958800..13af8792 100644 --- a/tests/src/jmap/calendar/acl.rs +++ b/tests/src/jmap/calendar/acl.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, JmapUtils}; +use crate::utils::{jmap::JmapUtils, server::TestServer}; use calcard::jscalendar::JSCalendarProperty; use jmap_proto::{ object::{calendar::CalendarProperty, share_notification::ShareNotificationProperty}, @@ -13,7 +13,7 @@ use jmap_proto::{ use serde_json::json; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Calendar ACL tests..."); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); @@ -707,5 +707,5 @@ pub async fn test(test: &mut TestServer) { // Destroy all mailboxes john.destroy_all_calendars().await; jane.destroy_all_calendars().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/calendar/alarm.rs b/tests/src/jmap/calendar/alarm.rs index 90250592..10050b01 100644 --- a/tests/src/jmap/calendar/alarm.rs +++ b/tests/src/jmap/calendar/alarm.rs @@ -4,6 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::utils::{ + jmap::{IntoJmapSet, JmapUtils}, + server::TestServer, +}; use futures::StreamExt; use jmap_client::{ CalendarAlert, PushObject, client_ws::WebSocketMessage, event_source::PushNotification, @@ -15,14 +19,12 @@ use std::time::Instant; use store::write::now; use tokio::sync::mpsc; -use crate::jmap::{IntoJmapSet, JMAPTest, JmapUtils}; - -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Calendar Alarm tests..."); let account = test.account("jdoe@example.com"); let account_id = account.id_string(); let client = account.jmap_client().await; - let client_ws = account.client_owned().await; + let client_ws = account.jmap_client().await; // Create test calendar let response = account @@ -169,5 +171,5 @@ pub async fn test(test: &mut TestServer) { // Cleanup account.destroy_all_calendars().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/calendar/calendars.rs b/tests/src/jmap/calendar/calendars.rs index a1d232fb..b876b784 100644 --- a/tests/src/jmap/calendar/calendars.rs +++ b/tests/src/jmap/calendar/calendars.rs @@ -4,11 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{ChangeType, JMAPTest, JmapUtils}; +use crate::utils::{ + jmap::{ChangeType, JmapUtils}, + server::TestServer, +}; use jmap_proto::{object::calendar::CalendarProperty, request::method::MethodObject}; use serde_json::json; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Calendar tests..."); let account = test.account("jdoe@example.com"); @@ -370,5 +373,5 @@ pub async fn test(test: &mut TestServer) { // Destroy all mailboxes account.destroy_all_calendars().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/calendar/event.rs b/tests/src/jmap/calendar/event.rs index 390e6a52..95b64be9 100644 --- a/tests/src/jmap/calendar/event.rs +++ b/tests/src/jmap/calendar/event.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils, wait_for_tasks}, - webdav::DummyWebDavClient, +use crate::utils::{ + jmap::{ChangeType, IntoJmapSet, JmapUtils}, + server::TestServer, }; use ahash::AHashSet; use calcard::jscalendar::JSCalendarProperty; @@ -16,7 +16,7 @@ use jmap_proto::request::method::MethodObject; use serde_json::{Value, json}; use types::{collection::SyncCollection, id::Id}; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Calendar Event tests..."); let account = test.account("jdoe@example.com"); @@ -101,6 +101,7 @@ pub async fn test(test: &mut TestServer) { let event_4_id = response.created(3).id().to_string(); // Destroy tmp event + test.wait_for_tasks().await; assert_eq!( account .jmap_destroy( @@ -657,6 +658,7 @@ END:VCALENDAR ])); // Deletion tests + test.wait_for_tasks().await; assert_eq!( account .jmap_destroy( @@ -674,19 +676,10 @@ END:VCALENDAR // CardDAV compatibility tests let account_id = account.id().document_id(); - let dav_client = DummyWebDavClient::new( - u32::MAX, - account.name(), - account.secret(), - account.emails()[0], - ); - let resources = params + let dav_client = account.webdav_client(); + let resources = test .server - .fetch_dav_resources( - ¶ms.server.get_access_token(account_id).await.unwrap(), - account_id, - SyncCollection::Calendar, - ) + .fetch_dav_resources(account_id, account_id, SyncCollection::Calendar) .await .unwrap(); let path = format!( @@ -715,8 +708,9 @@ END:VCALENDAR assert_eq!(ical, expected_ical); // Clean up + test.wait_for_tasks().await; account.destroy_all_calendars().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } pub fn test_jscalendar_1() -> Value { diff --git a/tests/src/jmap/calendar/identity.rs b/tests/src/jmap/calendar/identity.rs index a062d65b..f3ad6b3a 100644 --- a/tests/src/jmap/calendar/identity.rs +++ b/tests/src/jmap/calendar/identity.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, JmapUtils}; +use crate::utils::{jmap::JmapUtils, server::TestServer}; use jmap_proto::{ object::participant_identity::ParticipantIdentityProperty, request::method::MethodObject, }; @@ -12,7 +12,7 @@ use serde_json::json; use store::write::BatchBuilder; use types::{collection::Collection, field::PrincipalField}; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Participant Identity tests..."); let account = test.account("jdoe@example.com"); @@ -146,6 +146,6 @@ pub async fn test(test: &mut TestServer) { .with_collection(Collection::Principal) .with_document(0) .clear(PrincipalField::ParticipantIdentities); - params.server.commit_batch(batch).await.unwrap(); - test.assert_is_empty().await;; + test.server.commit_batch(batch).await.unwrap(); + test.assert_is_empty().await; } diff --git a/tests/src/jmap/calendar/notification.rs b/tests/src/jmap/calendar/notification.rs index 0baf0c55..2921a7f1 100644 --- a/tests/src/jmap/calendar/notification.rs +++ b/tests/src/jmap/calendar/notification.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{IntoJmapSet, JMAPTest, JmapUtils, wait_for_tasks}; +use crate::utils::{ + jmap::{IntoJmapSet, JmapUtils}, + server::TestServer, +}; use calcard::jscalendar::JSCalendarProperty; use jmap_proto::{ object::calendar_event_notification::CalendarEventNotificationProperty, @@ -15,7 +18,7 @@ use serde_json::{Value, json}; use store::write::now; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Calendar Event Notification tests..."); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); @@ -388,7 +391,7 @@ pub async fn test(test: &mut TestServer) { client.destroy_all_event_notifications().await; test.destroy_all_mailboxes(client).await; } - test.assert_is_empty().await;; + test.assert_is_empty().await; } fn test_event() -> Value { diff --git a/tests/src/jmap/contacts/acl.rs b/tests/src/jmap/contacts/acl.rs index fedb90ab..bd7f7a4b 100644 --- a/tests/src/jmap/contacts/acl.rs +++ b/tests/src/jmap/contacts/acl.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, JmapUtils}; +use crate::utils::{jmap::JmapUtils, server::TestServer}; use calcard::jscontact::JSContactProperty; use jmap_proto::{ object::{addressbook::AddressBookProperty, share_notification::ShareNotificationProperty}, @@ -13,7 +13,7 @@ use jmap_proto::{ use serde_json::json; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Contacts ACL tests..."); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); @@ -670,5 +670,5 @@ pub async fn test(test: &mut TestServer) { // Destroy all mailboxes john.destroy_all_addressbooks().await; jane.destroy_all_addressbooks().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/contacts/addressbook.rs b/tests/src/jmap/contacts/addressbook.rs index aca115b6..33cf406b 100644 --- a/tests/src/jmap/contacts/addressbook.rs +++ b/tests/src/jmap/contacts/addressbook.rs @@ -4,12 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::utils::{ + jmap::{ChangeType, JmapUtils}, + server::TestServer, +}; use jmap_proto::{object::addressbook::AddressBookProperty, request::method::MethodObject}; use serde_json::json; -use crate::jmap::{ChangeType, JMAPTest, JmapUtils}; - -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running AddressBook tests..."); let account = test.account("jdoe@example.com"); @@ -210,5 +212,5 @@ pub async fn test(test: &mut TestServer) { // Destroy all mailboxes account.destroy_all_addressbooks().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/contacts/contact.rs b/tests/src/jmap/contacts/contact.rs index 22ac4abf..8db607b1 100644 --- a/tests/src/jmap/contacts/contact.rs +++ b/tests/src/jmap/contacts/contact.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - jmap::{ChangeType, IntoJmapSet, JMAPTest, JmapUtils, wait_for_tasks}, - webdav::DummyWebDavClient, +use crate::utils::{ + jmap::{ChangeType, IntoJmapSet, JmapUtils}, + server::TestServer, }; use ahash::AHashSet; use calcard::jscontact::JSContactProperty; @@ -16,7 +16,7 @@ use jmap_proto::request::method::MethodObject; use serde_json::{Value, json}; use types::{collection::SyncCollection, id::Id}; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Contact Card tests..."); let account = test.account("jdoe@example.com"); @@ -337,7 +337,7 @@ pub async fn test(test: &mut TestServer) { // Query tests test.wait_for_tasks().await; - let email = if !params.server.search_store().is_mysql() { + let email = if !test.server.search_store().is_mysql() { "sarah.johnson@example.com" } else { "sarah.johnson@example" @@ -455,19 +455,10 @@ END:VCARD"# // CardDAV compatibility tests let account_id = account.id().document_id(); - let dav_client = DummyWebDavClient::new( - u32::MAX, - account.name(), - account.secret(), - account.emails()[0], - ); - let resources = params + let dav_client = account.webdav_client(); + let resources = test .server - .fetch_dav_resources( - ¶ms.server.get_access_token(account_id).await.unwrap(), - account_id, - SyncCollection::AddressBook, - ) + .fetch_dav_resources(account_id, account_id, SyncCollection::AddressBook) .await .unwrap(); let path = format!( @@ -495,6 +486,7 @@ END:VCARD"# assert_eq!(vcard, expected_vcard); // Clean up + test.wait_for_tasks().await; account.destroy_all_addressbooks().await; test.assert_is_empty().await; } diff --git a/tests/src/jmap/core/blob.rs b/tests/src/jmap/core/blob.rs index 4be2f69b..cca4d788 100644 --- a/tests/src/jmap/core/blob.rs +++ b/tests/src/jmap/core/blob.rs @@ -4,16 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{jmap::JMAPTest, store::cleanup::store_blob_expire_all}; +use crate::utils::server::TestServer; use email::mailbox::INBOX_ID; use serde_json::{Value, json}; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running blob tests..."); - let server = params.server.clone(); let account = test.account("jdoe@example.com"); - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; // Blob/set simple test let response = account.jmap_method_call("Blob/upload", json!({ @@ -139,7 +138,7 @@ pub async fn test(test: &mut TestServer) { ); } - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; // Blob/upload Complex Example let response = account @@ -226,7 +225,7 @@ pub async fn test(test: &mut TestServer) { "Pointer {pointer:?} Response: {response:?}", ); } - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; // Blob/get Example with Range and Encoding Errors let response = account.jmap_method_calls(json!([ @@ -353,7 +352,7 @@ pub async fn test(test: &mut TestServer) { "Pointer {pointer:?} Response: {response:?}", ); } - store_blob_expire_all(&server.core.storage.data).await; + test.blob_expire_all().await; // Blob/lookup let client = account.jmap_client().await; diff --git a/tests/src/jmap/core/event_source.rs b/tests/src/jmap/core/event_source.rs index 8ae8ac03..6b02bd3b 100644 --- a/tests/src/jmap/core/event_source.rs +++ b/tests/src/jmap/core/event_source.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, mail::delivery::SmtpConnection}; use email::mailbox::INBOX_ID; use futures::StreamExt; use jmap_client::{ @@ -17,7 +16,9 @@ use store::ahash::AHashSet; use tokio::sync::mpsc; use types::id::Id; -pub async fn test(test: &mut TestServer) { +use crate::utils::{server::TestServer, smtp::SmtpConnection}; + +pub async fn test(test: &TestServer) { println!("Running EventSource tests..."); // Create test account diff --git a/tests/src/jmap/core/push_subscription.rs b/tests/src/jmap/core/push_subscription.rs index ddd03326..37318435 100644 --- a/tests/src/jmap/core/push_subscription.rs +++ b/tests/src/jmap/core/push_subscription.rs @@ -4,49 +4,41 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{AssertConfig, add_test_certs, jmap::JMAPTest}; +use crate::{AssertConfig, utils::server::TestServer}; use base64::{Engine, engine::general_purpose}; -use common::{Caches, Core, Data, Inner, config::server::Listeners, network::SessionData}; +use common::{config::server::Listeners, network::SessionData}; use ece::EcKeyComponents; use http_proto::{HtmlResponse, ToHttpResponse, request::fetch_body}; use hyper::{StatusCode, body, header::CONTENT_ENCODING, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use jmap_client::{mailbox::Role, push_subscription::Keys}; use jmap_proto::{response::status::PushObject, types::state::State}; +use registry::{ + schema::{ + enums::NetworkListenerProtocol, + prelude::{ObjectType, SocketAddr}, + structs::{NetworkListener, SystemSettings}, + }, + types::{id::ObjectId, map::Map}, +}; use services::state_manager::ece::ece_encrypt; use std::{ + str::FromStr, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, time::Duration, }; -use store::ahash::AHashSet; +use store::{ + ahash::AHashSet, + registry::{RegistryObject, bootstrap::Bootstrap}, +}; use tokio::sync::mpsc; use types::{id::Id, type_state::DataType}; +use utils::map::vec_map::VecMap; -const SERVER: &str = r#" -[server] -hostname = "'jmap-push.example.org'" - -[http] -url = "'https://127.0.0.1:9000'" - -[server.listener.jmap] -bind = ['127.0.0.1:9000'] -protocol = 'http' -tls.implicit = true - -[server.socket] -reuse-addr = true - -[certificate.default] -cert = '%{file:{CERT}}%' -private-key = '%{file:{PK}}%' -default = true -"#; - -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Push Subscription tests..."); // ECE roundtrip test @@ -72,28 +64,35 @@ pub async fn test(test: &mut TestServer) { }); // Start mock push server - let mut settings = Config::new(add_test_certs(SERVER)).unwrap(); - settings.resolve_all_macros().await; - let mock_inner = Arc::new(Inner { - shared_core: Core::parse(&mut settings, Default::default(), Default::default()) - .await - .into_shared(), - data: Data::parse(&mut settings), - cache: Caches::parse(&mut settings), - ..Default::default() - }); - settings.errors.clear(); - settings.warnings.clear(); - let mut servers = Listeners::parse(&mut settings); - servers.parse_tcp_acceptors(&mut settings, mock_inner.clone()); - - // Start JMAP server - servers.bind_and_drop_priv(&mut settings); - settings.assert_no_errors(); + let mut bp = Bootstrap::new_uninitialized(test.server.registry().clone()); + let mut servers = Listeners::default(); + servers.parse_server( + &mut bp, + RegistryObject { + id: ObjectId::new(ObjectType::NetworkListener, 0u64.into()), + object: NetworkListener { + name: "mock-push".into(), + bind: Map::new(vec![SocketAddr::from_str("127.0.0.1:19000").unwrap()]), + protocol: NetworkListenerProtocol::Http, + tls_implicit: true, + use_tls: true, + socket_reuse_address: true, + socket_reuse_port: true, + ..Default::default() + }, + revision: 0, + }, + &SystemSettings::default(), + ); + servers + .parse_tcp_acceptors(&mut bp, test.server.inner.clone()) + .await; + servers.bind_and_drop_priv(&mut bp); + bp.assert_no_errors(); let _shutdown_tx = servers.spawn(|server, acceptor, shutdown_rx| { server.spawn( SessionManager::from(push_server.clone()), - mock_inner.clone(), + test.server.inner.clone(), acceptor, shutdown_rx, ); @@ -101,7 +100,7 @@ pub async fn test(test: &mut TestServer) { // Register push notification (no encryption) let push_id = client - .push_subscription_create("123", "https://127.0.0.1:9000/push", None) + .push_subscription_create("123", "https://127.0.0.1:19000/push", None) .await .unwrap() .take_id(); @@ -141,7 +140,7 @@ pub async fn test(test: &mut TestServer) { // Only one verification per minute is allowed let push_id = client - .push_subscription_create("invalid", "https://127.0.0.1:9000/push", None) + .push_subscription_create("invalid", "https://127.0.0.1:19000/push", None) .await .unwrap() .take_id(); @@ -152,7 +151,7 @@ pub async fn test(test: &mut TestServer) { let push_id = client .push_subscription_create( "123", - "https://127.0.0.1:9000/push?skip_checks=true", // skip_checks only works in cfg(test) + "https://127.0.0.1:19000/push?skip_checks=true", // skip_checks only works in cfg(test) keys.into(), ) .await @@ -261,9 +260,9 @@ struct PushVerification { pub verification_code: String, } -impl common::listener::SessionManager for SessionManager { +impl common::network::SessionManager for SessionManager { #[allow(clippy::manual_async_fn)] - fn handle( + fn handle( self, session: SessionData, ) -> impl std::future::Future + Send { @@ -345,12 +344,12 @@ async fn expect_nothing(event_rx: &mut mpsc::Receiver) { } } -async fn assert_state(event_rx: &mut mpsc::Receiver, id: &Id, state: &[DataType]) { +async fn assert_state(event_rx: &mut mpsc::Receiver, id: Id, state: &[DataType]) { assert_eq!( expect_push(event_rx) .await .unwrap_state_change() - .get(id) + .get(&id) .unwrap() .iter() .map(|x| x.0) diff --git a/tests/src/jmap/core/websocket.rs b/tests/src/jmap/core/websocket.rs index 750bd1e5..c2ac2e08 100644 --- a/tests/src/jmap/core/websocket.rs +++ b/tests/src/jmap/core/websocket.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::JMAPTest; +use crate::utils::server::TestServer; use ahash::AHashSet; use futures::StreamExt; use jmap_client::{ @@ -18,7 +18,7 @@ use jmap_client::{ use std::time::Duration; use tokio::sync::mpsc; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running WebSockets tests..."); // Authenticate all accounts diff --git a/tests/src/jmap/files/acl.rs b/tests/src/jmap/files/acl.rs index a989c149..77a3017c 100644 --- a/tests/src/jmap/files/acl.rs +++ b/tests/src/jmap/files/acl.rs @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, JmapUtils}; +use crate::utils::{jmap::JmapUtils, server::TestServer}; use jmap_proto::{ object::{file_node::FileNodeProperty, share_notification::ShareNotificationProperty}, request::method::MethodObject, }; use serde_json::json; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running File Storage ACL tests..."); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); diff --git a/tests/src/jmap/files/node.rs b/tests/src/jmap/files/node.rs index dfd2a82f..ec74394e 100644 --- a/tests/src/jmap/files/node.rs +++ b/tests/src/jmap/files/node.rs @@ -4,12 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{ChangeType, JMAPTest, JmapUtils}; +use crate::utils::{ + jmap::{ChangeType, JmapUtils}, + server::TestServer, +}; use ahash::AHashSet; use jmap_proto::{object::file_node::FileNodeProperty, request::method::MethodObject}; use serde_json::json; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running File Storage tests..."); let account = test.account("jdoe@example.com"); diff --git a/tests/src/jmap/mail/acl.rs b/tests/src/jmap/mail/acl.rs index 5d0666e3..99404301 100644 --- a/tests/src/jmap/mail/acl.rs +++ b/tests/src/jmap/mail/acl.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{directory::internal::TestInternalDirectory, jmap::JMAPTest}; +use crate::utils::server::TestServer; use ::email::mailbox::{INBOX_ID, TRASH_ID}; use jmap_client::{ core::{ @@ -15,27 +15,29 @@ use jmap_client::{ mailbox::{self, Role}, principal::ACL, }; +use registry::schema::prelude::ObjectType; +use serde_json::json; use std::fmt::Debug; use store::ahash::AHashMap; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running ACL tests..."); - let server = params.server.clone(); // Create a group and three test accounts let inbox_id = Id::new(INBOX_ID as u64).to_string(); let trash_id = Id::new(TRASH_ID as u64).to_string(); + let admin = test.account("admin@example.com"); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); let bill = test.account("bill@example.com"); let sales = test.account("sales@example.com"); // Authenticate all accounts - let mut john_client = john.client_owned().await; - let mut jane_client = jane.client_owned().await; - let mut bill_client = bill.client_owned().await; + let mut john_client = john.jmap_client().await; + let mut jane_client = jane.jmap_client().await; + let mut bill_client = bill.jmap_client().await; // Insert two emails in each account let mut email_ids = AHashMap::default(); @@ -43,11 +45,7 @@ pub async fn test(test: &mut TestServer) { (&mut john_client, john.id(), "john"), (&mut jane_client, jane.id(), "jane"), (&mut bill_client, bill.id(), "bill"), - ( - &mut test.account("admin").client_owned().await, - sales.id(), - "sales", - ), + (&mut admin.jmap_client().await, sales.id(), "sales"), ] { let user_name = client.session().username().to_string(); let mut ids = Vec::with_capacity(2); @@ -593,15 +591,15 @@ pub async fn test(test: &mut TestServer) { ); // Add John and Jane to the Sales group + let sales_id = test.account("sales@example.com").id(); for name in ["jdoe@example.com", "jane.smith@example.com"] { - server - .invalidate_principal_caches( - server - .core - .storage - .data - .add_to_group(name, "sales@example.com") - .await, + admin + .registry_update_object( + ObjectType::Account, + test.account(name).id(), + json!({ + "memberGroupIds": { sales_id: true }, + }), ) .await; } @@ -697,16 +695,16 @@ pub async fn test(test: &mut TestServer) { ); // Remove John from the sales group - server - .invalidate_principal_caches( - server - .core - .storage - .data - .remove_from_group("jdoe@example.com", "sales@example.com") - .await, + admin + .registry_update_object( + ObjectType::Account, + test.account("jdoe@example.com").id(), + json!({ + "memberGroupIds": { sales_id: false }, + }), ) .await; + assert_forbidden( john_client .set_default_account_id(sales.id_string()) @@ -715,10 +713,12 @@ pub async fn test(test: &mut TestServer) { ); // Destroy test account data - for id in [john, bill, jane, sales] { - test.destroy_all_mailboxes(id).await; + for account in [john, bill, jane, sales] { + admin + .destroy_all_mailboxes_for_account(account.id().document_id()) + .await; } - test.assert_is_empty().await;; + test.assert_is_empty().await; } pub fn assert_forbidden(result: Result) { diff --git a/tests/src/jmap/mail/changes.rs b/tests/src/jmap/mail/changes.rs index 5db750d8..e58802cf 100644 --- a/tests/src/jmap/mail/changes.rs +++ b/tests/src/jmap/mail/changes.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::JMAPTest; +use crate::utils::server::TestServer; use jmap_proto::types::state::State; use std::str::FromStr; use store::{ahash::AHashSet, write::BatchBuilder}; @@ -13,10 +13,10 @@ use types::{ id::Id, }; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email Changes tests..."); - let server = params.server.clone(); + let server = test.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; let mut states = vec![State::Initial]; @@ -313,7 +313,7 @@ pub async fn test(test: &mut TestServer) { assert_eq!(changes.updated(), Vec::::new()); assert_eq!(changes.destroyed(), Vec::::new()); test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } #[derive(Debug, Clone, Copy)] diff --git a/tests/src/jmap/mail/copy.rs b/tests/src/jmap/mail/copy.rs index 0cb47199..93799906 100644 --- a/tests/src/jmap/mail/copy.rs +++ b/tests/src/jmap/mail/copy.rs @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, mail::mailbox::destroy_all_mailboxes_for_account}; +use crate::utils::server::TestServer; use jmap_client::mailbox::Role; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email Copy tests..."); - let account = test.account("admin"); - let mut client = account.client_owned().await; + let account = test.account("admin@example.com"); + let mut client = account.jmap_client().await; // Create a mailbox on account 1 let ac1_mailbox_id = client @@ -96,7 +96,7 @@ pub async fn test(test: &mut TestServer) { ); // Empty store - destroy_all_mailboxes_for_account(1).await; - destroy_all_mailboxes_for_account(2).await; - test.assert_is_empty().await;; + account.destroy_all_mailboxes_for_account(1).await; + account.destroy_all_mailboxes_for_account(2).await; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/mail/get.rs b/tests/src/jmap/mail/get.rs index a6d8b6b9..89b041e4 100644 --- a/tests/src/jmap/mail/get.rs +++ b/tests/src/jmap/mail/get.rs @@ -4,14 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, replace_blob_ids}; +use crate::{jmap::replace_blob_ids, utils::server::TestServer}; use ::email::mailbox::INBOX_ID; use jmap_client::email::{self, Header, HeaderForm, import::EmailImportResponse}; use mail_parser::HeaderName; +use registry::schema::prelude::ObjectType; use std::{fs, path::PathBuf}; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email Get tests..."); let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -166,7 +167,10 @@ pub async fn test(test: &mut TestServer) { } test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.account("admin@example.com") + .registry_destroy_all(ObjectType::SpamTrainingSample) + .await; + test.assert_is_empty().await; } pub fn all_headers() -> Vec { diff --git a/tests/src/jmap/mail/mailbox.rs b/tests/src/jmap/mail/mailbox.rs index 8aabfa56..05df4045 100644 --- a/tests/src/jmap/mail/mailbox.rs +++ b/tests/src/jmap/mail/mailbox.rs @@ -4,10 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{Account, JMAPTest, wait_for_tasks}; +use crate::utils::server::TestServer; use jmap_client::{ Error, Set, - client::{Client, Credentials}, + client::Client, core::{ query::Filter, set::{SetError, SetErrorType, SetObject, SetRequest}, @@ -16,17 +16,16 @@ use jmap_client::{ }; use jmap_proto::types::state::State; use serde::{Deserialize, Serialize}; -use std::time::Duration; use store::ahash::AHashMap; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Mailbox tests..."); - let account = test.account("admin"); - let mut client = account.client_owned().await; + let account = test.account("admin@example.com"); + let client = account.jmap_client().await; // Create test mailboxes - client.set_default_account_id(Id::from(0u64)); + test.destroy_all_mailboxes(account).await; let id_map = create_test_mailboxes(&client).await; // Sort by name @@ -607,7 +606,7 @@ pub async fn test(test: &mut TestServer) { ["inbox", "sent", "spam"] ); - destroy_all_mailboxes_no_wait(&client).await; + test.destroy_all_mailboxes(account).await; test.assert_is_empty().await; } diff --git a/tests/src/jmap/mail/mod.rs b/tests/src/jmap/mail/mod.rs index 0351c552..7c6bc076 100644 --- a/tests/src/jmap/mail/mod.rs +++ b/tests/src/jmap/mail/mod.rs @@ -5,11 +5,8 @@ */ pub mod acl; -pub mod antispam; pub mod changes; pub mod copy; -pub mod crypto; -pub mod delivery; pub mod get; pub mod mailbox; pub mod parse; diff --git a/tests/src/jmap/mail/parse.rs b/tests/src/jmap/mail/parse.rs index 9d965cb2..b657d55d 100644 --- a/tests/src/jmap/mail/parse.rs +++ b/tests/src/jmap/mail/parse.rs @@ -4,14 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, mail::get::all_headers, replace_blob_ids}; +use crate::{ + jmap::{mail::get::all_headers, replace_blob_ids}, + utils::server::TestServer, +}; use jmap_client::{ email::{self, Header, HeaderForm}, mailbox::Role, }; use std::{fs, path::PathBuf}; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email Parse tests..."); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; @@ -222,5 +225,5 @@ pub async fn test(test: &mut TestServer) { } test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/mail/query.rs b/tests/src/jmap/mail/query.rs index d7682388..77e849b1 100644 --- a/tests/src/jmap/mail/query.rs +++ b/tests/src/jmap/mail/query.rs @@ -4,13 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - jmap::{Account, JMAPTest, wait_for_tasks}, - store::{deflate_test_resource, query::FIELDS}, -}; +use crate::store::{deflate_test_resource, query::FIELDS}; +use crate::utils::account::Account; +use crate::utils::server::TestServer; use ::email::{cache::MessageCacheFetch, mailbox::Mailbox}; use ahash::AHashSet; -use common::{Server, storage::index::ObjectIndexBuilder}; +use common::storage::index::ObjectIndexBuilder; use jmap_client::{ client::Client, core::query::{Comparator, Filter}, @@ -32,13 +31,13 @@ const MAX_THREADS: usize = 100; const MAX_MESSAGES: usize = 1000; const MAX_MESSAGES_PER_THREAD: usize = 100; -pub async fn test(test: &mut TestServer, insert: bool) { +pub async fn test(test: &TestServer) { println!("Running Email Query tests..."); - let server = params.server.clone(); + let server = test.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; - if insert { + if test.is_reset() { // Add some "virtual" mailbox ids so create doesn't fail let mut batch = BatchBuilder::new(); let account_id = Id::from_str(client.default_account_id()) @@ -71,11 +70,10 @@ pub async fn test(test: &mut TestServer, insert: bool) { // Create test messages println!("Inserting JMAP Mail query test messages..."); - create(&server, account).await; + create(test, account).await; assert_eq!( - params - .server + test.server .get_cached_messages(account_id) .await .unwrap() @@ -89,16 +87,16 @@ pub async fn test(test: &mut TestServer, insert: bool) { ); // Wait for indexing to complete - test.wait_for_tasks().await; + test.wait_for_tasks().await; } - let can_stem = !params.server.search_store().is_mysql(); + let can_stem = !test.server.search_store().is_mysql(); println!("Running JMAP Mail query tests..."); - query(client, can_stem).await; + query(&client, can_stem).await; println!("Running JMAP Mail query options tests..."); - query_options(client).await; + query_options(&client).await; println!("Deleting all messages..."); let mut request = client.build(); @@ -113,7 +111,7 @@ pub async fn test(test: &mut TestServer, insert: bool) { .unwrap(); test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } pub async fn query(client: &Client, can_stem: bool) { @@ -709,7 +707,7 @@ pub async fn query_options(client: &Client) { } } -pub async fn create(server: &Server, account: &Account) { +pub async fn create(test: &TestServer, account: &Account) { let sent_at = now(); let now = Instant::now(); let mut fields = AHashMap::default(); @@ -852,7 +850,7 @@ pub async fn create(server: &Server, account: &Account) { let mut tasks = Vec::new(); for chunk in chunks { - let client = account.client_owned().await; + let client = account.jmap_client().await; tasks.push(tokio::spawn(async move { for (raw_message, mailbox_ids, keywords, sent_at) in chunk { client @@ -867,7 +865,7 @@ pub async fn create(server: &Server, account: &Account) { task.await.unwrap(); } - wait_for_tasks(server).await; + test.wait_for_tasks().await; println!( "Imported {} messages in {} ms (single thread).", diff --git a/tests/src/jmap/mail/query_changes.rs b/tests/src/jmap/mail/query_changes.rs index 48209645..0a65024a 100644 --- a/tests/src/jmap/mail/query_changes.rs +++ b/tests/src/jmap/mail/query_changes.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{ - JMAPTest, - mail::changes::{LogAction, ParseState}, +use crate::{ + jmap::mail::changes::{LogAction, ParseState}, + utils::server::TestServer, }; use ::email::message::metadata::MessageData; use common::storage::index::ObjectIndexBuilder; @@ -27,9 +27,9 @@ use types::{ id::Id, }; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email QueryChanges tests..."); - let server = params.server.clone(); + let server = test.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; @@ -323,7 +323,7 @@ pub async fn test(test: &mut TestServer) { } test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } #[derive(Debug, Clone)] diff --git a/tests/src/jmap/mail/search_snippet.rs b/tests/src/jmap/mail/search_snippet.rs index eac800e9..ec707fb5 100644 --- a/tests/src/jmap/mail/search_snippet.rs +++ b/tests/src/jmap/mail/search_snippet.rs @@ -4,16 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, wait_for_tasks}; +use crate::utils::server::TestServer; use email::mailbox::INBOX_ID; use jmap_client::{core::query, email::query::Filter}; use std::{fs, path::PathBuf}; use store::ahash::AHashMap; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running SearchSnippet tests..."); - let server = params.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; let mailbox_id = Id::from(INBOX_ID).to_string(); @@ -47,9 +46,9 @@ pub async fn test(test: &mut TestServer) { .take_id(); email_ids.insert(email_name, email_id); } - test.wait_for_tasks().await; + test.wait_for_tasks().await; - let can_stem = params.server.search_store().internal_fts().is_some(); + let can_stem = test.server.search_store().internal_fts().is_some(); // Run tests for (filter, email_name, snippet_subject, snippet_preview) in [ @@ -171,5 +170,5 @@ pub async fn test(test: &mut TestServer) { // Destroy test data test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/mail/set.rs b/tests/src/jmap/mail/set.rs index d54ab089..69c6f075 100644 --- a/tests/src/jmap/mail/set.rs +++ b/tests/src/jmap/mail/set.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, find_values, replace_blob_ids, replace_boundaries, replace_values}; +use crate::{ + jmap::{find_values, replace_blob_ids, replace_boundaries, replace_values}, + utils::server::TestServer, +}; use ::email::mailbox::INBOX_ID; use ahash::AHashSet; use jmap_client::{ @@ -14,20 +17,24 @@ use jmap_client::{ email::{self, Email}, mailbox::Role, }; +use registry::schema::prelude::ObjectType; use std::{fs, path::PathBuf}; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email Set tests..."); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; let mailbox_id = Id::from(INBOX_ID).to_string(); - create(client, &mailbox_id).await; - update(client, &mailbox_id).await; + create(&client, &mailbox_id).await; + update(&client, &mailbox_id).await; test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.account("admin@example.com") + .registry_destroy_all(ObjectType::SpamTrainingSample) + .await; + test.assert_is_empty().await; } async fn create(client: &Client, mailbox_id: &str) { diff --git a/tests/src/jmap/mail/sieve_script.rs b/tests/src/jmap/mail/sieve_script.rs index f2044b57..65aed2a0 100644 --- a/tests/src/jmap/mail/sieve_script.rs +++ b/tests/src/jmap/mail/sieve_script.rs @@ -5,14 +5,8 @@ */ use crate::{ - jmap::{ - JMAPTest, - mail::{ - delivery::SmtpConnection, - submission::{MockMessage, assert_message_delivery, spawn_mock_smtp_server}, - }, - }, - smtp::DnsCache, + jmap::mail::submission::{MockMessage, assert_message_delivery, spawn_mock_smtp_server}, + utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection}, }; use jmap_client::{ Error, @@ -26,9 +20,9 @@ use std::{ time::{Duration, Instant}, }; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Sieve tests..."); - let server = params.server.clone(); + let server = test.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; @@ -497,7 +491,7 @@ pub async fn test(test: &mut TestServer) { client.sieve_script_destroy(&id).await.unwrap(); } test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } fn get_script(name: &str) -> Vec { diff --git a/tests/src/jmap/mail/submission.rs b/tests/src/jmap/mail/submission.rs index 530085d7..7bafccee 100644 --- a/tests/src/jmap/mail/submission.rs +++ b/tests/src/jmap/mail/submission.rs @@ -5,8 +5,8 @@ */ use crate::{ - jmap::{JMAPTest, mail::set::assert_email_properties}, - smtp::DnsCache, + jmap::mail::set::assert_email_properties, + utils::{dns::DnsCache, server::TestServer}, }; use ahash::AHashMap; use jmap_client::{ @@ -58,10 +58,10 @@ pub struct MockSMTPSettings { } #[allow(clippy::disallowed_types)] -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running E-mail submissions tests..."); // Start mock SMTP server - let server = params.server.clone(); + let server = test.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; let (mut smtp_rx, smtp_settings) = spawn_mock_smtp_server(); @@ -458,7 +458,7 @@ pub async fn test(test: &mut TestServer) { .mailbox_id(&mailbox_id_2, true); request.send().await.unwrap().unwrap_method_responses(); - assert_email_properties(client, &email_id, &[&mailbox_id_2], &["$draft"]).await; + assert_email_properties(&client, &email_id, &[&mailbox_id_2], &["$draft"]).await; // Verify onSuccessDestroyEmail action let mut request = client.build(); @@ -501,7 +501,7 @@ pub async fn test(test: &mut TestServer) { client.email_submission_destroy(&id).await.unwrap(); } test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } pub fn spawn_mock_smtp_server() -> (mpsc::Receiver, Arc>) { diff --git a/tests/src/jmap/mail/thread_get.rs b/tests/src/jmap/mail/thread_get.rs index 350987ec..6e2450e3 100644 --- a/tests/src/jmap/mail/thread_get.rs +++ b/tests/src/jmap/mail/thread_get.rs @@ -4,10 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, wait_for_tasks}; +use crate::utils::server::TestServer; use jmap_client::mailbox::Role; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Email Thread tests..."); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; diff --git a/tests/src/jmap/mail/thread_merge.rs b/tests/src/jmap/mail/thread_merge.rs index 20185e45..fd9a396e 100644 --- a/tests/src/jmap/mail/thread_merge.rs +++ b/tests/src/jmap/mail/thread_merge.rs @@ -4,10 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - jmap::{JMAPTest, mail::mailbox::destroy_all_mailboxes_no_wait, wait_for_tasks}, - store::deflate_test_resource, -}; +use crate::{store::deflate_test_resource, utils::server::TestServer}; use ::email::{ cache::MessageCacheFetch, mailbox::INBOX_ID, @@ -23,15 +20,15 @@ use store::{ }; use types::id::Id; -pub async fn test(test: &mut TestServer) { - test_single_thread(params).await; - test_multi_thread(params).await; +pub async fn test(test: &TestServer) { + test_single_thread(test).await; + test_multi_thread(test).await; } -async fn test_single_thread(test: &mut TestServer) { +async fn test_single_thread(test_server: &TestServer) { println!("Running Email Merge Threads tests..."); - let account = test.account("admin"); - let mut client = account.client_owned().await; + let account = test_server.account("admin@example.com"); + let mut client = account.jmap_client().await; let mut all_mailboxes = AHashMap::default(); for (base_test_num, test) in [test_1(), test_2(), test_3()].iter().enumerate() { @@ -141,7 +138,7 @@ async fn test_single_thread(test: &mut TestServer) { } } - test.wait_for_tasks().await; + test_server.wait_for_tasks().await; for test_num in 0..=5 { let result = client @@ -201,16 +198,17 @@ async fn test_single_thread(test: &mut TestServer) { // Delete all messages and make sure no keys are left in the store. for (base_test_num, mailbox_ids) in all_mailboxes { for (test_num, _) in mailbox_ids.into_iter().enumerate() { - client.set_default_account_id(Id::new((base_test_num + test_num) as u64).to_string()); - destroy_all_mailboxes_no_wait(&client).await; + account + .destroy_all_mailboxes_for_account((base_test_num + test_num) as u32) + .await; } } - test.assert_is_empty().await; + test_server.assert_is_empty().await; } #[allow(dead_code)] -async fn test_multi_thread(test: &mut TestServer) { +async fn test_multi_thread(test: &TestServer) { println!("Running Email Merge Threads tests (multi-threaded)..."); let mut handles = vec![]; let account = test.account("jdoe@example.com"); @@ -222,7 +220,7 @@ async fn test_multi_thread(test: &mut TestServer) { .into_iter() { let message = message.unwrap(); - let server = params.server.clone(); + let server = test.server.clone(); handles.push(tokio::task::spawn(async move { let mut retry_count = 0; loop { @@ -231,7 +229,7 @@ async fn test_multi_thread(test: &mut TestServer) { raw_message: message.contents(), message: MessageParser::new().parse(message.contents()), blob_hash: None, - access_token: &AccessToken::from_id(account_id), + access_token: &AccessToken::from_id_maybe_invalid(account_id), mailbox_ids: vec![mailbox_id], keywords: vec![], received_at: None, @@ -267,8 +265,7 @@ async fn test_multi_thread(test: &mut TestServer) { } assert_eq!( messages, - params - .server + test.server .get_cached_messages(account_id) .await .unwrap() diff --git a/tests/src/jmap/mail/vacation_response.rs b/tests/src/jmap/mail/vacation_response.rs index e0691b0e..6125e739 100644 --- a/tests/src/jmap/mail/vacation_response.rs +++ b/tests/src/jmap/mail/vacation_response.rs @@ -5,25 +5,19 @@ */ use crate::{ - jmap::{ - JMAPTest, - mail::{ - delivery::SmtpConnection, - submission::{ - MockMessage, assert_message_delivery, expect_nothing, spawn_mock_smtp_server, - }, - }, + jmap::mail::submission::{ + MockMessage, assert_message_delivery, expect_nothing, spawn_mock_smtp_server, }, - smtp::DnsCache, + utils::{dns::DnsCache, server::TestServer, smtp::SmtpConnection}, }; use chrono::{TimeDelta, Utc}; use std::time::Instant; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Vacation Response tests..."); // Create test account - let server = params.server.clone(); + let server = test.server.clone(); let account = test.account("jdoe@example.com"); let client = account.jmap_client().await; @@ -162,5 +156,5 @@ pub async fn test(test: &mut TestServer) { // Remove test data client.vacation_response_destroy().await.unwrap(); test.destroy_all_mailboxes(account).await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index d337e007..ac2b9658 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -4,54 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - AssertConfig, add_test_certs, - directory::internal::TestInternalDirectory, - jmap::server::{ - enterprise::{EnterpriseCore, insert_test_metrics}, - webhooks::{MockWebhookEndpoint, spawn_mock_webhook_endpoint}, - }, - store::{ - TempDir, build_store_config, - cleanup::{search_store_destroy, store_assert_is_empty, store_destroy}, +use crate::utils::server::TestServerBuilder; +use registry::{ + schema::{ + enums::{MtaProtocol, Permission}, + structs::{ + CalendarAlarm, Expression, ExpressionMatch, Imap, Jmap, MtaOutboundStrategy, MtaRoute, + MtaRouteRelay, MtaStageAuth, Sharing, + }, }, + types::list::List, }; -use ahash::AHashMap; -use base64::{ - Engine, - engine::general_purpose::{self, STANDARD}, -}; -use common::{ - Caches, Core, Data, Inner, Server, - config::{ - server::{Listeners, ServerProtocol}, - telemetry::Telemetry, - }, -}; -use http::HttpSessionManager; -use hyper::{Method, header::AUTHORIZATION}; -use imap::core::ImapSessionManager; -use jmap_client::client::{Client, Credentials}; -use jmap_proto::error::request::RequestError; -use managesieve::core::ManageSieveSessionManager; -use pop3::Pop3SessionManager; -use reqwest::header; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use serde_json::{Value, json}; -use services::SpawnServices; -use smtp::{SpawnQueueManager, core::SmtpSessionManager}; -use std::{ - fmt::{Debug, Display}, - path::PathBuf, - sync::Arc, - time::Duration, -}; -use store::{ - IterateParams, SUBSPACE_TASK_QUEUE, U32_LEN, U64_LEN, - write::{AnyKey, key::DeserializeBigEndian}, -}; -use tokio::sync::watch; -use types::id::Id; pub mod calendar; pub mod contacts; @@ -59,301 +22,188 @@ pub mod core; pub mod files; pub mod mail; pub mod principal; -pub mod server; #[tokio::test(flavor = "multi_thread")] async fn jmap_tests() { - let delete = std::env::var("NO_DELETE").is_err(); - let mut params = init_jmap_tests(delete).await; - - server::webhooks::test(&mut params).await; - - mail::get::test(&mut params).await; - mail::set::test(&mut params).await; - mail::parse::test(&mut params).await; - mail::query::test(&mut params, delete).await; - mail::search_snippet::test(&mut params).await; - mail::changes::test(&mut params).await; - mail::query_changes::test(&mut params).await; - mail::copy::test(&mut params).await; - mail::thread_get::test(&mut params).await; - mail::thread_merge::test(&mut params).await; - mail::mailbox::test(&mut params).await; - mail::delivery::test(&mut params).await; - mail::acl::test(&mut params).await; - mail::sieve_script::test(&mut params).await; - mail::vacation_response::test(&mut params).await; - mail::submission::test(&mut params).await; - mail::crypto::test(&mut params).await; - mail::antispam::test(&mut params).await; - - core::event_source::test(&mut params).await; - core::websocket::test(&mut params).await; - core::push_subscription::test(&mut params).await; - core::blob::test(&mut params).await; - - auth::limits::test(&mut params).await; - auth::oauth::test(&mut params).await; - auth::quota::test(&mut params).await; - auth::permissions::test(¶ms).await; - - contacts::addressbook::test(&mut params).await; - contacts::contact::test(&mut params).await; - contacts::acl::test(&mut params).await; - - files::node::test(&mut params).await; - files::acl::test(&mut params).await; - - calendar::calendars::test(&mut params).await; - calendar::event::test(&mut params).await; - calendar::notification::test(&mut params).await; - calendar::alarm::test(&mut params).await; - - calendar::identity::test(&mut params).await; - calendar::acl::test(&mut params).await; - - principal::get::test(&mut params).await; - principal::availability::test(&mut params).await; - - server::purge::test(&mut params).await; - server::enterprise::test(&mut params).await; - - assert_is_empty(¶ms.server).await; - - if delete { - params.temp_dir.delete(); - } -} - -#[ignore] -#[tokio::test(flavor = "multi_thread")] -pub async fn jmap_metric_tests() { - let params = init_jmap_tests(false).await; - - insert_test_metrics(params.server.core.clone()).await; -} - -#[allow(dead_code)] -pub struct JMAPTest { - server: Server, - accounts: AHashMap<&'static str, Account>, - temp_dir: TempDir, - webhook: Arc, - shutdown_tx: watch::Sender, -} - -impl JMAPTest { - pub fn account(&self, name: &str) -> &Account { - self.accounts.get(name).unwrap() - } - - pub async fn assert_is_empty(&self) { - assert_is_empty(&self.server).await; - } -} - -async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest { - // Load and parse config - let temp_dir = TempDir::new("jmap_tests", delete_if_exists); - let mut config = Config::new( - add_test_certs(&(build_store_config(&temp_dir.path.to_string_lossy()) + SERVER)) - .replace("{TMP}", &temp_dir.path.display().to_string()) - .replace( - "{LEVEL}", - &std::env::var("LOG").unwrap_or_else(|_| "disable".to_string()), - ), - ) - .unwrap(); - config.resolve_all_macros().await; - - // Parse servers - let mut servers = Listeners::parse(&mut config); - - // Bind ports and drop privileges - servers.bind_and_drop_priv(&mut config); - - // Build stores - let stores = Stores::parse_all(&mut config, false).await; - - // Parse core - let config_manager = ConfigManager { - cfg_local: Default::default(), - cfg_local_path: PathBuf::new(), - cfg_local_patterns: Patterns::parse(&mut config).into(), - cfg_store: config - .value("storage.data") - .and_then(|id| stores.stores.get(id)) - .cloned() - .unwrap_or_default(), - }; - let tracers = Telemetry::parse(&mut config, &stores); - let core = Core::parse(&mut config, stores, config_manager) + let mut test = TestServerBuilder::new("jmap_tests") .await - .enable_enterprise(); - let data = Data::parse(&mut config); - let cache = Caches::parse(&mut config); - let store = core.storage.data.clone(); - let search_store = core.storage.fts.clone(); - let (ipc, mut ipc_rxs) = build_ipc(false); - let inner = Arc::new(Inner { - shared_core: core.into_shared(), - data, - ipc, - cache, - }); + .with_default_listeners() + .await + .build() + .await; - if delete_if_exists { - store_destroy(&store).await; - search_store_destroy(&search_store).await; - } + // Create admin account + let admin = test + .create_user_account( + "admin", + "admin@example.com", + "these_pretzels_are_making_me_thirsty", + &[], + ) + .await; + test.account("admin") + .assign_roles_to_account(admin.id(), &["user", "system"]) + .await; - // Parse acceptors - servers.parse_tcp_acceptors(&mut config, inner.clone()); - - // Enable tracing - tracers.enable(true); - - // Start services - config.assert_no_errors(); - ipc_rxs.spawn_queue_manager(inner.clone()); - ipc_rxs.spawn_services(inner.clone()); - - // Spawn servers - let (shutdown_tx, _) = servers.spawn(|server, acceptor, shutdown_rx| { - match &server.protocol { - ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn( - SmtpSessionManager::new(inner.clone()), - inner.clone(), - acceptor, - shutdown_rx, - ), - ServerProtocol::Http => server.spawn( - HttpSessionManager::new(inner.clone()), - inner.clone(), - acceptor, - shutdown_rx, - ), - ServerProtocol::Imap => server.spawn( - ImapSessionManager::new(inner.clone()), - inner.clone(), - acceptor, - shutdown_rx, - ), - ServerProtocol::Pop3 => server.spawn( - Pop3SessionManager::new(inner.clone()), - inner.clone(), - acceptor, - shutdown_rx, - ), - ServerProtocol::ManageSieve => server.spawn( - ManageSieveSessionManager::new(inner.clone()), - inner.clone(), - acceptor, - shutdown_rx, - ), - }; - }); - - // Create tables - let server = inner.build_server(); - let mut accounts = AHashMap::new(); - - for (name, secret, description, emails) in [ - ("admin", "secret", "Superuser", &[][..]), + // Create test users + for (name, secret, description, aliases) in [ ( "jdoe@example.com", - "12345", + "12345 + extra safety", "John Doe", - &["jdoe@example.com", "john.doe@example.com"][..], + &["john.doe@example.com"][..], ), ( "jane.smith@example.com", - "abcde", + "abcde + extra safety", "Jane Smith", - &["jane.smith@example.com"], + &["jane@example.com"], ), ( "bill@example.com", - "098765", + "098765 + extra safety", "Bill Foobar", - &["bill@example.com"], + &["bill.foobar@example.com"], ), ( "robert@example.com", - "aabbcc", + "aabbcc + extra safety", "Robert Foobar", - &["robert@example.com"][..], + &[][..], ), ] { - let id: Id = server - .store() - .create_test_user(name, secret, description, emails) - .await - .into(); - let id_string = id.to_string(); - - let mut client = Client::new() - .credentials(Credentials::basic(name, secret)) - .timeout(Duration::from_secs(3600)) - .accept_invalid_certs(true) - .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") - .await - .unwrap(); - client.set_default_account_id(id_string.clone()); - - accounts.insert( - name, - Account { + let account = admin + .create_user_account( name, secret, - emails, - id, - id_string, - client, - }, - ); + description.into(), + aliases, + vec![Permission::UnlimitedRequests, Permission::UnlimitedUploads], + ) + .await; + test.insert_account(account); } - for (name, description, emails) in - [("sales@example.com", "Sales Group", &["sales@example.com"])] - { - let id: Id = server - .store() - .create_test_group(name, description, emails) - .await - .into(); - let id_string = id.to_string(); + // Create test group + test.insert_account( + admin + .create_group_account("sales@example.com", "Sales Group".into(), &[]) + .await, + ); - let mut client = Client::new() - .credentials(Credentials::basic("admin", "secret")) - .timeout(Duration::from_secs(3600)) - .accept_invalid_certs(true) - .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") - .await - .unwrap(); - client.set_default_account_id(id_string.clone()); - - accounts.insert( - name, - Account { - name, - secret: "", - emails, - id, - id_string, - client, + // Add test settings + admin + .registry_create_object(Imap { + allow_plain_text_auth: true, + ..Default::default() + }) + .await; + admin + .registry_create_object(Jmap { + set_max_objects: 100_000, + get_max_results: 100_000, + event_source_throttle: 500u64.into(), + push_throttle: 500u64.into(), + websocket_throttle: 500u64.into(), + push_attempt_wait: 500u64.into(), + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".to_string(), + ..Default::default() }, - ); - } + ..Default::default() + }) + .await; + admin + .registry_create_object(CalendarAlarm { + min_trigger_interval: 1000u64.into(), + ..Default::default() + }) + .await; + admin + .registry_create_object(Sharing { + allow_directory_queries: true, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaOutboundStrategy { + route: Expression { + match_: List::from_iter([ + ExpressionMatch { + if_: "rcpt_domain == 'example.com'".into(), + then: "'local'".into(), + }, + ExpressionMatch { + if_: "contains(['remote.org', 'foobar.com', 'test.com', 'other_domain.com'], rcpt_domain)".into(), + then: "'mock-smtp'".into(), + }, + ]), + else_: "'mx'".to_string(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaRoute::Relay(MtaRouteRelay { + address: "127.0.0.1".into(), + port: 9999, + allow_invalid_certs: true, + implicit_tls: true, + name: "mock-smtp".into(), + protocol: MtaProtocol::Smtp, + ..Default::default() + })) + .await; + admin.reload_settings().await; - JMAPTest { - server, - temp_dir, - accounts, - shutdown_tx, - webhook: spawn_mock_webhook_endpoint(), + test.insert_account(admin); + + /*mail::get::test(&test).await; + mail::set::test(&test).await; + mail::parse::test(&test).await; + mail::query::test(&test).await; + mail::search_snippet::test(&test).await; + mail::changes::test(&test).await; + mail::query_changes::test(&test).await; + mail::copy::test(&test).await; + mail::thread_get::test(&test).await; + mail::thread_merge::test(&test).await; + mail::mailbox::test(&test).await; + mail::acl::test(&test).await;*/ + mail::sieve_script::test(&test).await; + mail::vacation_response::test(&test).await; + mail::submission::test(&test).await; + + /*core::event_source::test(&test).await; + core::websocket::test(&test).await; + core::push_subscription::test(&test).await; + core::blob::test(&test).await; + + contacts::addressbook::test(&test).await; + contacts::contact::test(&test).await; + contacts::acl::test(&test).await; + + files::node::test(&test).await; + files::acl::test(&test).await; + + calendar::calendars::test(&test).await; + calendar::event::test(&test).await; + calendar::notification::test(&test).await; + calendar::alarm::test(&test).await; + + calendar::identity::test(&test).await; + calendar::acl::test(&test).await; + + principal::get::test(&test).await; + principal::availability::test(&test).await;*/ + + if test.is_reset() { + test.temp_dir.delete(); } } @@ -412,472 +262,3 @@ pub fn replace_blob_ids(string: String) -> String { string } } - -#[derive(Deserialize)] -#[serde(untagged)] -pub enum Response { - RequestError(RequestError<'static>), - Error { - error: String, - details: Option, - item: Option, - reason: Option, - }, - Data { - data: T, - }, -} - -pub struct ManagementApi { - pub port: u16, - pub username: String, - pub password: String, -} - -impl Default for ManagementApi { - fn default() -> Self { - Self { - port: 9980, - username: "admin".to_string(), - password: "secret".to_string(), - } - } -} - -impl ManagementApi { - pub fn new(port: u16, username: &str, password: &str) -> Self { - Self { - port, - username: username.to_string(), - password: password.to_string(), - } - } - - pub async fn post( - &self, - query: &str, - body: &impl Serialize, - ) -> Result, String> { - self.request_raw( - Method::POST, - query, - Some(serde_json::to_string(body).unwrap()), - ) - .await - .map(|result| { - serde_json::from_str::>(&result) - .unwrap_or_else(|err| panic!("{err}: {result}")) - }) - } - - pub async fn patch( - &self, - query: &str, - body: &impl Serialize, - ) -> Result, String> { - self.request_raw( - Method::PATCH, - query, - Some(serde_json::to_string(body).unwrap()), - ) - .await - .map(|result| { - serde_json::from_str::>(&result) - .unwrap_or_else(|err| panic!("{err}: {result}")) - }) - } - - pub async fn delete(&self, query: &str) -> Result, String> { - self.request_raw(Method::DELETE, query, None) - .await - .map(|result| { - serde_json::from_str::>(&result) - .unwrap_or_else(|err| panic!("{err}: {result}")) - }) - } - - pub async fn get(&self, query: &str) -> Result, String> { - self.request_raw(Method::GET, query, None) - .await - .map(|result| { - serde_json::from_str::>(&result) - .unwrap_or_else(|err| panic!("{err}: {result}")) - }) - } - pub async fn request( - &self, - method: Method, - query: &str, - ) -> Result, String> { - self.request_raw(method, query, None).await.map(|result| { - serde_json::from_str::>(&result) - .unwrap_or_else(|err| panic!("{err}: {result}")) - }) - } - - async fn request_raw( - &self, - method: Method, - query: &str, - body: Option, - ) -> Result { - let mut request = reqwest::Client::builder() - .timeout(Duration::from_millis(500)) - .danger_accept_invalid_certs(true) - .build() - .unwrap() - .request(method, format!("https://127.0.0.1:{}{query}", self.port)); - - if let Some(body) = body { - request = request.body(body); - } - - request - .header( - AUTHORIZATION, - format!( - "Basic {}", - STANDARD.encode(format!("{}:{}", self.username, self.password).as_bytes()) - ), - ) - .send() - .await - .map_err(|err| err.to_string())? - .bytes() - .await - .map(|bytes| String::from_utf8(bytes.to_vec()).unwrap()) - .map_err(|err| err.to_string()) - } -} - -impl Response { - pub fn unwrap_data(self) -> T { - match self { - Response::Data { data } => data, - Response::Error { - error, - details, - reason, - .. - } => { - panic!("Expected data, found error {error:?}: {details:?} {reason:?}") - } - Response::RequestError(err) => { - panic!("Expected data, found error {err:?}") - } - } - } - - pub fn try_unwrap_data(self) -> Option { - match self { - Response::Data { data } => Some(data), - Response::RequestError(error) if error.status == 404 => None, - Response::Error { - error, - details, - reason, - .. - } => { - panic!("Expected data, found error {error:?}: {details:?} {reason:?}") - } - Response::RequestError(err) => { - panic!("Expected data, found error {err:?}") - } - } - } - - pub fn unwrap_error(self) -> (String, Option, Option) { - match self { - Response::Error { - error, - details, - reason, - .. - } => (error, details, reason), - Response::Data { data } => panic!("Expected error, found data: {data:?}"), - Response::RequestError(err) => { - panic!("Expected error, found request error {err:?}") - } - } - } - - pub fn unwrap_request_error(self) -> RequestError<'static> { - match self { - Response::Error { - error, - details, - reason, - .. - } => { - panic!("Expected request error, found error {error:?}: {details:?} {reason:?}") - } - Response::Data { data } => panic!("Expected request error, found data: {data:?}"), - Response::RequestError(err) => err, - } - } - - pub fn expect_request_error(self, value: &str) { - let err = self.unwrap_request_error(); - if !err.detail.contains(value) && !err.title.as_ref().is_some_and(|t| t.contains(value)) { - panic!("Expected request error containing {value:?}, found {err:?}") - } - } - - pub fn expect_error(self, value: &str) { - let (error, details, reason) = self.unwrap_error(); - if !error.contains(value) - && !details.as_ref().is_some_and(|d| d.contains(value)) - && !reason.as_ref().is_some_and(|r| r.contains(value)) - { - panic!("Expected error containing {value:?}, found {error:?}: {details:?} {reason:?}") - } - } -} - -const SERVER: &str = r#" -[server] -hostname = "'jmap.example.org'" - -[http] -url = "'https://127.0.0.1:8899'" - -[server.listener.jmap] -bind = ["127.0.0.1:8899"] -protocol = "http" -max-connections = 81920 -tls.implicit = true - -[server.listener.imap] -bind = ["127.0.0.1:9991"] -protocol = "imap" -max-connections = 81920 - -[server.listener.lmtp-debug] -bind = ['127.0.0.1:11200'] -greeting = 'Test LMTP instance' -protocol = 'lmtp' -tls.implicit = false - -[server.listener.pop3] -bind = ["127.0.0.1:4110"] -protocol = "pop3" -max-connections = 81920 -tls.implicit = true - -[server.socket] -reuse-addr = true - -[server.tls] -enable = true -implicit = false -certificate = "default" - -[server.fail2ban] -authentication = "100/5s" - -[authentication] -rate-limit = "100/2s" - -[session.ehlo] -reject-non-fqdn = false - -[session.rcpt] -relay = [ { if = "!is_empty(authenticated_as)", then = true }, - { else = false } ] - -[session.rcpt.errors] -total = 5 -wait = "1ms" - -[session.auth] -mechanisms = "[plain, login, oauthbearer]" - -[session.data] -spam-filter = "recipients[0] != 'robert@example.com'" - -[session.data.add-headers] -delivered-to = false - -[queue] -path = "{TMP}" -hash = 64 - -[report] -path = "{TMP}" -hash = 64 - -[resolver] -type = "system" - -[queue.strategy] -route = [ { if = "rcpt_domain == 'example.com'", then = "'local'" }, - { if = "contains(['remote.org', 'foobar.com', 'test.com', 'other_domain.com'], rcpt_domain)", then = "'mock-smtp'" }, - { else = "'mx'" } ] - -[queue.route."mock-smtp"] -type = "relay" -address = "localhost" -port = 9999 -protocol = "smtp" - -[queue.route."mock-smtp".tls] -implicit = false -allow-invalid-certs = true - -[session.extensions] -future-release = [ { if = "!is_empty(authenticated_as)", then = "99999999d"}, - { else = false } ] - -[certificate.default] -cert = "%{file:{CERT}}%" -private-key = "%{file:{PK}}%" - -[jmap.protocol.get] -max-objects = 100000 - -[jmap.protocol.set] -max-objects = 100000 - -[jmap.protocol.request] -max-concurrent = 8 - -[jmap.protocol.upload] -max-size = 5000000 -max-concurrent = 4 -ttl = "1m" - -[jmap.protocol.upload.quota] -files = 3 -size = 50000 - -[jmap.rate-limit] -account = "1000/1m" -anonymous = "100/1m" - -[jmap.event-source] -throttle = "500ms" - -[jmap.web-sockets] -throttle = "500ms" - -[jmap.push] -throttle = "500ms" -attempts.interval = "500ms" - -[email] -auto-expunge = "1s" - -[changes] -max-history = "1" - -[store."auth"] -type = "sqlite" -path = "{TMP}/auth.db" - -[store."auth".query] -name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true" -members = "SELECT member_of FROM group_members WHERE name = ?" -recipients = "SELECT name FROM emails WHERE address = ?" -emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" -verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" -domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" - -[imap.auth] -allow-plain-text = true - -[oauth] -key = "parerga_und_paralipomena" - -[oauth.auth] -max-attempts = 1 - -[oauth.expiry] -user-code = "1s" -token = "1s" -refresh-token = "3s" -refresh-token-renew = "2s" - -[oauth.client-registration] -anonymous = true -require = true - -[oauth.oidc] -signature-key = '''-----BEGIN PRIVATE KEY----- -MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDMXJI1bL3z8gaF -Ze/6493VjL+jHkFMP2Pc7fLwRF1fhkuIdYTp69LabzrSEJCRCz0UI2NHqPOgtOta -+zRHKAMr7c7Z6uKO0K+aXiQYHw4Y70uSG8CnmNl7kb4OM/CAcoO6fePmvBsyESfn -TmkJ5bfHEZQFDQEAoDlDjtjxuwYsAQQVQXuAydi8j8pyTWKAJ1RDgnUT+HbOub7j -JrQ7sPe6MPCjXv5N76v9RMHKktfYwRNMlkLkxImQU55+vlvghNztgFlIlJDFfNiy -UQPV5FTEZJli9BzMoj1JQK3sZyV8WV0W1zN41QQ+glAAC6+K7iTDPRMINBSwbHyn -6Lb9Q6U7AgMBAAECggEAB93qZ5xrhYgEFeoyKO4mUdGsu4qZyJB0zNeWGgdaXCfZ -zC4l8zFM+R6osix0EY6lXRtC95+6h9hfFQNa5FWseupDzmIQiEnim1EowjWef87l -Eayi0nDRB8TjqZKjR/aLOUhzrPlXHKrKEUk/RDkacCiDklwz9S0LIfLOSXlByBDM -/n/eczfX2gUATexMHSeIXs8vN2jpuiVv0r+FPXcRvqdzDZnYSzS8BJ9k6RYXVQ4o -NzCbfqgFIpVryB7nHgSTrNX9G7299If8/dXmesXWSFEJvvDSSpcBoINKbfgSlrxd -6ubjiotcEIBUSlbaanRrydwShhLHnXyupNAb7tlvyQKBgQDsIipSK4+H9FGl1rAk -Gg9DLJ7P/94sidhoq1KYnj/CxwGLoRq22khZEUYZkSvYXDu1Qkj9Avi3TRhw8uol -l2SK1VylL5FQvTLKhWB7b2hjrUd5llMRgS3/NIdLhOgDMB7w3UxJnCA/df/Rj+dM -WhkyS1f0x3t7XPLwWGurW0nJcwKBgQDdjhrNfabrK7OQvDpAvNJizuwZK9WUL7CD -rR0V0MpDGYW12BTEOY6tUK6XZgiRitAXf4EkEI6R0Q0bFzwDDLrg7TvGdTuzNeg/ -8vm8IlRlOkrdihtHZI4uRB7Ytmz24vzywEBE0p6enA7v4oniscUks/KKmDGr0V90 -yT9gIVrjGQKBgQCjnWC5otlHGLDiOgm+WhgtMWOxN9dYAQNkMyF+Alinu4CEoVKD -VGhA3sk1ufMpbW8pvw4X0dFIITFIQeift3DBCemxw23rBc2FqjkaDi3EszINO22/ -eUTHyjvcxfCFFPi7aHsNnhJyJm7lY9Kegudmg/Ij93zGE7d5darVBuHvpQKBgBBY -YovUgFMLR1UfPeD2zUKy52I4BKrJFemxBNtOKw3mPSIcTfPoFymcMTVENs+eARoq -svlZK1uAo8ni3e+Pqd3cQrOyhHQFPxwwrdH+amGJemp7vOV4erDZH7l3Q/S27Fhw -bI1nSIKFGukBupB58wRxLiyha9C0QqmYC0/pRg5JAn8Rbj5tP26oVCXjZEfWJL8J -axxSxsGA4Vol6i6LYnVgZG+1ez2rP8vUORo1lRzmdeP4o1BSJf9TPwXkuppE5J+t -UZVKtYGlEn1RqwGNd8I9TiWvU84rcY9nsxlDR86xwKRWFvYqVOiGYtzRyewYRdjU -rTs9aqB3v1+OVxGxR6Na ------END PRIVATE KEY----- -''' -signature-algorithm = "RS256" - -[oauth.oidc-ignore] -signature-key = '''-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggybcqc86ulFFiOon -WiYrLO4z8/kmkqvA7wGElBok9IqhRANCAAQxZK68FnQtHC0eyh8CA05xRIvxhVHn -0ymka6XBh9aFtW4wfeoKhTkSKjHc/zjh9Rr2dr3kvmYe80fMGhW4ycGA ------END PRIVATE KEY----- -''' -signature-algorithm = "ES256" - -[session.extensions] -expn = true -vrfy = true - -[spam-filter] -enable = true - -[spam-filter.list] -scores = {"GTUBE_TEST" = "1000.0"} - -[sharing] -allow-directory-query = true - -[calendar.alarms] -minimum-interval = "1s" - -[tracer.console] -type = "console" -level = "{LEVEL}" -multiline = false -ansi = true -#disabled-events = ["network.*", "telemetry.webhook-error"] -disabled-events = ["network.*", "telemetry.webhook-error", "http.request-body"] - -[webhook."test"] -url = "http://127.0.0.1:8821/hook" -events = ["auth.*", "delivery.dsn*", "message-ingest.*", "security.authentication-ban"] -signature-key = "ovos-moles" -throttle = "100ms" - -[sieve.untrusted.scripts."common"] -contents = ''' -require "reject"; - -reject "Rejected from a global script."; -stop; -''' -"#; diff --git a/tests/src/jmap/principal/availability.rs b/tests/src/jmap/principal/availability.rs index 92e3a600..96bd25b2 100644 --- a/tests/src/jmap/principal/availability.rs +++ b/tests/src/jmap/principal/availability.rs @@ -4,13 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{IntoJmapSet, JMAPTest, JmapUtils, calendar::event::*}; +use crate::{ + jmap::calendar::event::*, + utils::{ + jmap::{IntoJmapSet, JmapUtils}, + server::TestServer, + }, +}; use calcard::jscalendar::JSCalendarProperty; use jmap_proto::request::method::MethodObject; use serde_json::json; use types::id::Id; -pub async fn test(test: &mut TestServer) { +pub async fn test(test: &TestServer) { println!("Running Principal Availability tests..."); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); @@ -251,5 +257,5 @@ pub async fn test(test: &mut TestServer) { // Cleanup john.destroy_all_calendars().await; - test.assert_is_empty().await;; + test.assert_is_empty().await; } diff --git a/tests/src/jmap/principal/get.rs b/tests/src/jmap/principal/get.rs index 5f913a17..aecaa2f9 100644 --- a/tests/src/jmap/principal/get.rs +++ b/tests/src/jmap/principal/get.rs @@ -4,11 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::{JMAPTest, JmapUtils}; use jmap_proto::{object::principal::PrincipalProperty, request::method::MethodObject}; use serde_json::json; -pub async fn test(test: &mut TestServer) { +use crate::utils::{jmap::JmapUtils, server::TestServer}; + +pub async fn test(test: &TestServer) { println!("Running Principal get/query tests..."); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); @@ -25,10 +26,10 @@ pub async fn test(test: &mut TestServer) { response.assert_is_equal(json!({ "capabilities": { "urn:ietf:params:jmap:core": { - "maxSizeUpload": 5000000, + "maxSizeUpload": 50000000, "maxConcurrentUpload": 4, "maxSizeRequest": 10000000, - "maxConcurrentRequests": 8, + "maxConcurrentRequests": 4, "maxCallsInRequest": 16, "maxObjectsInGet": 100000, "maxObjectsInSet": 100000, @@ -113,7 +114,7 @@ pub async fn test(test: &mut TestServer) { "urn:ietf:params:jmap:websocket": {}, "urn:ietf:params:jmap:sieve": { "maxSizeScriptName": 512, - "maxSizeScript": 1048576, + "maxSizeScript": 102400, "maxNumberScripts": 100, "maxNumberRedirects": 1, "sieveExtensions": [ diff --git a/tests/src/jmap/server/enterprise.rs b/tests/src/jmap/server/enterprise.rs deleted file mode 100644 index a1abe125..00000000 --- a/tests/src/jmap/server/enterprise.rs +++ /dev/null @@ -1,412 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: LicenseRef-SEL - * - * This file is subject to the Stalwart Enterprise License Agreement (SEL) and - * is NOT open source software. - * - */ - -use crate::{ - AssertConfig, - directory::internal::TestInternalDirectory, - imap::{ImapConnection, Type}, - jmap::{ - JMAPTest, ManagementApi, - mail::delivery::{AssertResult, SmtpConnection}, - server::List, - wait_for_tasks, - }, -}; -use common::{ - Core, Server, - config::telemetry::{StoreTracer, TelemetrySubscriberType}, - enterprise::{Enterprise, license::LicenseKey}, - telemetry::{ - metrics::store::{MetricsStore, SharedMetricHistory}, - tracers::store::TracingStore, - }, -}; -use imap_proto::ResponseType; -use nlp::language::Language; -use std::{sync::Arc, time::Duration}; -use store::{ - rand::{self, Rng}, - search::{ - SearchField, SearchFilter, SearchOperator, SearchQuery, SearchValue, TracingSearchField, - }, - write::{SearchIndex, now}, -}; -use trc::{ - ipc::{bitset::Bitset, subscriber::SubscriberBuilder}, - *, -}; - -const METRICS_CONFIG: &str = r#" -[metrics.alerts.expected] -enable = true -condition = "domain_count > 1 && cluster_publisher_error > 3" - -[metrics.alerts.expected.notify.event] -enable = true -message = "Yikes! Found %{cluster.publisher-error}% cluster errors!" - -[metrics.alerts.expected.notify.email] -enable = true -from-name = "Alert Subsystem" -from-addr = "alert@example.com" -to = ["jdoe@example.com"] -subject = "Found %{cluster.publisher-error}% cluster errors" -body = "Sorry for the bad news, but we found %{domain.count}% domains and %{cluster.publisher-error}% cluster errors." - -[metrics.alerts.unexpected] -enable = true -condition = "domain_count < 1 || cluster_publisher_error < 3" - -[metrics.alerts.unexpected.notify.event] -enable = true -message = "this should not have happened" - -"#; - -pub async fn test(test: &mut TestServer) { - // Enable Enterprise - println!("Running Enterprise tests..."); - let mut core = params.server.inner.shared_core.load_full().as_ref().clone(); - let mut config = Config::new(METRICS_CONFIG).unwrap(); - core.enterprise = Enterprise { - license: LicenseKey { - valid_to: now() + 3600, - valid_from: now() - 3600, - domain: String::new(), - accounts: 100, - }, - undelete: Undelete { - retention: Duration::from_secs(2), - } - .into(), - trace_store: TraceStore { - retention: Some(Duration::from_secs(1)), - store: core.storage.data.clone(), - } - .into(), - metrics_store: MetricStore { - retention: Some(Duration::from_secs(1)), - store: core.storage.data.clone(), - interval: SimpleCron::Day { hour: 0, minute: 0 }, - } - .into(), - metrics_alerts: parse_metric_alerts(&mut config), - logo_url: None, - ai_apis: Default::default(), - spam_filter_llm: None, - template_calendar_alarm: None, - template_scheduling_email: None, - template_scheduling_web: None, - } - .into(); - config.assert_no_errors(); - assert_ne!(core.enterprise.as_ref().unwrap().metrics_alerts.len(), 0); - params.server.inner.shared_core.store(core.into()); - assert!( - params - .server - .inner - .shared_core - .load() - .is_enterprise_edition() - ); - - // Create test account - let server = params.server.inner.build_server(); - let account_id = server - .store() - .create_test_user( - "jdoe@example.com", - "12345", - "John Doe", - &["jdoe@example.com"], - ) - .await; - - alerts(&server).await; - undelete(params).await; - tracing(params).await; - metrics(params).await; - - // Delete test account - server - .store() - .delete_principal(QueryBy::Id(account_id)) - .await - .unwrap(); - destroy_account_data(&server, account_id, true) - .await - .unwrap(); - test.assert_is_empty().await; - - params.server.inner.shared_core.store( - params - .server - .inner - .shared_core - .load_full() - .as_ref() - .clone() - .enable_enterprise() - .into(), - ); -} - -async fn alerts(server: &Server) { - // Make sure the required metrics are set to 0 - assert_eq!( - Collector::read_event_metric(EventType::Cluster(ClusterEvent::PublisherError).id()), - 0 - ); - assert_eq!(Collector::read_metric(MetricType::DomainCount), 0.0); - assert_eq!( - Collector::read_event_metric(EventType::Telemetry(TelemetryEvent::Alert).id()), - 0 - ); - - // Increment metrics to trigger alerts - Collector::update_event_counter(EventType::Cluster(ClusterEvent::PublisherError), 5); - Collector::update_gauge(MetricType::DomainCount, 3); - - // Make sure the values were set - assert_eq!( - Collector::read_event_metric(EventType::Cluster(ClusterEvent::PublisherError).id()), - 5 - ); - assert_eq!(Collector::read_metric(MetricType::DomainCount), 3.0); - - // Process alerts - let message = server.process_alerts().await.unwrap().pop().unwrap(); - assert_eq!(message.from, "alert@example.com"); - assert_eq!(message.to, vec!["jdoe@example.com".to_string()]); - let body = String::from_utf8(message.body).unwrap(); - assert!( - body.contains("Sorry for the bad news, but we found 3 domains and 5 cluster errors."), - "{body:?}" - ); - assert!(body.contains("Subject: Found 5 cluster errors"), "{body:?}"); - assert!( - body.contains("From: \"Alert Subsystem\" "), - "{body:?}" - ); - assert!(body.contains("To: "), "{body:?}"); - - // Make sure the event was triggered - assert_eq!( - Collector::read_event_metric(EventType::Telemetry(TelemetryEvent::Alert).id()), - 1 - ); -} - -async fn tracing(test: &mut TestServer) { - // Enable tracing - let store = params.server.core.storage.data.clone(); - let query = params.server.core.storage.fts.clone(); - TelemetrySubscriberType::StoreTracer(StoreTracer { - store: store.clone(), - }) - .spawn( - SubscriberBuilder::new("store-tracer".to_string()).with_interests(Box::new(Bitset::all())), - true, - ); - - // Make sure there are no span entries in the db - store - .purge_spans(Duration::from_secs(0), Some(&query)) - .await - .unwrap(); - assert_eq!( - query - .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![ - SearchFilter::Operator { - field: SearchField::Tracing(TracingSearchField::EventType), - op: SearchOperator::Equal, - value: SearchValue::Uint(EventType::Smtp(SmtpEvent::ConnectionStart).code()) - } - ])) - .await - .unwrap(), - Vec::::new() - ); - - // Send an email - let mut lmtp = SmtpConnection::connect().await; - lmtp.ingest( - "bill@example.com", - &["jdoe@example.com"], - concat!( - "From: bill@example.com\r\n", - "To: jdoe@example.com\r\n", - "Subject: TPS Report\r\n", - "X-Spam-Status: No\r\n", - "\r\n", - "I'm going to need those TPS reports ASAP. ", - "So, if you could do that, that'd be great." - ), - ) - .await; - lmtp.quit().await; - tokio::time::sleep(Duration::from_millis(300)).await; - - test.server.notify_task_queue(); - test.wait_for_tasks().await; - - // Purge should not delete anything at this point - store - .purge_spans(Duration::from_secs(2), Some(&query)) - .await - .unwrap(); - - // There should be a span entry in the db - for span_type in [ - EventType::Delivery(DeliveryEvent::AttemptStart), - EventType::Smtp(SmtpEvent::ConnectionStart), - ] { - let spans = query - .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![ - SearchFilter::Operator { - field: SearchField::Tracing(TracingSearchField::EventType), - op: SearchOperator::Equal, - value: SearchValue::Uint(span_type.code()), - }, - ])) - .await - .unwrap(); - assert_eq!(spans.len(), 1, "{span_type:?}"); - assert_eq!( - store.get_span(spans[0]).await.unwrap()[0].inner.typ, - span_type - ); - } - - // Try searching - for keyword in ["bill@example.com", "jdoe@example.com", "example.com"] { - let spans = query - .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![ - SearchFilter::Operator { - field: SearchField::Tracing(TracingSearchField::Keywords), - op: SearchOperator::Equal, - value: SearchValue::Text { - value: keyword.to_string(), - language: Language::None, - }, - }, - ])) - .await - .unwrap(); - - assert_eq!(spans.len(), 2, "keyword: {keyword}"); - assert!(spans[0] != spans[1], "keyword: {keyword}"); - } - - // Purge should delete the span entries - tokio::time::sleep(Duration::from_millis(800)).await; - store - .purge_spans(Duration::from_secs(1), Some(&query)) - .await - .unwrap(); - - assert_eq!( - query - .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![ - SearchFilter::Operator { - field: SearchField::Id, - op: SearchOperator::GreaterThan, - value: SearchValue::Uint(0), - }, - ])) - .await - .unwrap(), - Vec::::new() - ); -} - -async fn metrics(test: &mut TestServer) { - // Make sure there are no span entries in the db - let store = params.server.core.storage.data.clone(); - assert_eq!( - store.query_metrics(0, u64::MAX).await.unwrap(), - Vec::>::new() - ); - - insert_test_metrics(params.server.core.clone()).await; - - let total = store.query_metrics(0, u64::MAX).await.unwrap(); - assert!(!total.is_empty(), "{total:?}"); - - store.purge_metrics(Duration::from_secs(0)).await.unwrap(); - assert_eq!( - store.query_metrics(0, u64::MAX).await.unwrap(), - Vec::>::new() - ); -} - -pub async fn insert_test_metrics(core: Arc) { - let store = core.storage.data.clone(); - store.purge_metrics(Duration::from_secs(0)).await.unwrap(); - let mut start_time = now() - (90 * 24 * 60 * 60); - let timestamp = now(); - let history = SharedMetricHistory::default(); - - while start_time < timestamp { - for event_type in [ - EventType::Smtp(SmtpEvent::ConnectionStart), - EventType::Imap(ImapEvent::ConnectionStart), - EventType::Pop3(Pop3Event::ConnectionStart), - EventType::ManageSieve(ManageSieveEvent::ConnectionStart), - EventType::Http(HttpEvent::ConnectionStart), - EventType::Delivery(DeliveryEvent::AttemptStart), - EventType::Queue(QueueEvent::QueueMessage), - EventType::Queue(QueueEvent::QueueMessageAuthenticated), - EventType::Queue(QueueEvent::QueueDsn), - EventType::Queue(QueueEvent::QueueReport), - EventType::MessageIngest(MessageIngestEvent::Ham), - EventType::MessageIngest(MessageIngestEvent::Spam), - EventType::Auth(AuthEvent::Failed), - EventType::Security(SecurityEvent::AuthenticationBan), - EventType::Security(SecurityEvent::ScanBan), - EventType::Security(SecurityEvent::AbuseBan), - EventType::Security(SecurityEvent::LoiterBan), - EventType::Security(SecurityEvent::IpBlocked), - EventType::IncomingReport(IncomingReportEvent::DmarcReport), - EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings), - EventType::IncomingReport(IncomingReportEvent::TlsReport), - EventType::IncomingReport(IncomingReportEvent::TlsReportWithWarnings), - ] { - // Generate a random value between 0 and 100 - Collector::update_event_counter(event_type, rand::rng().random_range(0..=100)) - } - - Collector::update_gauge(MetricType::QueueCount, rand::rng().random_range(0..=1000)); - Collector::update_gauge( - MetricType::ServerMemory, - rand::rng().random_range(100 * 1024 * 1024..=300 * 1024 * 1024), - ); - - for metric_type in [ - MetricType::MessageIngestionTime, - MetricType::MessageFtsIndexTime, - MetricType::DeliveryTime, - MetricType::DnsLookupTime, - ] { - Collector::update_histogram(metric_type, rand::rng().random_range(2..=1000)) - } - Collector::update_histogram( - MetricType::DeliveryTotalTime, - rand::rng().random_range(1000..=5000), - ); - - store - .write_metrics(core.clone(), start_time, history.clone()) - .await - .unwrap(); - start_time += 60 * 60; - } -} diff --git a/tests/src/jmap/server/mod.rs b/tests/src/jmap/server/mod.rs deleted file mode 100644 index a009f091..00000000 --- a/tests/src/jmap/server/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -pub mod enterprise; -pub mod purge; -pub mod webhooks; - -#[derive(serde::Deserialize, Debug)] -#[allow(dead_code)] -pub(crate) struct List { - pub items: Vec, - pub total: usize, -} diff --git a/tests/src/lib.rs b/tests/src/lib.rs index dc122981..bcf30d90 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -4,8 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::path::PathBuf; - #[cfg(test)] use ::store::registry::bootstrap::Bootstrap; #[cfg(not(target_env = "msvc"))] @@ -24,33 +22,23 @@ pub mod cluster; pub mod directory; #[cfg(test)] pub mod imap; -#[cfg(test)] -pub mod jmap; + #[cfg(test)] pub mod smtp; #[cfg(test)] pub mod webdav; */ #[cfg(test)] +pub mod jmap; +#[cfg(test)] pub mod store; #[cfg(test)] pub mod system; #[cfg(test)] +pub mod telemetry; +#[cfg(test)] pub mod utils; -pub fn add_test_certs(config: &str) -> String { - let mut cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - cert_path.push("resources"); - let mut cert = cert_path.clone(); - cert.push("tls_cert.pem"); - let mut pk = cert_path.clone(); - pk.push("tls_privatekey.pem"); - - config - .replace("{CERT}", cert.as_path().to_str().unwrap()) - .replace("{PK}", pk.as_path().to_str().unwrap()) -} - #[cfg(test)] pub trait AssertConfig { fn assert_no_errors(self) -> Self; @@ -84,16 +72,3 @@ pub fn enable_logging() { Telemetry::test_tracer(level.parse().expect("Invalid log level")); } } - -pub const TEST_USERS: &[(&str, &str, &str, &str)] = &[ - ("admin", "secret", "Superuser", "admin@example.com"), - ("john", "secret2", "John Doe", "jdoe@example.com"), - ( - "jane", - "secret3", - "Jane Doe-Smith", - "jane.smith@example.com", - ), - ("bill", "secret4", "Bill Foobar", "bill@example.com"), - ("mike", "secret5", "Mike Noquota", "mike@example.com"), -]; diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index bda84c54..dc678139 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -282,117 +282,3 @@ impl TestSMTP { self.server.clone() } } - -pub trait DnsCache { - fn txt_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: impl Into, - valid_until: std::time::Instant, - ); - fn ipv4_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: Vec, - valid_until: std::time::Instant, - ); - fn ipv6_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: Vec, - valid_until: std::time::Instant, - ); - fn dnsbl_add(&self, name: &str, value: Vec, valid_until: std::time::Instant); - fn ptr_add(&self, name: IpAddr, value: Vec, valid_until: std::time::Instant); - fn mx_add<'x>(&self, name: impl IntoFqdn<'x>, value: Vec, valid_until: std::time::Instant); - fn tlsa_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: Arc, - valid_until: std::time::Instant, - ); -} - -impl DnsCache for Server { - fn txt_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: impl Into, - valid_until: std::time::Instant, - ) { - self.inner.cache.dns_txt.insert_with_expiry( - name.into_fqdn().into_owned(), - value.into(), - valid_until, - ); - } - - fn ipv4_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: Vec, - valid_until: std::time::Instant, - ) { - self.inner.cache.dns_ipv4.insert_with_expiry( - name.into_fqdn().into_owned(), - Arc::new(value), - valid_until, - ); - } - - fn dnsbl_add(&self, name: &str, value: Vec, valid_until: std::time::Instant) { - self.inner.cache.dns_rbl.insert_with_expiry( - name.to_string(), - Some(Arc::new(IpResolver::new( - value - .iter() - .copied() - .next() - .unwrap_or(Ipv4Addr::BROADCAST) - .into(), - ))), - valid_until, - ); - } - - fn ipv6_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: Vec, - valid_until: std::time::Instant, - ) { - self.inner.cache.dns_ipv6.insert_with_expiry( - name.into_fqdn().into_owned(), - Arc::new(value), - valid_until, - ); - } - - fn ptr_add(&self, name: IpAddr, value: Vec, valid_until: std::time::Instant) { - self.inner - .cache - .dns_ptr - .insert_with_expiry(name, Arc::new(value), valid_until); - } - - fn mx_add<'x>(&self, name: impl IntoFqdn<'x>, value: Vec, valid_until: std::time::Instant) { - self.inner.cache.dns_mx.insert_with_expiry( - name.into_fqdn().into_owned(), - Arc::new(value), - valid_until, - ); - } - - fn tlsa_add<'x>( - &self, - name: impl IntoFqdn<'x>, - value: Arc, - valid_until: std::time::Instant, - ) { - self.inner.cache.dns_tlsa.insert_with_expiry( - name.into_fqdn().into_owned(), - value, - valid_until, - ); - } -} diff --git a/tests/src/system/antispam.rs b/tests/src/system/antispam.rs index 95badd76..dc2a5892 100644 --- a/tests/src/system/antispam.rs +++ b/tests/src/system/antispam.rs @@ -262,7 +262,7 @@ pub async fn test(test: &mut TestServer) { admin.destroy_account(account).await; admin.destroy_account(other_account).await; - test.wait_for_tasks().await; + test.cleanup().await; } pub const SPAM: [&str; 10] = [ diff --git a/tests/src/system/archiving.rs b/tests/src/system/archiving.rs index 7b960c34..cdb90a64 100644 --- a/tests/src/system/archiving.rs +++ b/tests/src/system/archiving.rs @@ -15,21 +15,19 @@ use crate::utils::{ }; use imap_proto::ResponseType; use jmap_proto::error::set::SetErrorType; -use registry::{ - schema::{ - enums::{AccountType, ArchivedItemStatus, TaskStoreMaintenanceType}, - prelude::{ObjectType, Property}, - structs::{ - Account, ArchivedItem, Credential, DataRetention, PasswordCredential, Task, TaskStatus, - TaskStoreMaintenance, - }, +use registry::schema::{ + enums::{AccountType, ArchivedItemStatus, TaskStoreMaintenanceType}, + prelude::{ObjectType, Property}, + structs::{ + Account, Action, ArchivedItem, DataRetention, Task, TaskStatus, TaskStoreMaintenance, }, - types::list::List, }; use serde_json::json; use types::id::Id; pub async fn test(test: &mut TestServer) { + println!("Running Archiving tests..."); + // Add test settings let admin = test.account("admin@example.org"); admin @@ -48,7 +46,7 @@ pub async fn test(test: &mut TestServer) { admin.reload_settings().await; // Create test account - let john = test + let mut john = test .create_user_account( "admin@example.org", "jdoe@example.org", @@ -319,25 +317,37 @@ pub async fn test(test: &mut TestServer) { ObjectType::Account, john.id(), json!({ - Property::Credentials: List::from_iter([Credential::Password(PasswordCredential { - secret: "brand new secret".to_string(), - ..Default::default() - })]), + "credentials/0": { + Property::Type: "Password", + Property::Secret: "brand new secret" + } }), ) .await; + // Reset cache + admin.registry_create_object(Action::InvalidateCaches).await; + // Authenticate with the new password and fetch the message again let mut john_imap = ImapConnection::connect(b"_x ").await; john_imap .authenticate("jdoe@example.org", "brand new secret") .await; + john_imap.send("SELECT INBOX").await; + john_imap.assert_read(Type::Tagged, ResponseType::Ok).await; john_imap.send("FETCH 1 BODY[]").await; john_imap .assert_read(Type::Tagged, ResponseType::Ok) .await .assert_contains(&format!("Subject: undelete test for {}", john.name())); + // Delete spam samples + john.update_secret("brand new secret"); + john.registry_destroy_all(ObjectType::SpamTrainingSample) + .await; + jane.registry_destroy_all(ObjectType::SpamTrainingSample) + .await; + // Restore settings admin .registry_update_setting( @@ -354,7 +364,7 @@ pub async fn test(test: &mut TestServer) { admin.destroy_account(john).await; admin.destroy_account(jane).await; - test.assert_is_empty().await; + test.cleanup().await; } const RAW_MESSAGE: &str = "From: NAME@example.org diff --git a/tests/src/system/authentication.rs b/tests/src/system/authentication.rs index 7d681ac1..01797011 100644 --- a/tests/src/system/authentication.rs +++ b/tests/src/system/authentication.rs @@ -22,6 +22,8 @@ use std::str::FromStr; use store::write::now; pub async fn test(test: &TestServer) { + println!("Running Authentication tests..."); + let admin = test.account("admin@example.org"); let domain_id = admin.find_or_create_domain("example.org").await; @@ -392,7 +394,7 @@ pub async fn test(test: &TestServer) { .await; admin.reload_settings().await; - test.assert_is_empty().await; + test.cleanup().await; } pub async fn validate_password(username: &str, password: &str, is_valid: bool) { diff --git a/tests/src/system/authorization.rs b/tests/src/system/authorization.rs index 975724e3..c1f6d9ca 100644 --- a/tests/src/system/authorization.rs +++ b/tests/src/system/authorization.rs @@ -24,7 +24,7 @@ use std::str::FromStr; use types::id::Id; pub async fn test(test: &mut TestServer) { - println!("Running authorization tests..."); + println!("Running Authorization tests..."); let admin = test.account("admin@example.org"); let domain_id = admin.find_or_create_domain("example.org").await; @@ -358,8 +358,10 @@ pub async fn test(test: &mut TestServer) { .assert_destroyed(&[*id]); } } + admin.destroy_account(john).await; + admin.destroy_account(jane).await; - test.assert_is_empty().await; + test.cleanup().await; } const SMIME_CERTIFICATE: &str = "-----BEGIN CERTIFICATE----- diff --git a/tests/src/system/crypto.rs b/tests/src/system/crypto.rs index ca49b921..a4790622 100644 --- a/tests/src/system/crypto.rs +++ b/tests/src/system/crypto.rs @@ -200,7 +200,7 @@ pub async fn test(test: &mut TestServer) { test.account("admin@example.org") .destroy_account(account) .await; - test.assert_is_empty().await; + test.cleanup().await; } pub async fn import_certs_and_encrypt() { diff --git a/tests/src/system/delivery.rs b/tests/src/system/delivery.rs index 363683a9..6d47a934 100644 --- a/tests/src/system/delivery.rs +++ b/tests/src/system/delivery.rs @@ -43,7 +43,7 @@ use types::{ use utils::chained_bytes::ChainedBytes; pub async fn test(test: &mut TestServer) { - println!("Running message delivery tests..."); + println!("Running Email delivery tests..."); let admin = test.account("admin@example.org"); // Prepare tests @@ -594,6 +594,8 @@ END:VCARD for account in [john, jane, bill] { admin.destroy_account(account).await; } + + test.cleanup().await; } impl Account { diff --git a/tests/src/system/directory.rs b/tests/src/system/directory.rs index 1525a405..cb273098 100644 --- a/tests/src/system/directory.rs +++ b/tests/src/system/directory.rs @@ -26,6 +26,7 @@ use std::sync::Arc; use utils::map::vec_map::VecMap; pub async fn test(test: &TestServer) { + println!("Running Directory tests..."); let account = test.account("admin@example.org"); // Create a domain and make sure it's in the cache @@ -534,5 +535,5 @@ pub async fn test(test: &TestServer) { .is_none() ); - test.assert_is_empty().await; + test.cleanup().await; } diff --git a/tests/src/system/mod.rs b/tests/src/system/mod.rs index be1cc52f..e9275e4d 100644 --- a/tests/src/system/mod.rs +++ b/tests/src/system/mod.rs @@ -15,6 +15,7 @@ pub mod oidc; pub mod purge; pub mod quota; pub mod security; +pub mod task; pub mod tenant; use crate::utils::server::TestServerBuilder; @@ -56,18 +57,21 @@ pub async fn system_tests() { .await; test.insert_account(admin); - let todo = "test tasks retries and other types"; - - //directory::test(&test).await; - //authentication::test(&test).await; - //oidc::test(&mut test).await; - //authorization::test(&mut test).await; - //tenant::test(&mut test).await; - //security::test(&mut test).await; - //quota::test(&mut test).await; - //purge::test(&mut test).await; - //delivery::test(&mut test).await; - //crypto::test(&mut test).await; - //antispam::test(&mut test).await; + directory::test(&test).await; + authentication::test(&test).await; + oidc::test(&mut test).await; + authorization::test(&mut test).await; + tenant::test(&mut test).await; + security::test(&mut test).await; + quota::test(&mut test).await; + purge::test(&mut test).await; + delivery::test(&mut test).await; + crypto::test(&mut test).await; + antispam::test(&mut test).await; archiving::test(&mut test).await; + task::test(&mut test).await; + + if test.is_reset() { + test.temp_dir.delete(); + } } diff --git a/tests/src/system/oidc.rs b/tests/src/system/oidc.rs index eced61a4..e32ad71d 100644 --- a/tests/src/system/oidc.rs +++ b/tests/src/system/oidc.rs @@ -429,7 +429,7 @@ pub async fn test(test: &mut TestServer) { // Clean up admin.registry_destroy_all(ObjectType::OAuthClient).await; admin.destroy_account(user).await; - test.assert_is_empty().await; + test.cleanup().await; } async fn post_bytes( diff --git a/tests/src/system/purge.rs b/tests/src/system/purge.rs index 150a761c..c1d30206 100644 --- a/tests/src/system/purge.rs +++ b/tests/src/system/purge.rs @@ -27,7 +27,7 @@ use store::{IterateParams, LogKey, U32_LEN, U64_LEN, write::key::DeserializeBigE use types::id::Id; pub async fn test(test: &mut TestServer) { - println!("Running purge tests..."); + println!("Running Account purge tests..."); let inbox_id = Id::from(INBOX_ID).to_string(); let trash_id = Id::from(TRASH_ID).to_string(); let junk_id = Id::from(JUNK_ID).to_string(); @@ -210,6 +210,7 @@ pub async fn test(test: &mut TestServer) { admin .registry_update_setting(SpamClassifier::default(), &[Property::HoldSamplesFor]) .await; + test.cleanup().await; } async fn get_changes(server: &Server) -> (AHashSet<(u64, u8)>, bool) { diff --git a/tests/src/system/quota.rs b/tests/src/system/quota.rs index eb0d045d..a32d3c4a 100644 --- a/tests/src/system/quota.rs +++ b/tests/src/system/quota.rs @@ -27,7 +27,7 @@ use types::id::Id; use utils::map::vec_map::VecMap; pub async fn test(test: &mut TestServer) { - println!("Running quota tests..."); + println!("Running Quota tests..."); let admin = test.account("admin@example.org"); let domain_id = admin.find_or_create_domain("example.org").await; @@ -417,6 +417,7 @@ pub async fn test(test: &mut TestServer) { .registry_destroy(ObjectType::Account, [account_id, other_account_id]) .await .assert_destroyed(&[account_id, other_account_id]); + test.cleanup().await; } fn assert_over_quota(result: Result) { diff --git a/tests/src/system/security.rs b/tests/src/system/security.rs index 25937939..0cfb3607 100644 --- a/tests/src/system/security.rs +++ b/tests/src/system/security.rs @@ -321,5 +321,5 @@ pub async fn test(test: &mut TestServer) { // Destroy account admin.destroy_account(user).await; - test.assert_is_empty().await; + test.cleanup().await; } diff --git a/tests/src/system/task.rs b/tests/src/system/task.rs new file mode 100644 index 00000000..7ef7f920 --- /dev/null +++ b/tests/src/system/task.rs @@ -0,0 +1,223 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::utils::{account::Account, server::TestServer}; +use registry::schema::{ + enums::TaskStoreMaintenanceType, + prelude::{ObjectType, Property}, + structs::{ + Task, TaskManager, TaskRetryStrategy, TaskRetryStrategyFixed, TaskStatus, TaskStatusFailed, + TaskStatusPending, TaskStatusRetry, TaskStoreMaintenance, + }, +}; +use serde_json::json; +use store::write::now; +use types::id::Id; + +const TASK_SUCCESS: u64 = 0; +const TASK_TEMP_FAIL: u64 = 1; +const TASK_PERM_FAIL: u64 = 2; + +pub async fn test(test: &mut TestServer) { + println!("Running Task manager tests..."); + let admin = test.account("admin@example.org"); + + // Make sure there are no existing tasks + admin.assert_no_tasks().await; + + // Create a successful task for immediate execution + admin.schedule_test_task(TASK_SUCCESS, 0).await; + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + admin.assert_no_tasks().await; + + // Create a successful task for future execution + admin.schedule_test_task(TASK_SUCCESS, 1).await; + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + admin.assert_has_tasks(1).await; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + admin.assert_no_tasks().await; + + // Create a permanent failure task for immediate execution + admin.schedule_test_task(TASK_PERM_FAIL, 0).await; + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let task = admin.assert_has_tasks(1).await.into_iter().next().unwrap(); + assert_eq!( + task.task.status().unwrap_failed().failure_reason, + "Simulated permanent failure" + ); + + // Reschedule the failed task for retry + admin + .registry_update_object( + ObjectType::Task, + task.id, + json!({ + Property::ShardIndex: TASK_SUCCESS, + Property::Status: TaskStatus::at((now() + 1) as i64), + }), + ) + .await; + test.wait_for_tasks().await; + admin.assert_no_tasks().await; + + // Test attempt limits strategy + admin + .registry_update_setting( + TaskManager { + max_attempts: 3, + strategy: TaskRetryStrategy::FixedDelay(TaskRetryStrategyFixed { + delay: 1_000u64.into(), + }), + total_deadline: 86_400_000u64.into(), // 24 hours + }, + &[], + ) + .await; + admin.reload_settings().await; + + // Create a temporary failure task for immediate execution + admin.schedule_test_task(TASK_TEMP_FAIL, 0).await; + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let task = admin.assert_has_tasks(1).await.into_iter().next().unwrap(); + let task_status = task.task.status().unwrap_retry(); + assert_eq!(task_status.failure_reason, "Simulated temporary failure"); + assert_eq!(task_status.attempt_number, 1); + + // Wait until the max attempts is reached + test.wait_for_tasks_skip_failures().await; + let task = admin.assert_has_tasks(1).await.into_iter().next().unwrap(); + let task_status = task.task.status().unwrap_failed(); + assert_eq!(task_status.failure_reason, "Simulated temporary failure"); + assert_eq!(task_status.failed_attempt_number, 3); + admin + .registry_destroy(ObjectType::Task, [task.id]) + .await + .assert_destroyed(&[task.id]); + + // Test attempt limits strategy + admin + .registry_update_setting( + TaskManager { + max_attempts: 100, + strategy: TaskRetryStrategy::FixedDelay(TaskRetryStrategyFixed { + delay: 1_000u64.into(), + }), + total_deadline: 2_000u64.into(), // 2 seconds + }, + &[], + ) + .await; + admin.reload_settings().await; + + // Create a temporary failure task for immediate execution + admin.schedule_test_task(TASK_TEMP_FAIL, 0).await; + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let task = admin.assert_has_tasks(1).await.into_iter().next().unwrap(); + let task_status = task.task.status().unwrap_retry(); + assert_eq!(task_status.failure_reason, "Simulated temporary failure"); + assert_eq!(task_status.attempt_number, 1); + + // Wait until 2 seconds deadline is reached + test.wait_for_tasks_skip_failures().await; + let task = admin.assert_has_tasks(1).await.into_iter().next().unwrap(); + let task_status = task.task.status().unwrap_failed(); + assert_eq!(task_status.failure_reason, "Simulated temporary failure"); + assert_eq!(task_status.failed_attempt_number, 2); + admin + .registry_destroy(ObjectType::Task, [task.id]) + .await + .assert_destroyed(&[task.id]); + + test.cleanup().await; +} + +impl Account { + async fn schedule_test_task(&self, test_type: u64, schedule_in: u64) -> Id { + self.registry_create_object(Task::StoreMaintenance(TaskStoreMaintenance { + maintenance_type: TaskStoreMaintenanceType::RemoveLockDav, + shard_index: Some(test_type), + status: TaskStatus::at((now() + schedule_in) as i64), + })) + .await + } + + pub async fn task_ids(&self) -> Vec { + self.registry_query_ids( + ObjectType::Task, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + } + + pub async fn tasks(&self) -> Vec { + let ids = self.task_ids().await; + let mut results = Vec::with_capacity(ids.len()); + for id in ids { + let sample = self.registry_get::(id).await; + results.push(TaskId { id, task: sample }); + } + results + } + + async fn assert_no_tasks(&self) { + let tasks = self.tasks().await; + assert!( + tasks.is_empty(), + "Expected no tasks, found {}: {:?}", + tasks.len(), + tasks + ); + } + + async fn assert_has_tasks(&self, count: usize) -> Vec { + let tasks = self.tasks().await; + assert!( + tasks.len() == count, + "Expected {} tasks, found {}: {:?}", + count, + tasks.len(), + tasks + ); + tasks + } +} + +#[derive(Debug)] +pub struct TaskId { + pub id: Id, + pub task: Task, +} + +#[allow(dead_code)] +trait UnwrapTaskStatus { + fn unwrap_pending(&self) -> &TaskStatusPending; + fn unwrap_retry(&self) -> &TaskStatusRetry; + fn unwrap_failed(&self) -> &TaskStatusFailed; +} + +impl UnwrapTaskStatus for TaskStatus { + fn unwrap_pending(&self) -> &TaskStatusPending { + match self { + TaskStatus::Pending(status) => status, + _ => panic!("Expected TaskStatus::Pending, found {:?}", self), + } + } + + fn unwrap_retry(&self) -> &TaskStatusRetry { + match self { + TaskStatus::Retry(status) => status, + _ => panic!("Expected TaskStatus::Retry, found {:?}", self), + } + } + + fn unwrap_failed(&self) -> &TaskStatusFailed { + match self { + TaskStatus::Failed(status) => status, + _ => panic!("Expected TaskStatus::Failed, found {:?}", self), + } + } +} diff --git a/tests/src/system/tenant.rs b/tests/src/system/tenant.rs index 0c886b37..12067764 100644 --- a/tests/src/system/tenant.rs +++ b/tests/src/system/tenant.rs @@ -31,7 +31,7 @@ use types::id::Id; use utils::map::vec_map::VecMap; pub async fn test(test: &mut TestServer) { - println!("Running multi-tenancy tests..."); + println!("Running Multi-tenancy tests..."); let admin_system = test.account("admin@example.org"); // Create tenants @@ -620,7 +620,8 @@ pub async fn test(test: &mut TestServer) { ); } } - test.assert_is_empty().await; + + test.cleanup().await; } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] diff --git a/tests/src/telemetry/alerts.rs b/tests/src/telemetry/alerts.rs new file mode 100644 index 00000000..446a5f55 --- /dev/null +++ b/tests/src/telemetry/alerts.rs @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::utils::server::TestServer; +use common::BuildServer; +use registry::{ + schema::{ + prelude::ObjectType, + structs::{ + Alert, AlertEmail, AlertEmailProperties, AlertEvent, AlertEventProperties, Expression, + }, + }, + types::map::Map, +}; +use trc::{ClusterEvent, Collector, EventType, MetricType}; + +pub async fn test(test: &TestServer) { + println!("Running Alerts tests..."); + + // Create alerts + let admin = test.account("admin@example.org"); + admin + .registry_create_object(Alert { + enable: true, + condition: Expression { + else_: "metric('domain.count') > 1 && metric('cluster.publisher-error') > 3".into(), + ..Default::default() + }, + email_alert: AlertEmail::Enabled(AlertEmailProperties { + body: concat!( + "Sorry for the bad news, but we found %{domain.count}% ", + "domains and %{cluster.publisher-error}% cluster errors." + ) + .to_string(), + from_address: "alert@example.com".to_string(), + from_name: "Alert Subsystem".to_string().into(), + subject: "Found %{cluster.publisher-error}% cluster errors".to_string(), + to: Map::new(vec!["jdoe@example.com".to_string()]), + }), + event_alert: AlertEvent::Enabled(AlertEventProperties { + event_message: "Yikes! Found %{cluster.publisher-error}% cluster errors!" + .to_string() + .into(), + }), + }) + .await; + admin + .registry_create_object(Alert { + enable: true, + condition: Expression { + else_: "metric('domain.count') < 1 || metric('cluster.publisher-error') < 3".into(), + ..Default::default() + }, + email_alert: AlertEmail::Disabled, + event_alert: AlertEvent::Enabled(AlertEventProperties { + event_message: "this should not have happened".to_string().into(), + }), + }) + .await; + admin.reload_settings().await; + + // Make sure the required metrics are set to 0 + assert_eq!( + Collector::read_metric(MetricType::ClusterPublisherError), + 0.0 + ); + assert_eq!(Collector::read_metric(MetricType::DomainCount), 1.0); + assert_eq!(Collector::read_metric(MetricType::TelemetryAlertEvent), 0.0); + + // Increment metrics to trigger alerts + Collector::update_event_counter(EventType::Cluster(ClusterEvent::PublisherError), 5); + Collector::update_gauge(MetricType::DomainCount, 3); + + // Make sure the values were set + assert_eq!( + Collector::read_metric(MetricType::ClusterPublisherError), + 5.0 + ); + assert_eq!(Collector::read_metric(MetricType::DomainCount), 3.0); + + // Process alerts + let message = test + .server + .inner + .build_server() + .process_alerts() + .await + .unwrap() + .pop() + .unwrap(); + assert_eq!(message.from, "alert@example.com"); + assert_eq!(message.to, vec!["jdoe@example.com".to_string()]); + let body = String::from_utf8(message.body).unwrap(); + assert!( + body.contains("Sorry for the bad news, but we found 3 domains and 5 cluster errors."), + "{body:?}" + ); + assert!(body.contains("Subject: Found 5 cluster errors"), "{body:?}"); + assert!( + body.contains("From: \"Alert Subsystem\" "), + "{body:?}" + ); + assert!(body.contains("To: "), "{body:?}"); + + // Make sure the event was triggered + assert_eq!(Collector::read_metric(MetricType::TelemetryAlertEvent), 1.0); + + // Cleanup + admin.registry_destroy_all(ObjectType::Alert).await; + admin.reload_settings().await; + test.cleanup().await; +} diff --git a/tests/src/telemetry/metrics.rs b/tests/src/telemetry/metrics.rs new file mode 100644 index 00000000..32222965 --- /dev/null +++ b/tests/src/telemetry/metrics.rs @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::utils::server::TestServer; +use common::telemetry::metrics::store::{MetricsStore, SharedMetricHistory}; +use registry::{schema::prelude::ObjectType, types::datetime::UTCDateTime}; +use std::time::Duration; +use store::{ + rand::{self, Rng}, + write::now, +}; +use trc::*; +use types::id::Id; + +pub async fn test(test: &TestServer) { + println!("Running Metrics tests..."); + + // Make sure there are no span entries in the db + let admin = test.account("admin@example.org"); + assert_eq!( + admin + .registry_query( + ObjectType::Metric, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(), + Vec::::new() + ); + + // Insert test metrics + insert_test_metrics(test).await; + + // Fetch all metrics + let metric_ids = admin + .registry_query( + ObjectType::Metric, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(); + let response = admin + .registry_get_many(ObjectType::Metric, Vec::<&str>::new()) + .await; + let metrics = response.list(); + assert!( + metrics.len() > 2000, + "Found {} metrics, expected more than 2000", + metrics.len() + ); + assert_eq!(metrics.len(), metric_ids.len()); + + // Fetch the last 48 hours of metrics + let metric_ids = admin + .registry_query( + ObjectType::Metric, + [( + "timestampIsGreaterThan", + UTCDateTime::from_timestamp((now() - (2 * 86400)) as i64).to_string(), + )], + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(); + assert!( + metric_ids.len() > 20 && metric_ids.len() < 2000, + "Found {} metrics, expected more than 20 and less than 2000", + metric_ids.len() + ); + + // Purge metrics and make sure they are gone + test.server + .metrics_store() + .purge_metrics(Duration::from_secs(0)) + .await + .unwrap(); + assert_eq!( + admin + .registry_query( + ObjectType::Metric, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(), + Vec::::new() + ); +} + +async fn insert_test_metrics(test: &TestServer) { + test.server + .metrics_store() + .purge_metrics(Duration::from_secs(0)) + .await + .unwrap(); + let mut start_time = now() - (90 * 24 * 60 * 60); + let timestamp = now(); + let history = SharedMetricHistory::default(); + + while start_time < timestamp { + for event_type in [ + EventType::Smtp(SmtpEvent::ConnectionStart), + EventType::Imap(ImapEvent::ConnectionStart), + EventType::Pop3(Pop3Event::ConnectionStart), + EventType::ManageSieve(ManageSieveEvent::ConnectionStart), + EventType::Http(HttpEvent::ConnectionStart), + EventType::Delivery(DeliveryEvent::AttemptStart), + EventType::Queue(QueueEvent::MessageQueued), + EventType::Queue(QueueEvent::AuthenticatedMessageQueued), + EventType::Queue(QueueEvent::DsnQueued), + EventType::Queue(QueueEvent::ReportQueued), + EventType::MessageIngest(MessageIngestEvent::Ham), + EventType::MessageIngest(MessageIngestEvent::Spam), + EventType::Auth(AuthEvent::Failed), + EventType::Security(SecurityEvent::AuthenticationBan), + EventType::Security(SecurityEvent::ScanBan), + EventType::Security(SecurityEvent::AbuseBan), + EventType::Security(SecurityEvent::LoiterBan), + EventType::Security(SecurityEvent::IpBlocked), + EventType::IncomingReport(IncomingReportEvent::DmarcReport), + EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings), + EventType::IncomingReport(IncomingReportEvent::TlsReport), + EventType::IncomingReport(IncomingReportEvent::TlsReportWithWarnings), + ] { + // Generate a random value between 0 and 100 + Collector::update_event_counter(event_type, rand::rng().random_range(0..=100)) + } + + Collector::update_gauge(MetricType::QueueCount, rand::rng().random_range(0..=1000)); + Collector::update_gauge( + MetricType::ServerMemory, + rand::rng().random_range(100 * 1024 * 1024..=300 * 1024 * 1024), + ); + + for metric_type in [ + MetricType::MessageIngestTime, + MetricType::MessageIngestIndexTime, + MetricType::DeliveryTotalTime, + MetricType::DnsLookupTime, + ] { + Collector::update_histogram(metric_type, rand::rng().random_range(2..=1000)) + } + Collector::update_histogram( + MetricType::DeliveryTotalTime, + rand::rng().random_range(1000..=5000), + ); + + test.server + .metrics_store() + .write_metrics(start_time.into(), history.clone()) + .await + .unwrap(); + start_time += 60 * 60 * 24; + } +} diff --git a/tests/src/telemetry/mod.rs b/tests/src/telemetry/mod.rs new file mode 100644 index 00000000..e505fd39 --- /dev/null +++ b/tests/src/telemetry/mod.rs @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod alerts; +pub mod metrics; +pub mod tracing; +pub mod webhooks; + +use crate::utils::server::TestServerBuilder; +use registry::schema::structs::{Expression, Jmap, MetricsStore, MtaStageAuth, TracingStore}; + +#[tokio::test(flavor = "multi_thread")] +pub async fn telemetry_tests() { + let mut test = TestServerBuilder::new("telemetry_tests") + .await + .with_logging() + .with_default_listeners() + .await + .with_object(MetricsStore::Default) + .await + .with_object(TracingStore::Default) + .await + .with_object(Jmap { + get_max_results: 100_000, + query_max_results: 100_000, + ..Default::default() + }) + .await + .with_object(MtaStageAuth { + require: Expression { + else_: "false".to_string(), + ..Default::default() + }, + ..Default::default() + }) + .await + .build() + .await; + + // Create admin account + let admin = test + .create_user_account( + "admin", + "admin@example.org", + "these_pretzels_are_making_me_thirsty", + &[], + ) + .await; + test.account("admin") + .assign_roles_to_account(admin.id(), &["user", "system"]) + .await; + test.insert_account(admin); + + alerts::test(&test).await; + metrics::test(&test).await; + tracing::test(&test).await; + webhooks::test(&test).await; + + if test.is_reset() { + test.temp_dir.delete(); + } +} diff --git a/tests/src/telemetry/tracing.rs b/tests/src/telemetry/tracing.rs new file mode 100644 index 00000000..92a38242 --- /dev/null +++ b/tests/src/telemetry/tracing.rs @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::utils::{server::TestServer, smtp::SmtpConnection}; +use common::telemetry::tracers::store::TracingStore; +use registry::schema::{ + prelude::{ObjectType, Property}, + structs::Trace, +}; +use std::time::Duration; +use trc::{DeliveryEvent, EventType, SmtpEvent}; +use types::id::Id; + +pub async fn test(test: &TestServer) { + println!("Running Tracing tests..."); + + // Create test accounts + let admin = test.account("admin@example.org"); + let account = test + .create_user_account( + "admin@example.org", + "jdoe@example.org", + "this is a very strong password", + &[], + ) + .await; + + // Make sure there are no span entries in the db + test.server + .tracing_store() + .purge_spans(Duration::from_secs(0), test.server.search_store().into()) + .await + .unwrap(); + assert_eq!( + admin + .registry_query( + ObjectType::Trace, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(), + Vec::::new() + ); + + // Send an email + let mut lmtp = SmtpConnection::connect().await; + lmtp.ingest( + "bill@example.org", + &["jdoe@example.org"], + concat!( + "From: bill@example.org\r\n", + "To: jdoe@example.org\r\n", + "Subject: TPS Report\r\n", + "X-Spam-Status: No\r\n", + "\r\n", + "I'm going to need those TPS reports ASAP. ", + "So, if you could do that, that'd be great." + ), + ) + .await; + lmtp.quit().await; + tokio::time::sleep(Duration::from_millis(300)).await; + test.server.notify_task_queue(); + test.wait_for_tasks().await; + + // There should be 2 spans + assert_eq!( + admin + .registry_query( + ObjectType::Trace, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .count(), + 2 + ); + + // Purge should not delete anything at this point + test.server + .tracing_store() + .purge_spans(Duration::from_secs(2), test.server.search_store().into()) + .await + .unwrap(); + + // There should be 2 spans + assert_eq!( + admin + .registry_query( + ObjectType::Trace, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .count(), + 2 + ); + + // Search by spam type + for span_type in [ + EventType::Delivery(DeliveryEvent::AttemptStart), + EventType::Smtp(SmtpEvent::ConnectionStart), + ] { + let span_ids = admin + .registry_query( + ObjectType::Trace, + [(Property::Event, span_type.as_str())], + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(); + + assert_eq!(span_ids.len(), 1, "{span_type:?}"); + let trace = admin.registry_get::(span_ids[0]).await; + + assert_eq!(trace.events.iter().next().unwrap().event, span_type); + } + + // Try searching + for keyword in ["bill@example.org", "jdoe@example.org", "example.org"] { + let span_ids = admin + .registry_query( + ObjectType::Trace, + [(Property::Text, keyword)], + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(); + + assert_eq!(span_ids.len(), 2, "keyword: {keyword}"); + + let trace_1 = admin.registry_get::(span_ids[0]).await; + let trace_2 = admin.registry_get::(span_ids[1]).await; + + assert!(trace_1 != trace_2, "keyword: {keyword}"); + } + + // Purge should delete the span entries + tokio::time::sleep(Duration::from_millis(800)).await; + test.server + .tracing_store() + .purge_spans(Duration::from_secs(1), test.server.search_store().into()) + .await + .unwrap(); + + assert_eq!( + admin + .registry_query( + ObjectType::Trace, + Vec::<(&str, &str)>::new(), + Vec::<&str>::new(), + ) + .await + .object_ids() + .collect::>(), + Vec::::new() + ); + + admin.destroy_account(account).await; + test.cleanup().await; +} diff --git a/tests/src/jmap/server/webhooks.rs b/tests/src/telemetry/webhooks.rs similarity index 62% rename from tests/src/jmap/server/webhooks.rs rename to tests/src/telemetry/webhooks.rs index 25d50137..c7f716f0 100644 --- a/tests/src/jmap/server/webhooks.rs +++ b/tests/src/telemetry/webhooks.rs @@ -4,6 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::utils::server::TestServer; +use crate::utils::smtp::SmtpConnection; +use base64::{Engine, engine::general_purpose::STANDARD}; +use common::{manager::application::Resource, telemetry::tracers::store::TracingStore}; +use http_proto::{ToHttpResponse, request::fetch_body}; +use hyper::{body, server::conn::http1, service::service_fn}; +use hyper_util::rt::TokioIo; +use jmap::api::ToJmapHttpResponse; +use jmap_proto::error::request::RequestError; +use registry::{ + schema::{ + enums::EventPolicy, + prelude::ObjectType, + structs::{SecretKeyOptional, SecretKeyValue, WebHook}, + }, + types::map::Map, +}; +use ring::hmac; use std::{ sync::{ Arc, @@ -11,38 +29,100 @@ use std::{ }, time::Duration, }; - -use crate::jmap::JMAPTest; -use base64::{Engine, engine::general_purpose::STANDARD}; -use common::manager::application::Resource; -use http_proto::{ToHttpResponse, request::fetch_body}; -use hyper::{body, server::conn::http1, service::service_fn}; -use hyper_util::rt::TokioIo; -use jmap::api::ToJmapHttpResponse; -use jmap_proto::error::request::RequestError; -use ring::hmac; use store::parking_lot::Mutex; use tokio::{net::TcpListener, sync::watch}; +use trc::EventType; -pub struct MockWebhookEndpoint { - pub tx: watch::Sender, +struct MockWebhookEndpoint { + pub _tx: watch::Sender, pub events: Mutex>, pub reject: AtomicBool, } -pub async fn test(test: &mut TestServer) { - println!("Running Webhook tests..."); +pub async fn test(test: &TestServer) { + println!("Running Webhooks tests..."); - // Webhooks endpoint starts disabled by default, make sure there are no events. + // Spawn mock webhook endpoint + let webhook = spawn_mock_webhook_endpoint(); + + // Add telemetry webhook + let admin = test.account("admin@example.org"); + admin + .registry_create_object(WebHook { + enable: true, + url: "http://127.0.0.1:8821/hook".into(), + signature_key: SecretKeyOptional::Value(SecretKeyValue { + secret: "ovos-moles".into(), + }), + throttle: 100u64.into(), + allow_invalid_certs: true, + events: Map::new( + EventType::variants() + .iter() + .filter(|ev| { + let ev = ev.as_str(); + ev.starts_with("smtp.connection-") + || ev.starts_with("delivery.dsn") + || ev.starts_with("message-ingest.") + }) + .copied() + .collect(), + ), + events_policy: EventPolicy::Include, + ..Default::default() + }) + .await; + admin.reload_settings().await; + + // Send test email + let john = test + .create_user_account( + "admin@example.org", + "jdoe@example.org", + "this is a very strong password", + &["john.doe@example.org"], + ) + .await; + let mut lmtp = SmtpConnection::connect().await; + lmtp.ingest( + "bill@example.org", + &["jdoe@example.org"], + concat!( + "From: bill@example.org\r\n", + "To: jdoe@example.org\r\n", + "Subject: TPS Report\r\n", + "\r\n", + "I'm going to need those TPS reports ASAP. ", + "So, if you could do that, that'd be great." + ), + ) + .await; + test.wait_for_tasks().await; + + // Enable the webhook + webhook.assert_is_empty(); + webhook.accept(); tokio::time::sleep(Duration::from_millis(200)).await; - params.webhook.assert_is_empty(); - - // Enable the endpoint - params.webhook.accept(); - tokio::time::sleep(Duration::from_millis(1000)).await; // Check for events - params.webhook.assert_contains(&["auth.success"]); + webhook.assert_contains(&[ + "smtp.connection-start", + "message-ingest.", + "delivery.dsn", + "\"from\": \"bill@example.org\"", + "\"jdoe@example.org\"", + ]); + + // Cleanup + admin.registry_destroy_all(ObjectType::WebHook).await; + admin.reload_settings().await; + admin.destroy_account(john).await; + test.server + .tracing_store() + .purge_spans(Duration::from_secs(0), test.server.search_store().into()) + .await + .unwrap(); + test.cleanup().await; } impl MockWebhookEndpoint { @@ -65,23 +145,23 @@ impl MockWebhookEndpoint { self.reject.store(false, Ordering::Relaxed); } - pub fn reject(&self) { + /*pub fn reject(&self) { self.reject.store(true, Ordering::Relaxed); } pub fn clear(&self) { self.events.lock().clear(); - } + }*/ pub fn assert_is_empty(&self) { assert!(self.events.lock().is_empty()); } } -pub fn spawn_mock_webhook_endpoint() -> Arc { - let (tx, rx) = watch::channel(true); +fn spawn_mock_webhook_endpoint() -> Arc { + let (_tx, rx) = watch::channel(true); let endpoint_ = Arc::new(MockWebhookEndpoint { - tx, + _tx, events: Mutex::new(vec![]), reject: true.into(), }); @@ -153,7 +233,6 @@ pub fn spawn_mock_webhook_endpoint() -> Arc { } }, _ = rx_.changed() => { - //println!("Mock jMilter server stopping"); break; } }; diff --git a/tests/src/utils/account.rs b/tests/src/utils/account.rs index 169cf11e..aa5e4724 100644 --- a/tests/src/utils/account.rs +++ b/tests/src/utils/account.rs @@ -9,10 +9,11 @@ use ahash::AHashMap; use jmap_client::client::{Client, Credentials}; use registry::{ schema::{ + enums::Permission, prelude::{ObjectType, Property}, structs::{ - self, Credential, CustomRoles, Domain, EmailAlias, PasswordCredential, Roles, - UserAccount, + self, Credential, CustomRoles, Domain, EmailAlias, GroupAccount, PasswordCredential, + Permissions, PermissionsList, Roles, UserAccount, }, }, types::{list::List, map::Map}, @@ -37,51 +38,9 @@ impl TestServer { secret: &'static str, aliases: &'static [&'static str], ) -> Account { - let mut domains = AHashMap::from_iter( - aliases - .iter() - .copied() - .chain([name].into_iter()) - .map(|email| { - let domain = email.split('@').nth(1).expect("Invalid email address"); - (domain, Id::singleton()) - }), - ); - let account = self.account(using_account); - for (name, id) in &mut domains { - *id = account.find_or_create_domain(name).await; - } - let (account_name, domain_id) = name - .rsplit_once('@') - .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) - .unwrap(); - let account_aliases = aliases.iter().map(|email| { - let (name, domain_id) = email - .rsplit_once('@') - .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) - .unwrap(); - EmailAlias { - name, - domain_id, - enabled: true, - ..Default::default() - } - }); - - let account_id = account - .registry_create_object(structs::Account::User(UserAccount { - name: account_name, - domain_id, - credentials: List::from_iter([Credential::Password(PasswordCredential { - secret: secret.to_string(), - ..Default::default() - })]), - aliases: List::from_iter(account_aliases), - ..Default::default() - })) - .await; - - Account::new(name, secret, aliases, account_id) + self.account(using_account) + .create_user_account(name, secret, None, aliases, vec![]) + .await } pub fn insert_account(&mut self, account: Account) { @@ -144,6 +103,114 @@ impl Account { } } + pub async fn create_user_account( + &self, + name: &'static str, + secret: &'static str, + description: Option<&'static str>, + aliases: &'static [&'static str], + extra_permissions: Vec, + ) -> Account { + let mut domains = AHashMap::from_iter( + aliases + .iter() + .copied() + .chain([name].into_iter()) + .map(|email| { + let domain = email.split('@').nth(1).expect("Invalid email address"); + (domain, Id::singleton()) + }), + ); + for (name, id) in &mut domains { + *id = self.find_or_create_domain(name).await; + } + let (account_name, domain_id) = name + .rsplit_once('@') + .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) + .unwrap(); + let account_aliases = aliases.iter().map(|email| { + let (name, domain_id) = email + .rsplit_once('@') + .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) + .unwrap(); + EmailAlias { + name, + domain_id, + enabled: true, + ..Default::default() + } + }); + + let account_id = self + .registry_create_object(structs::Account::User(UserAccount { + name: account_name, + domain_id, + credentials: List::from_iter([Credential::Password(PasswordCredential { + secret: secret.to_string(), + ..Default::default() + })]), + aliases: List::from_iter(account_aliases), + description: description.map(|d| d.to_string()), + permissions: Permissions::Merge(PermissionsList { + disabled_permissions: Default::default(), + enabled_permissions: Map::new(extra_permissions), + }), + ..Default::default() + })) + .await; + + Account::new(name, secret, aliases, account_id) + } + + pub async fn create_group_account( + &self, + name: &'static str, + description: Option<&'static str>, + aliases: &'static [&'static str], + ) -> Account { + let mut domains = AHashMap::from_iter( + aliases + .iter() + .copied() + .chain([name].into_iter()) + .map(|email| { + let domain = email.split('@').nth(1).expect("Invalid email address"); + (domain, Id::singleton()) + }), + ); + for (name, id) in &mut domains { + *id = self.find_or_create_domain(name).await; + } + let (account_name, domain_id) = name + .rsplit_once('@') + .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) + .unwrap(); + let account_aliases = aliases.iter().map(|email| { + let (name, domain_id) = email + .rsplit_once('@') + .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) + .unwrap(); + EmailAlias { + name, + domain_id, + enabled: true, + ..Default::default() + } + }); + + let account_id = self + .registry_create_object(structs::Account::Group(GroupAccount { + name: account_name, + domain_id, + aliases: List::from_iter(account_aliases), + description: description.map(|d| d.to_string()), + ..Default::default() + })) + .await; + + Account::new(name, "", aliases, account_id) + } + pub async fn create_domain(&self, name: &'static str) -> Id { self.registry_create_object(Domain { is_enabled: true, diff --git a/tests/src/utils/cleanup.rs b/tests/src/utils/cleanup.rs index 3e900840..05ff9a76 100644 --- a/tests/src/utils/cleanup.rs +++ b/tests/src/utils/cleanup.rs @@ -237,6 +237,7 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include let store = store.clone(); let mut failed = false; + let mut delete_batch = BatchBuilder::new(); for (subspace, with_values) in [ (SUBSPACE_ACL, true), @@ -287,6 +288,13 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include || key.len() == U16_LEN => { // Message ID, change ID counters and registry counters + if key.len() != U16_LEN { + // Keep registry counters, delete the rest + delete_batch.clear(ValueClass::Any(AnyClass { + subspace, + key: key.to_vec(), + })); + } return Ok(true); } SUBSPACE_INDEXES => { @@ -303,7 +311,7 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include key ); } - SUBSPACE_REGISTRY | SUBSPACE_DIRECTORY => { + SUBSPACE_REGISTRY | SUBSPACE_DIRECTORY | SUBSPACE_SPAM_SAMPLES => { let object_id = ObjectType::from_id(key.deserialize_be_u16(0).unwrap()).unwrap(); @@ -392,19 +400,9 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include .await .unwrap(); - store - .delete_range( - AnyKey { - subspace: SUBSPACE_COUNTER, - key: &[0u8], - }, - AnyKey { - subspace: SUBSPACE_COUNTER, - key: (u32::MAX / 2).to_be_bytes().as_slice(), - }, - ) - .await - .unwrap(); + if !delete_batch.is_empty() { + store.write(delete_batch.build_all()).await.unwrap(); + } if failed { panic!("Store is not empty."); diff --git a/tests/src/utils/dns.rs b/tests/src/utils/dns.rs new file mode 100644 index 00000000..c84d9279 --- /dev/null +++ b/tests/src/utils/dns.rs @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use common::{ + Server, + config::{mailstore::spamfilter::IpResolver, smtp::resolver::Tlsa}, +}; +use mail_auth::{MX, Txt, common::resolver::ToFqdn}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::Arc, +}; + +pub trait DnsCache { + fn txt_add(&self, name: impl ToFqdn, value: impl Into, valid_until: std::time::Instant); + fn ipv4_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant); + fn ipv6_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant); + fn dnsbl_add(&self, name: &str, value: Vec, valid_until: std::time::Instant); + fn ptr_add(&self, name: IpAddr, value: Vec, valid_until: std::time::Instant); + fn mx_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant); + fn tlsa_add(&self, name: impl ToFqdn, value: Arc, valid_until: std::time::Instant); +} + +impl DnsCache for Server { + fn txt_add(&self, name: impl ToFqdn, value: impl Into, valid_until: std::time::Instant) { + self.inner + .cache + .dns_txt + .insert_with_expiry(name.to_fqdn(), value.into(), valid_until); + } + + fn ipv4_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant) { + self.inner + .cache + .dns_ipv4 + .insert_with_expiry(name.to_fqdn(), Arc::from(value), valid_until); + } + + fn dnsbl_add(&self, name: &str, value: Vec, valid_until: std::time::Instant) { + self.inner.cache.dns_rbl.insert_with_expiry( + name.into(), + Some(Arc::new(IpResolver::new( + value + .iter() + .copied() + .next() + .unwrap_or(Ipv4Addr::BROADCAST) + .into(), + ))), + valid_until, + ); + } + + fn ipv6_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant) { + self.inner + .cache + .dns_ipv6 + .insert_with_expiry(name.to_fqdn(), Arc::from(value), valid_until); + } + + fn ptr_add(&self, name: IpAddr, value: Vec, valid_until: std::time::Instant) { + self.inner.cache.dns_ptr.insert_with_expiry( + name, + Arc::from(value.into_iter().map(Into::into).collect::>()), + valid_until, + ); + } + + fn mx_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant) { + self.inner + .cache + .dns_mx + .insert_with_expiry(name.to_fqdn(), Arc::from(value), valid_until); + } + + fn tlsa_add(&self, name: impl ToFqdn, value: Arc, valid_until: std::time::Instant) { + self.inner + .cache + .dns_tlsa + .insert_with_expiry(name.to_fqdn(), value, valid_until); + } +} diff --git a/tests/src/utils/http_server.rs b/tests/src/utils/http_server.rs index 29136034..70df1a56 100644 --- a/tests/src/utils/http_server.rs +++ b/tests/src/utils/http_server.rs @@ -4,36 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{AssertConfig, add_test_certs}; +use crate::{AssertConfig, utils::server::TestServer}; use ahash::AHashMap; -use common::{Caches, Core, Data, Inner, config::server::Listeners, network::SessionData}; +use common::{config::server::Listeners, network::SessionData}; use http_proto::{HttpResponse, request::fetch_body}; use hyper::{Method, Uri, body, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; -use std::sync::Arc; +use registry::{ + schema::{ + enums::NetworkListenerProtocol, + prelude::{ObjectType, SocketAddr}, + structs::{NetworkListener, SystemSettings}, + }, + types::{id::ObjectId, map::Map}, +}; +use std::{str::FromStr, sync::Arc}; +use store::registry::{RegistryObject, bootstrap::Bootstrap}; use tokio::sync::watch; -const MOCK_HTTP_SERVER: &str = r#" -[server] -hostname = "'oidc.example.org'" - -[http] -url = "'https://127.0.0.1:9090'" - -[server.listener.jmap] -bind = ['127.0.0.1:9090'] -protocol = 'http' -tls.implicit = true - -[server.socket] -reuse-addr = true - -[certificate.default] -cert = '%{file:{CERT}}%' -private-key = '%{file:{PK}}%' -default = true -"#; - #[derive(Clone)] pub struct HttpSessionManager { inner: HttpRequestHandler, @@ -58,33 +46,44 @@ impl HttpMessage { } pub async fn spawn_mock_http_server( + test: &TestServer, handler: HttpRequestHandler, + port: u16, ) -> (watch::Sender, watch::Receiver) { - // Start mock push server - let mut settings = Config::new(add_test_certs(MOCK_HTTP_SERVER)).unwrap(); - settings.resolve_all_macros().await; - let mock_inner = Arc::new(Inner { - shared_core: Core::parse(&mut settings, Default::default(), Default::default()) - .await - .into_shared(), - data: Data::parse(&mut settings), - cache: Caches::parse(&mut settings), - ..Default::default() - }); - settings.errors.clear(); - settings.warnings.clear(); - let mut servers = Listeners::parse(&mut settings); - servers.parse_tcp_acceptors(&mut settings, mock_inner.clone()); - - // Start JMAP server - servers.bind_and_drop_priv(&mut settings); - settings.assert_no_errors(); + // Start mock HTTP server + let mut bp = Bootstrap::new_uninitialized(test.server.registry().clone()); + let mut servers = Listeners::default(); + servers.parse_server( + &mut bp, + RegistryObject { + id: ObjectId::new(ObjectType::NetworkListener, 0u64.into()), + object: NetworkListener { + name: "mock-http".into(), + bind: Map::new(vec![ + SocketAddr::from_str(&format!("127.0.0.1:{port}")).unwrap(), + ]), + protocol: NetworkListenerProtocol::Http, + tls_implicit: true, + use_tls: true, + socket_reuse_address: true, + socket_reuse_port: true, + ..Default::default() + }, + revision: 0, + }, + &SystemSettings::default(), + ); + servers + .parse_tcp_acceptors(&mut bp, test.server.inner.clone()) + .await; + servers.bind_and_drop_priv(&mut bp); + bp.assert_no_errors(); servers.spawn(|server, acceptor, shutdown_rx| { server.spawn( HttpSessionManager { inner: handler.clone(), }, - mock_inner.clone(), + test.server.inner.clone(), acceptor, shutdown_rx, ); diff --git a/tests/src/utils/jmap.rs b/tests/src/utils/jmap.rs index 9dbe7b75..897a5d77 100644 --- a/tests/src/utils/jmap.rs +++ b/tests/src/utils/jmap.rs @@ -275,7 +275,7 @@ impl Account { serde_json::from_slice( &reqwest::Client::builder() .danger_accept_invalid_certs(true) - .timeout(Duration::from_millis(1000)) + .timeout(Duration::from_millis(5000)) .default_headers(headers) .build() .unwrap() diff --git a/tests/src/utils/mod.rs b/tests/src/utils/mod.rs index 3f607c0d..3e45544c 100644 --- a/tests/src/utils/mod.rs +++ b/tests/src/utils/mod.rs @@ -6,7 +6,9 @@ pub mod account; pub mod cleanup; +pub mod dns; pub mod http; +pub mod http_server; pub mod imap; pub mod jmap; pub mod pop3; diff --git a/tests/src/utils/registry.rs b/tests/src/utils/registry.rs index 66b8d148..891e5312 100644 --- a/tests/src/utils/registry.rs +++ b/tests/src/utils/registry.rs @@ -59,7 +59,9 @@ impl Account { .await .list()[0] .to_string(); - serde_json::from_str(&value).expect("Failed to deserialize item") + serde_json::from_str(&value).unwrap_or_else(|_| { + panic!("Failed to deserialize {value}"); + }) } pub async fn registry_get_many( diff --git a/tests/src/utils/server.rs b/tests/src/utils/server.rs index 1f554aaa..c9bb0c42 100644 --- a/tests/src/utils/server.rs +++ b/tests/src/utils/server.rs @@ -27,7 +27,7 @@ use common::{ }; use http::HttpSessionManager; use imap::core::ImapSessionManager; -use jmap_client::client::{Client, Credentials}; +use jmap_client::client::Client; use managesieve::core::ManageSieveSessionManager; use pop3::Pop3SessionManager; use registry::{ @@ -47,7 +47,7 @@ use smtp::{ spool::{QueuedMessages, SmtpSpool}, }, }; -use std::{str::FromStr, sync::Arc, time::Duration}; +use std::{str::FromStr, sync::Arc}; use store::{ RegistryStore, Store, registry::{bootstrap::Bootstrap, write::RegistryWrite}, @@ -68,6 +68,7 @@ pub struct TestServerBuilder { bootstrap: Bootstrap, temp_dir: TempDir, reset: bool, + logging_enabled: bool, } impl TestServerBuilder { @@ -100,6 +101,7 @@ impl TestServerBuilder { .await, temp_dir, reset, + logging_enabled: false, } } @@ -151,6 +153,11 @@ impl TestServerBuilder { self } + pub fn with_logging(mut self) -> Self { + self.logging_enabled = true; + self + } + pub async fn insert_object(&self, object: impl Into) -> Id { self.bootstrap .registry @@ -170,7 +177,7 @@ impl TestServerBuilder { .ok(); self.insert_object(Tracer::Stdout(TracerStdout { - enable: level.is_some(), + enable: level.is_some() || self.logging_enabled, level: level.unwrap_or(TracingLevel::Info), ansi: true, multiline: false, @@ -180,8 +187,12 @@ impl TestServerBuilder { .filter(|ev| { let ev = ev.as_str(); ev.starts_with("network.") + || ev.starts_with("http.connection-") || ev == "telemetry.webhook-error" || ev == "http.request-body" + || ev == "http.request-url" + || ev == "tls.no-certificates-available" + || ev == "store.cache-hit" }) .copied() .collect(), @@ -296,7 +307,11 @@ impl TestServer { } pub async fn wait_for_tasks(&self) { - wait_for_tasks(&self.server).await; + wait_for_tasks(&self.server, false).await; + } + + pub async fn wait_for_tasks_skip_failures(&self) { + wait_for_tasks(&self.server, true).await; } pub async fn blob_expire_all(&self) { @@ -307,6 +322,11 @@ impl TestServer { assert_is_empty(&self.server, true).await; } + pub async fn cleanup(&self) { + self.assert_is_empty().await; + self.server.invalidate_all_local_caches(); + } + pub async fn destroy_store(&self) { store_destroy(self.server.store()).await; } @@ -334,17 +354,12 @@ impl TestServer { } } -pub async fn destroy_all_mailboxes_for_account(account_id: u32) { - let mut client = Client::new() - .credentials(Credentials::basic("admin", "secret")) - .follow_redirects(["127.0.0.1"]) - .timeout(Duration::from_secs(3600)) - .accept_invalid_certs(true) - .connect("https://127.0.0.1:8899") - .await - .unwrap(); - client.set_default_account_id(Id::from(account_id)); - destroy_all_mailboxes_no_wait(&client).await; +impl Account { + pub async fn destroy_all_mailboxes_for_account(&self, account_id: u32) { + let mut client = self.jmap_client().await; + client.set_default_account_id(Id::from(account_id)); + destroy_all_mailboxes_no_wait(&client).await; + } } async fn destroy_all_mailboxes_no_wait(client: &Client) { diff --git a/tests/src/utils/storage.rs b/tests/src/utils/storage.rs index 620a59e1..36ed7f4b 100644 --- a/tests/src/utils/storage.rs +++ b/tests/src/utils/storage.rs @@ -7,7 +7,7 @@ use crate::utils::cleanup::{search_store_destroy, store_assert_is_empty}; use crate::utils::registry::UnwrapRegistryId; use common::Server; -use registry::schema::structs::Task; +use registry::schema::structs::{Task, TaskStatus}; use registry::{ schema::{ enums::{BlobStoreType, DataStoreType, InMemoryStoreType, SearchStoreType}, @@ -153,7 +153,7 @@ fn build_search_store(typ: SearchStoreType, _path: &str) -> SearchStore { } } -pub async fn wait_for_tasks(server: &Server) { +pub async fn wait_for_tasks(server: &Server, skip_permanent_failures: bool) { let mut count = 0; loop { let mut has_index_tasks = None; @@ -168,9 +168,14 @@ pub async fn wait_for_tasks(server: &Server) { ) .ascending(), |_, value| { - has_index_tasks = Some(Task::deserialize(value)?); + let task = Task::deserialize(value)?; + if skip_permanent_failures && matches!(task.status(), TaskStatus::Failed(_)) { + Ok(true) + } else { + has_index_tasks = Some(task); - Ok(false) + Ok(false) + } }, ) .await @@ -190,7 +195,7 @@ pub async fn wait_for_tasks(server: &Server) { pub async fn assert_is_empty(server: &Server, include_registry: bool) { // Wait for pending index tasks - wait_for_tasks(server).await; + wait_for_tasks(server, false).await; // Assert is empty store_assert_is_empty(