From 944505a925bc2c5ccaf9a9e89e98484fb528b731 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:17:11 +0100 Subject: [PATCH] Registry testing - part 12 --- crates/smtp/src/inbound/auth.rs | 3 +- tests/src/imap/acl.rs | 27 +- tests/src/imap/antispam.rs | 217 +++----- tests/src/imap/append.rs | 13 +- tests/src/imap/basic.rs | 7 +- tests/src/imap/copy_move.rs | 3 +- tests/src/imap/fetch.rs | 3 +- tests/src/imap/idle.rs | 4 +- tests/src/imap/mailbox.rs | 19 +- tests/src/imap/managesieve.rs | 103 +--- tests/src/imap/mod.rs | 598 +++++++--------------- tests/src/imap/pop.rs | 35 +- tests/src/imap/search.rs | 7 +- tests/src/imap/store.rs | 10 +- tests/src/imap/thread.rs | 13 +- tests/src/jmap/mod.rs | 16 +- tests/src/lib.rs | 26 +- tests/src/smtp/config.rs | 532 ------------------- tests/src/smtp/inbound/antispam.rs | 4 +- tests/src/smtp/inbound/auth.rs | 163 +++--- tests/src/smtp/inbound/basic.rs | 20 +- tests/src/smtp/inbound/data.rs | 294 +++++++---- tests/src/smtp/inbound/dmarc.rs | 4 +- tests/src/smtp/inbound/ehlo.rs | 4 +- tests/src/smtp/inbound/limits.rs | 4 +- tests/src/smtp/inbound/mail.rs | 210 +++++--- tests/src/smtp/inbound/milter.rs | 6 +- tests/src/smtp/inbound/mod.rs | 137 ++--- tests/src/smtp/inbound/rcpt.rs | 192 ++++--- tests/src/smtp/inbound/rewrite.rs | 4 +- tests/src/smtp/inbound/scripts.rs | 2 +- tests/src/smtp/inbound/sign.rs | 4 +- tests/src/smtp/inbound/throttle.rs | 4 +- tests/src/smtp/inbound/vrfy.rs | 4 +- tests/src/smtp/lookup/sql.rs | 12 +- tests/src/smtp/lookup/utils.rs | 4 +- tests/src/smtp/management/queue.rs | 4 +- tests/src/smtp/management/report.rs | 4 +- tests/src/smtp/mod.rs | 214 +------- tests/src/smtp/outbound/dane.rs | 4 +- tests/src/smtp/outbound/extensions.rs | 4 +- tests/src/smtp/outbound/fallback_relay.rs | 4 +- tests/src/smtp/outbound/ip_lookup.rs | 4 +- tests/src/smtp/outbound/lmtp.rs | 4 +- tests/src/smtp/outbound/mta_sts.rs | 4 +- tests/src/smtp/outbound/smtp.rs | 4 +- tests/src/smtp/outbound/throttle.rs | 4 +- tests/src/smtp/outbound/tls.rs | 4 +- tests/src/smtp/queue/concurrent.rs | 4 +- tests/src/smtp/queue/dsn.rs | 11 +- tests/src/smtp/queue/manager.rs | 4 +- tests/src/smtp/queue/retry.rs | 4 +- tests/src/smtp/queue/virtualq.rs | 4 +- tests/src/smtp/reporting/analyze.rs | 4 +- tests/src/smtp/reporting/dmarc.rs | 23 +- tests/src/smtp/reporting/scheduler.rs | 4 +- tests/src/smtp/reporting/tls.rs | 4 +- tests/src/smtp/session.rs | 19 + tests/src/store/import_export.rs | 10 +- tests/src/store/mod.rs | 20 - tests/src/system/antispam.rs | 2 + tests/src/system/archiving.rs | 2 + tests/src/system/authentication.rs | 1 + tests/src/system/authorization.rs | 3 + tests/src/system/crypto.rs | 1 + tests/src/system/delivery.rs | 11 +- tests/src/system/directory.rs | 9 +- tests/src/system/mod.rs | 1 + tests/src/system/oidc.rs | 1 + tests/src/system/purge.rs | 1 + tests/src/system/quota.rs | 2 + tests/src/system/security.rs | 1 + tests/src/system/tenant.rs | 2 + tests/src/telemetry/mod.rs | 1 + tests/src/telemetry/tracing.rs | 3 +- tests/src/telemetry/webhooks.rs | 1 + tests/src/utils/account.rs | 47 +- tests/src/utils/imap.rs | 40 +- tests/src/utils/jmap.rs | 10 +- tests/src/utils/mod.rs | 2 + tests/src/utils/pop3.rs | 20 +- tests/src/utils/registry.rs | 22 + tests/src/utils/server.rs | 170 +++++- tests/src/utils/sieve.rs | 104 ++++ tests/src/utils/temp_dir.rs | 37 ++ tests/src/utils/webdav.rs | 20 +- tests/src/webdav/acl.rs | 25 +- tests/src/webdav/basic.rs | 32 +- tests/src/webdav/cal_alarm.rs | 14 +- tests/src/webdav/cal_query.rs | 11 +- tests/src/webdav/cal_scheduling.rs | 211 ++++---- tests/src/webdav/card_query.rs | 9 +- tests/src/webdav/copy_move.rs | 44 +- tests/src/webdav/lock.rs | 16 +- tests/src/webdav/mkcol.rs | 78 +-- tests/src/webdav/mod.rs | 550 ++++++-------------- tests/src/webdav/multiget.rs | 13 +- tests/src/webdav/principals.rs | 177 +++++-- tests/src/webdav/prop.rs | 36 +- tests/src/webdav/put_get.rs | 120 +++-- tests/src/webdav/sync.rs | 10 +- 101 files changed, 2114 insertions(+), 2823 deletions(-) delete mode 100644 tests/src/smtp/config.rs create mode 100644 tests/src/utils/sieve.rs create mode 100644 tests/src/utils/temp_dir.rs diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index 180fac51..0b70403d 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::core::Session; use common::{auth::AuthRequest, network::SessionStream}; use directory::Credentials; use mail_parser::decoders::base64::base64_decode; @@ -11,8 +12,6 @@ use registry::schema::enums::Permission; use smtp_proto::{AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2, IntoString}; use trc::AuthEvent; -use crate::core::Session; - pub struct SaslToken { mechanism: u64, credentials: Credentials, diff --git a/tests/src/imap/acl.rs b/tests/src/imap/acl.rs index c93732b9..20cd12ff 100644 --- a/tests/src/imap/acl.rs +++ b/tests/src/imap/acl.rs @@ -5,13 +5,17 @@ */ use super::{AssertResult, ImapConnection, Type, append::assert_append_message}; -use crate::jmap::mail::delivery::SmtpConnection; +use crate::utils::{server::TestServer, smtp::SmtpConnection}; use imap_proto::ResponseType; -pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConnection) { +pub async fn test( + mut imap_john: &mut ImapConnection, + _imap_check: &mut ImapConnection, + test: &TestServer, +) { // Delivery to support account println!("Running ACL tests..."); - let mut lmtp = SmtpConnection::connect_port(11201).await; + let mut lmtp = SmtpConnection::connect().await; lmtp.ingest( "bill@example.com", &["support@example.com"], @@ -27,21 +31,8 @@ pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConn .await; // Connect to all test accounts - let mut imap_jane = ImapConnection::connect(b"_w ").await; - let mut imap_bill = ImapConnection::connect(b"_z ").await; - for (imap, secret) in [ - (&mut imap_jane, "AGphbmUuc21pdGhAZXhhbXBsZS5jb20Ac2VjcmV0"), - (&mut imap_bill, "AGZvb2JhckBleGFtcGxlLmNvbQBzZWNyZXQ="), - ] { - imap.assert_read(Type::Untagged, ResponseType::Ok).await; - imap.send(&format!( - "AUTHENTICATE PLAIN {{{}+}}\r\n{}", - secret.len(), - secret - )) - .await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; - } + let mut imap_jane = test.account("jane.smith@example.com").imap_client().await; + let mut imap_bill = test.account("foobar@example.com").imap_client().await; // Jane should see the Support account imap_jane.send("LIST \"\" \"*\"").await; diff --git a/tests/src/imap/antispam.rs b/tests/src/imap/antispam.rs index 889ddee9..ad527e22 100644 --- a/tests/src/imap/antispam.rs +++ b/tests/src/imap/antispam.rs @@ -4,41 +4,43 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{IMAPTest, ImapConnection}; -use crate::{imap::Type, jmap::mail::delivery::SmtpConnection, smtp::session::VerifyResponse}; +use crate::{ + imap::Type, + system::antispam::{HAM, SPAM, TEST}, + utils::{imap::AssertResult, server::TestServer, smtp::SmtpConnection}, +}; use common::{Server, manager::SPAM_TRAINER_KEY}; use imap_proto::ResponseType; -use spam_filter::modules::classifier::{SpamClassifier, SpamTrainer}; -use store::{ - Deserialize, IterateParams, U32_LEN, U64_LEN, ValueKey, - write::{AlignedBytes, Archive, BlobOp, ValueClass, key::DeserializeBigEndian}, +use registry::schema::{ + enums::TaskSpamFilterMaintenanceType, + prelude::ObjectType, + structs::{Task, TaskSpamFilterMaintenance, TaskStatus}, +}; +use spam_filter::modules::classifier::SpamTrainer; +use store::{ + Deserialize, + write::{AlignedBytes, Archive}, }; -use types::blob_hash::BlobHash; -pub async fn test(handle: &IMAPTest) { +pub async fn test(test: &TestServer) { println!("Running Spam classifier tests..."); - let mut imap = ImapConnection::connect(b"_x ").await; - imap.assert_read(Type::Untagged, ResponseType::Ok).await; - imap.authenticate("sgd@example.com", "secret").await; - - let account_id = handle - .server - .directory() - .email_to_id("sgd@example.com") - .await - .unwrap() - .unwrap(); + let admin = test.account("admin@example.com"); + let account = test.account("sgd@example.com"); + let mut imap = account.imap_client().await; + let account_id = account.id(); // Make sure there are no training samples - spam_delete_samples(&handle.server).await; - assert_eq!(spam_training_samples(&handle.server).await.total_count, 0); + admin + .registry_destroy_all(ObjectType::SpamTrainingSample) + .await; + assert_eq!(admin.spam_training_samples().await, vec![]); // Train the classifier via APPEND imap.append("INBOX", HAM[0]).await; imap.append("Junk Mail", SPAM[0]).await; - let samples = spam_training_samples(&handle.server).await; - assert_eq!(samples.ham_count, 1); - assert_eq!(samples.spam_count, 1); + let samples = account.spam_training_samples().await; + assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 1); + assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 1); // Append two spam samples to "Drafts", then train the classifier via STORE and MOVE imap.append("Drafts", SPAM[1]).await; @@ -46,9 +48,9 @@ pub async fn test(handle: &IMAPTest) { imap.send_ok("SELECT Drafts").await; imap.send_ok("STORE 1 +FLAGS ($Junk)").await; imap.send_ok("MOVE 2 \"Junk Mail\"").await; - let samples = spam_training_samples(&handle.server).await; - assert_eq!(samples.ham_count, 1); - assert_eq!(samples.spam_count, 3); + let samples = account.spam_training_samples().await; + assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 1); + assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 3); // Add the remaining messages via APPEND for message in HAM.iter().skip(1) { @@ -57,32 +59,35 @@ pub async fn test(handle: &IMAPTest) { for message in SPAM.iter().skip(3) { imap.append("Junk Mail", message).await; } - let samples = spam_training_samples(&handle.server).await; - assert_eq!(samples.ham_count, 10); - assert_eq!(samples.spam_count, 10); - assert_eq!(samples.samples.len(), 20); - assert!( - samples - .samples - .iter() - .all(|s| s.account_id == account_id && s.remove.is_none()) - ); + let samples = account.spam_training_samples().await; + assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 10); + assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 10); + assert_eq!(samples.len(), 20); + assert!(samples.iter().all( + |(_, s)| s.blob_id.class.account_id() == account_id.document_id() && !s.delete_after_use + )); // Train the classifier - handle.server.spam_train(false).await.unwrap(); - let model = spam_classifier_model(&handle.server).await; + admin + .registry_create_object(Task::SpamFilterMaintenance(TaskSpamFilterMaintenance { + maintenance_type: TaskSpamFilterMaintenanceType::Train, + status: TaskStatus::now(), + })) + .await; + test.wait_for_tasks().await; + let model = spam_classifier_model(&test.server).await; assert_eq!(model.reservoir.ham.total_seen, 10); assert_eq!(model.reservoir.spam.total_seen, 10); assert_eq!( - model.last_sample_expiry, - samples.samples.iter().map(|s| s.until).max().unwrap() + model.last_id, + samples.iter().map(|(id, _)| id.id()).max().unwrap() ); - assert_eq!(spam_training_samples(&handle.server).await.total_count, 20); - assert!(handle.server.inner.data.spam_classifier.load().is_active()); + assert_eq!(account.spam_training_samples().await.len(), 20); + assert!(test.server.inner.data.spam_classifier.load().is_active()); // Send 3 test emails for message in TEST { - let mut lmtp = SmtpConnection::connect_port(11201).await; + let mut lmtp = SmtpConnection::connect().await; lmtp.ingest("bill@example.com", &["sgd@example.com"], message) .await; } @@ -101,7 +106,7 @@ pub async fn test(handle: &IMAPTest) { .assert_not_contains("FLAGS ($Junk") .assert_contains("Subject: classifier test") .assert_contains("X-Spam-Status: No") - .assert_contains("PROB_SPAM_UNCERTAIN"); + .assert_contains_any(&["PROB_SPAM_UNCERTAIN", "PROB_HAM_LOW"]); imap.send_ok("SELECT \"Junk Mail\"").await; imap.send("FETCH 10 (FLAGS RFC822.TEXT)").await; imap.assert_read(Type::Tagged, ResponseType::Ok) @@ -111,36 +116,23 @@ pub async fn test(handle: &IMAPTest) { .assert_contains("X-Spam-Status: Yes") .assert_contains("PROB_SPAM_HIGH"); imap.send_ok("MOVE 10 INBOX").await; - let samples = spam_training_samples(&handle.server).await; - assert_eq!(samples.ham_count, 11); - assert_eq!(samples.spam_count, 10); + let samples = account.spam_training_samples().await; + assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 11); + assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 10); // Make sure spam traps trigger spam classification - let mut lmtp = SmtpConnection::connect_port(11201).await; + let mut lmtp = SmtpConnection::connect().await; lmtp.ingest("bill@example.com", &["spamtrap@example.com"], SPAM[4]) .await; tokio::time::sleep(std::time::Duration::from_millis(200)).await; - let samples = spam_training_samples(&handle.server).await; - assert_eq!(samples.ham_count, 11); - assert_eq!(samples.spam_count, 11); -} + let samples = admin.spam_training_samples().await; + assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 11); + assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 11); -#[derive(Default, Debug)] -pub struct TrainingSamples { - pub samples: Vec, - pub spam_count: usize, - pub ham_count: usize, - pub total_count: usize, -} - -#[derive(Debug)] -#[allow(dead_code)] -pub struct TrainingSample { - pub hash: BlobHash, - pub account_id: u32, - pub is_spam: bool, - pub remove: Option, - pub until: u64, + // Global spam samples should not appear in the account + let samples = account.spam_training_samples().await; + assert_eq!(samples.iter().filter(|x| !x.1.is_spam).count(), 11); + assert_eq!(samples.iter().filter(|x| x.1.is_spam).count(), 10); } pub async fn spam_classifier_model(server: &Server) -> SpamTrainer { @@ -157,90 +149,3 @@ pub async fn spam_classifier_model(server: &Server) -> SpamTrainer { .unwrap() .unwrap() } - -pub async fn spam_delete_samples(server: &Server) { - let from_key = ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: ValueClass::Blob(BlobOp::SpamSample { - hash: BlobHash::default(), - until: 0, - }), - }; - let to_key = ValueKey { - account_id: u32::MAX, - collection: u8::MAX, - document_id: u32::MAX, - class: ValueClass::Blob(BlobOp::SpamSample { - hash: BlobHash::new_max(), - until: u64::MAX, - }), - }; - server.store().delete_range(from_key, to_key).await.unwrap(); -} - -pub async fn spam_training_samples(server: &Server) -> TrainingSamples { - let mut samples = TrainingSamples::default(); - let from_key = ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: ValueClass::Blob(BlobOp::SpamSample { - hash: BlobHash::default(), - until: 0, - }), - }; - let to_key = ValueKey { - account_id: u32::MAX, - collection: u8::MAX, - document_id: u32::MAX, - class: ValueClass::Blob(BlobOp::SpamSample { - hash: BlobHash::new_max(), - until: u64::MAX, - }), - }; - server - .store() - .iterate( - IterateParams::new(from_key, to_key).ascending(), - |key, value| { - let until = key.deserialize_be_u64(1)?; - let account_id = key.deserialize_be_u32(U64_LEN + 1)?; - let hash = - BlobHash::try_from_hash_slice(key.get(U64_LEN + U32_LEN + 1..).ok_or_else( - || trc::Error::corrupted_key(key, value.into(), trc::location!()), - )?) - .unwrap(); - let (Some(is_spam), Some(hold)) = (value.first(), value.get(1)) else { - return Err(trc::Error::corrupted_key( - key, - value.into(), - trc::location!(), - )); - }; - - let do_remove = *hold == 0; - let is_spam = *is_spam == 1; - samples.samples.push(TrainingSample { - hash, - account_id, - is_spam, - remove: do_remove.then_some(until), - until, - }); - if is_spam { - samples.spam_count += 1; - } else { - samples.ham_count += 1; - } - samples.total_count += 1; - - Ok(true) - }, - ) - .await - .unwrap(); - - samples -} diff --git a/tests/src/imap/append.rs b/tests/src/imap/append.rs index e510fcd1..d6bcfba0 100644 --- a/tests/src/imap/append.rs +++ b/tests/src/imap/append.rs @@ -4,15 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{AssertResult, ImapConnection, Type, resources_dir}; +use crate::utils::server::TestServer; +use imap_proto::ResponseType; use std::{fs, io}; -use imap_proto::ResponseType; - -use crate::jmap::wait_for_tasks; - -use super::{AssertResult, IMAPTest, ImapConnection, Type, resources_dir}; - -pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, handle: &IMAPTest) { +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, test: &TestServer) { println!("Running APPEND tests..."); // Invalid APPEND commands @@ -64,7 +61,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, h expected_uid += 1; } - wait_for_tasks(&handle.server).await; + test.wait_for_tasks().await; } pub async fn assert_append_message( diff --git a/tests/src/imap/basic.rs b/tests/src/imap/basic.rs index 5acdb58e..60eed8c3 100644 --- a/tests/src/imap/basic.rs +++ b/tests/src/imap/basic.rs @@ -13,11 +13,12 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { println!("Running basic tests..."); // Test OAuth Bearer decoding - assert!( + assert_eq!( Credentials::Bearer { token: "vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==".to_string(), - username: None, - } == Credentials::decode_sasl_challenge_oauth( + username: Some("user@example.com".to_string()), + }, + Credentials::decode_sasl_challenge_oauth( &base64_decode( concat!( "bixhPXVzZXJAZXhhbXBsZS5jb20sAWhv", diff --git a/tests/src/imap/copy_move.rs b/tests/src/imap/copy_move.rs index 4c86069f..2c52f568 100644 --- a/tests/src/imap/copy_move.rs +++ b/tests/src/imap/copy_move.rs @@ -4,9 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use imap_proto::ResponseType; - use super::{AssertResult, ImapConnection, Type}; +use imap_proto::ResponseType; pub async fn test(_imap: &mut ImapConnection, imap_check: &mut ImapConnection) { println!("Running COPY/MOVE tests..."); diff --git a/tests/src/imap/fetch.rs b/tests/src/imap/fetch.rs index 517d76f2..1c28337e 100644 --- a/tests/src/imap/fetch.rs +++ b/tests/src/imap/fetch.rs @@ -4,9 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use imap_proto::ResponseType; - use super::{AssertResult, ImapConnection, Type}; +use imap_proto::ResponseType; pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { println!("Running FETCH tests..."); diff --git a/tests/src/imap/idle.rs b/tests/src/imap/idle.rs index dce021fd..b22a7deb 100644 --- a/tests/src/imap/idle.rs +++ b/tests/src/imap/idle.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::jmap::mail::delivery::SmtpConnection; +use crate::utils::smtp::SmtpConnection; use super::{AssertResult, ImapConnection, Type}; use imap_proto::ResponseType; @@ -163,7 +163,7 @@ pub async fn test( .assert_contains("* 0 EXISTS"); // Test SMTP delivery notifications - let mut lmtp = SmtpConnection::connect_port(if is_cluster_test { 17000 } else { 11201 }).await; + let mut lmtp = SmtpConnection::connect_port(if is_cluster_test { 17000 } else { 11200 }).await; lmtp.ingest( "bill@example.com", &["jdoe@example.com"], diff --git a/tests/src/imap/mailbox.rs b/tests/src/imap/mailbox.rs index 1d732a21..5880e8d2 100644 --- a/tests/src/imap/mailbox.rs +++ b/tests/src/imap/mailbox.rs @@ -7,20 +7,22 @@ use imap::op::list::matches_pattern; use imap_proto::ResponseType; +use crate::utils::server::TestServer; + use super::{AssertResult, ImapConnection, Type}; -pub async fn test(mut imap: &mut ImapConnection, mut imap_check: &mut ImapConnection) { +pub async fn test( + mut imap: &mut ImapConnection, + mut imap_check: &mut ImapConnection, + test: &TestServer, +) { println!("Running mailbox tests..."); // Pattern matching tests mailbox_matches_pattern(); // Create third connection for testing - let mut other_conn = ImapConnection::connect(b"_z ").await; - other_conn - .send("AUTHENTICATE PLAIN {32+}\r\nAGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0") - .await; - other_conn.assert_read(Type::Tagged, ResponseType::Ok).await; + let mut other_conn = test.account("jdoe@example.com").imap_client().await; // List folders imap.send("LIST \"\" \"*\"").await; @@ -326,10 +328,7 @@ pub async fn test(mut imap: &mut ImapConnection, mut imap_check: &mut ImapConnec imap.assert_read(Type::Tagged, ResponseType::Ok).await; // Shared folder creation tests - let mut imap_jane = ImapConnection::connect(b"_z ").await; - imap_jane - .authenticate("jane.smith@example.com", "secret") - .await; + let mut imap_jane = test.account("jane.smith@example.com").imap_client().await; imap_jane .send("CREATE \"Shared Folders/support@example.com/INBOX/Test\"") .await; diff --git a/tests/src/imap/managesieve.rs b/tests/src/imap/managesieve.rs index ca90e0fc..88ef3c26 100644 --- a/tests/src/imap/managesieve.rs +++ b/tests/src/imap/managesieve.rs @@ -4,20 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - -use imap_proto::ResponseType; -use mail_send::smtp::tls::build_tls_connector; -use rustls_pki_types::ServerName; -use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, - net::TcpStream, -}; -use tokio_rustls::client::TlsStream; - use super::AssertResult; +use crate::utils::{server::TestServer, sieve::SieveConnection}; +use imap_proto::ResponseType; -pub async fn test() { +pub async fn test(test: &TestServer) { println!("Running ManageSieve tests..."); // Connect to ManageSieve @@ -28,14 +19,8 @@ pub async fn test() { .assert_contains("IMPLEMENTATION"); // Authenticate - sieve - .send("AUTHENTICATE \"PLAIN\" \"AGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0\"") - .await; - sieve.assert_read(ResponseType::Ok).await; - /*sieve - .assert_read(ResponseType::Ok) - .await - .assert_contains("MAXREDIRECTS");*/ + let account = test.account("jdoe@example.com"); + sieve.authenticate(account.name(), account.secret()).await; // CheckScript sieve.send("CHECKSCRIPT \"if true { keep; }\"").await; @@ -139,81 +124,3 @@ pub async fn test() { .assert_count("minimalist script", 0) .assert_count("holidays", 0); } - -pub struct SieveConnection { - reader: Lines>>>, - writer: WriteHalf>, -} - -impl SieveConnection { - pub async fn connect() -> Self { - let (reader, writer) = tokio::io::split( - build_tls_connector(true) - .connect( - ServerName::try_from("imap.example.org").unwrap().to_owned(), - TcpStream::connect("127.0.0.1:4190").await.unwrap(), - ) - .await - .unwrap(), - ); - SieveConnection { - reader: BufReader::new(reader).lines(), - writer, - } - } - - pub async fn assert_read(&mut self, rt: ResponseType) -> Vec { - let lines = self.read().await; - let mut buf = Vec::with_capacity(10); - rt.serialize(&mut buf); - if lines - .last() - .unwrap() - .starts_with(&String::from_utf8(buf).unwrap()) - { - lines - } else { - panic!("Expected {:?} from server but got: {:?}", rt, lines); - } - } - - pub async fn read(&mut self) -> Vec { - let mut lines = Vec::new(); - loop { - match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await { - Ok(Ok(Some(line))) => { - let is_done = - line.starts_with("OK") || line.starts_with("NO") || line.starts_with("BYE"); - //println!("<- {:?}", line); - lines.push(line); - if is_done { - return lines; - } - } - Ok(Ok(None)) => { - panic!("Invalid response: {:?}.", lines); - } - Ok(Err(err)) => { - panic!("Connection broken: {} ({:?})", err, lines); - } - Err(_) => panic!("Timeout while waiting for server response: {:?}", lines), - } - } - } - - pub async fn send(&mut self, text: &str) { - //println!("-> {:?}", text); - self.writer.write_all(text.as_bytes()).await.unwrap(); - self.writer.write_all(b"\r\n").await.unwrap(); - } - - pub async fn send_raw(&mut self, text: &str) { - //println!("-> {:?}", text); - self.writer.write_all(text.as_bytes()).await.unwrap(); - } - - pub async fn send_literal(&mut self, text: &str, literal: &str) { - self.send(&format!("{}{{{}+}}\r\n{}", text, literal.len(), literal)) - .await; - } -} diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 85f33d5c..fcdfe339 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -20,48 +20,200 @@ pub mod search; pub mod store; pub mod thread; -use crate::{ - AssertConfig, add_test_certs, - directory::internal::TestInternalDirectory, - store::{ - TempDir, build_store_config, - cleanup::{search_store_destroy, store_destroy}, - }, +use crate::utils::{ + imap::{AssertResult, ImapConnection, Type}, + server::TestServerBuilder, }; -use ::managesieve::core::ManageSieveSessionManager; use ahash::AHashSet; -use base64::{Engine, engine::general_purpose}; -use common::{ - Caches, Core, Data, Inner, Server, - config::{ - server::{Listeners, ServerProtocol}, - telemetry::Telemetry, - }, - manager::boot::build_ipc, -}; -use http::HttpSessionManager; -use imap::core::ImapSessionManager; use imap_proto::ResponseType; -use pop3::Pop3SessionManager; -use services::SpawnServices; -use smtp::{SpawnQueueManager, core::SmtpSessionManager}; -use std::{ - path::PathBuf, - sync::Arc, - time::{Duration, Instant}, -}; -use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, - net::TcpStream, - sync::watch, +use registry::{ + schema::{ + enums::{Permission, SpecialUse}, + prelude::ObjectType, + structs::{ + Action, Email, EmailFolder, Expression, Imap, MemoryLookupKey, MtaStageAuth, + MtaStageData, SpamClassifier, SpamTag, SpamTagScore, + }, + }, + types::float::Float, }; +use serde_json::json; +use std::{path::PathBuf, time::Instant}; +use utils::map::vec_map::VecMap; #[tokio::test] pub async fn imap_tests() { - // Prepare settings + let mut test = TestServerBuilder::new("imap_tests") + .await + .with_default_listeners() + .await + .build() + .await; + + // Create admin account + let admin = test.create_admin_account("admin@example.com").await; + + // Create test users + for (name, secret, description, aliases) in [ + ( + "jdoe@example.com", + "12345 + extra safety", + "John Doe", + &["john.doe@example.com"][..], + ), + ( + "jane.smith@example.com", + "abcde + extra safety", + "Jane Smith", + &["jane@example.com"][..], + ), + ( + "foobar@example.com", + "098765 + extra safety", + "Bill Foobar", + &["bill.foobar@example.com"][..], + ), + ( + "popper@example.com", + "a_pop3_safe_secret_with_extra_safety", + "Karl Popper", + &["karl.popper@example.com"][..], + ), + ( + "sgd@example.com", + "secret2 + extra safety", + "Sigmund Gudmund Dudmundsson", + &[][..], + ), + ( + "spamtrap@example.com", + "secret3 + extra safety", + "Spam Trap", + &[][..], + ), + ] { + let account = admin + .create_user_account( + name, + secret, + description, + aliases, + vec![Permission::UnlimitedRequests, Permission::UnlimitedUploads], + ) + .await; + test.insert_account(account); + } + + // Create test group + test.insert_account( + admin + .create_group_account("support@example.com", "Support Group", &[]) + .await, + ); + + // Add Jane to the Support group + let support_id = test.account("support@example.com").id(); + admin + .registry_update_object( + ObjectType::Account, + test.account("jane.smith@example.com").id(), + json!({ + "memberGroupIds": { support_id: true }, + }), + ) + .await; + + // Add test settings + admin + .registry_create_object(Imap { + allow_plain_text_auth: true, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".to_string(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SpamClassifier { + min_ham_samples: 10, + min_spam_samples: 10, + ..Default::default() + }) + .await; + admin + .registry_create_object(Email { + default_folders: VecMap::from_iter( + [ + (SpecialUse::Inbox, "Inbox"), + (SpecialUse::Sent, "Sent Items"), + (SpecialUse::Trash, "Deleted Items"), + (SpecialUse::Junk, "Junk Mail"), + (SpecialUse::Drafts, "Drafts"), + ] + .into_iter() + .map(|(use_, name)| { + ( + use_, + EmailFolder { + name: name.into(), + subscribe: false, + ..Default::default() + }, + ) + }), + ), + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + add_delivered_to_header: false, + enable_spam_filter: Expression { + else_: "recipients[0] != 'popper@example.com'".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SpamTag::Score(SpamTagScore { + score: Float::new(10.0), + tag: "PROB_SPAM_LOW".into(), + })) + .await; + admin + .registry_create_object(SpamTag::Score(SpamTagScore { + score: Float::new(10.0), + tag: "PROB_SPAM_HIGH".into(), + })) + .await; + admin + .registry_create_object(SpamTag::Score(SpamTagScore { + score: Float::new(100.0), + tag: "SPAM_TRAP".into(), + })) + .await; + admin + .registry_create_object(MemoryLookupKey { + is_glob_pattern: true, + key: "spamtrap@*".into(), + namespace: "spam-traps".into(), + }) + .await; + admin.reload_settings().await; + admin + .registry_create_object(Action::ReloadLookupStores) + .await; + + test.insert_account(admin); + let start_time = Instant::now(); - let delete = true; - let handle = init_imap_tests(delete).await; // Body structure tests body_structure::test(); @@ -77,10 +229,9 @@ pub async fn imap_tests() { basic::test(&mut imap, &mut imap_check).await; // Login + let account = test.account("jdoe@example.com"); for imap in [&mut imap, &mut imap_check] { - imap.send("AUTHENTICATE PLAIN {32+}\r\nAGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0") - .await; - imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.authenticate(account.name(), account.secret()).await; } // Delete folders @@ -89,16 +240,16 @@ pub async fn imap_tests() { imap.assert_read(Type::Tagged, ResponseType::Ok).await; } - mailbox::test(&mut imap, &mut imap_check).await; - append::test(&mut imap, &mut imap_check, &handle).await; - search::test(&mut imap, &mut imap_check, &handle).await; + mailbox::test(&mut imap, &mut imap_check, &test).await; + append::test(&mut imap, &mut imap_check, &test).await; + search::test(&mut imap, &mut imap_check, &test).await; fetch::test(&mut imap, &mut imap_check).await; - store::test(&mut imap, &mut imap_check, &handle).await; + store::test(&mut imap, &mut imap_check, &test).await; copy_move::test(&mut imap, &mut imap_check).await; - thread::test(&mut imap, &mut imap_check, &handle).await; + thread::test(&mut imap, &mut imap_check, &test).await; idle::test(&mut imap, &mut imap_check, false).await; condstore::test(&mut imap, &mut imap_check).await; - acl::test(&mut imap, &mut imap_check).await; + acl::test(&mut imap, &mut imap_check, &test).await; // Logout for imap in [&mut imap, &mut imap_check] { @@ -110,13 +261,13 @@ pub async fn imap_tests() { } // Antispam training - antispam::test(&handle).await; + antispam::test(&test).await; // Run ManageSieve tests - managesieve::test().await; + managesieve::test(&test).await; // Run POP3 tests - pop::test().await; + pop::test(&test).await; // Print elapsed time let elapsed = start_time.elapsed(); @@ -127,176 +278,8 @@ pub async fn imap_tests() { ); // Remove test data - if delete { - handle.temp_dir.delete(); - } -} - -#[allow(dead_code)] -pub struct IMAPTest { - server: Server, - temp_dir: TempDir, - shutdown_tx: watch::Sender, -} - -async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest { - // Load and parse config - let temp_dir = TempDir::new("imap_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 tracers = Telemetry::parse(&mut config, &stores); - let core = Core::parse(&mut config, stores, Default::default()).await; - 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, - }); - - // 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, - ), - }; - }); - - if delete_if_exists { - store_destroy(&store).await; - search_store_destroy(&search_store).await; - } - - // Create tables and test accounts - store - .create_test_user("admin", "secret", "Superuser", &[]) - .await; - store - .create_test_user( - "jdoe@example.com", - "secret", - "John Doe", - &["jdoe@example.com"], - ) - .await; - store - .create_test_user( - "jane.smith@example.com", - "secret", - "Jane Smith", - &["jane.smith@example.com"], - ) - .await; - store - .create_test_user( - "foobar@example.com", - "secret", - "Bill Foobar", - &["foobar@example.com"], - ) - .await; - store - .create_test_user( - "popper@example.com", - "secret", - "Karl Popper", - &["popper@example.com"], - ) - .await; - store - .create_test_user( - "sgd@example.com", - "secret", - "Sigmund Gudmund Dudmundsson", - &["sgd@example.com"], - ) - .await; - store - .create_test_user( - "spamtrap@example.com", - "secret", - "Spam Trap", - &["spamtrap@example.com"], - ) - .await; - store - .create_test_group( - "support@example.com", - "Support Group", - &["support@example.com"], - ) - .await; - store - .add_to_group("jane.smith@example.com", "support@example.com") - .await; - - IMAPTest { - server: inner.build_server(), - temp_dir, - shutdown_tx, + if test.is_reset() { + test.temp_dir.delete(); } } @@ -323,186 +306,3 @@ fn resources_dir() -> PathBuf { resources.push("imap"); resources } - -const SERVER: &str = r#" -[server] -hostname = "imap.example.org" - -[server.listener.imap] -bind = ["127.0.0.1:9991"] -protocol = "imap" -max-connections = 81920 - -[server.listener.imaptls] -bind = ["127.0.0.1:9992"] -protocol = "imap" -max-connections = 81920 -tls.implicit = true - -[server.listener.sieve] -bind = ["127.0.0.1:4190"] -protocol = "managesieve" -max-connections = 81920 -tls.implicit = true - -[server.listener.pop3] -bind = ["127.0.0.1:4110"] -protocol = "pop3" -max-connections = 81920 -tls.implicit = true - -[server.listener.lmtp-debug] -bind = ['127.0.0.1:11201'] -greeting = 'Test LMTP instance' -protocol = 'lmtp' -tls.implicit = false - -[server.socket] -reuse-addr = true - -[server.tls] -enable = true -implicit = false -certificate = "default" - -[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" - -[spam-filter] -enable = true - -[spam-filter.list] -scores = {"PROB_SPAM_LOW" = "10.0", "PROB_SPAM_HIGH" = "10.0", "SPAM_TRAP" = "100.0"} - -[spam-filter.classifier.samples] -min-ham = 10 -min-spam = 10 - -[lookup] -"spam-traps" = {"spamtrap@*"} - -[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] -enable = false -allow-invalid-certs = true - -[session.data] -spam-filter = "recipients[0] != 'popper@example.com'" - -[session.data.add-headers] -delivered-to = false - -[session.extensions] -future-release = [ { if = "!is_empty(authenticated_as)", then = "99999999d"}, - { else = false } ] - -[certificate.default] -cert = "%{file:{CERT}}%" -private-key = "%{file:{PK}}%" - -[imap.protocol] -uidplus = true - -[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" -authentication = "100/2s" -anonymous = "100/1m" - -[jmap.event-source] -throttle = "500ms" - -[jmap.web-sockets] -throttle = "500ms" - -[jmap.push] -throttle = "500ms" -attempts.interval = "500ms" - -[email.folders.inbox] -name = "Inbox" -subscribe = false - -[email.folders.sent] -name = "Sent Items" -subscribe = false - -[email.folders.trash] -name = "Deleted Items" -subscribe = false - -[email.folders.junk] -name = "Junk Mail" -subscribe = false - -[email.folders.drafts] -name = "Drafts" -subscribe = false - -[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" - -[oauth] -key = "parerga_und_paralipomena" -[oauth.auth] -max-attempts = 1 - -[oauth.expiry] -user-code = "1s" -token = "1s" -refresh-token = "3s" -refresh-token-renew = "2s" - -[tracer.console] -type = "console" -level = "{LEVEL}" -multiline = false -ansi = true -disabled-events = ["network.*"] - -"#; diff --git a/tests/src/imap/pop.rs b/tests/src/imap/pop.rs index 89f13146..be1904ae 100644 --- a/tests/src/imap/pop.rs +++ b/tests/src/imap/pop.rs @@ -4,22 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{jmap::mail::delivery::SmtpConnection, smtp::session::VerifyResponse}; -use mail_send::smtp::tls::build_tls_connector; -use rustls_pki_types::ServerName; -use std::time::Duration; -use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, - net::TcpStream, +use crate::utils::{ + imap::AssertResult, + pop3::{Pop3Connection, ResponseType}, + server::TestServer, + smtp::SmtpConnection, }; -use tokio_rustls::client::TlsStream; -pub async fn test() { +pub async fn test(test: &TestServer) { println!("Running POP3 tests..."); // Send 3 test emails for i in 0..3 { - let mut lmtp = SmtpConnection::connect_port(11201).await; + let mut lmtp = SmtpConnection::connect().await; lmtp.ingest( "bill@example.com", &["popper@example.com"], @@ -41,8 +38,8 @@ pub async fn test() { } // Connect to POP3 + let account = test.account("popper@example.com"); let mut pop3 = Pop3Connection::connect().await; - pop3.assert_read(ResponseType::Ok).await; // Capabilities pop3.send("CAPA").await; @@ -64,16 +61,13 @@ pub async fn test() { pop3.assert_read(ResponseType::Err).await; pop3.send("USER popper@example.com").await; pop3.assert_read(ResponseType::Ok).await; - pop3.send("PASS secret").await; + pop3.send(&format!("PASS {}", account.secret())).await; pop3.assert_read(ResponseType::Ok).await; pop3.send("QUIT").await; // Authenticate using AUTH PLAIN let mut pop3 = Pop3Connection::connect().await; - pop3.assert_read(ResponseType::Ok).await; - pop3.send("AUTH PLAIN AHBvcHBlckBleGFtcGxlLmNvbQBzZWNyZXQ=") - .await; - pop3.assert_read(ResponseType::Ok).await; + pop3.authenticate(account.name(), account.secret()).await; // STAT pop3.send("STAT").await; @@ -149,7 +143,8 @@ pub async fn test() { pop3.send("RSET").await; pop3.assert_read(ResponseType::Ok).await; pop3.send("QUIT").await; - let mut pop3 = Pop3Connection::connect_and_login().await; + let mut pop3 = Pop3Connection::connect().await; + pop3.authenticate(account.name(), account.secret()).await; pop3.send("STAT").await; pop3.assert_read(ResponseType::Ok) .await @@ -160,7 +155,8 @@ pub async fn test() { pop3.assert_read(ResponseType::Ok).await; pop3.send("QUIT").await; pop3.assert_read(ResponseType::Ok).await; - let mut pop3 = Pop3Connection::connect_and_login().await; + let mut pop3 = Pop3Connection::connect().await; + pop3.authenticate(account.name(), account.secret()).await; pop3.send("STAT").await; pop3.assert_read(ResponseType::Ok) .await @@ -180,7 +176,8 @@ pub async fn test() { pop3.assert_read(ResponseType::Ok).await; pop3.send("QUIT").await; pop3.assert_read(ResponseType::Ok).await; - let mut pop3 = Pop3Connection::connect_and_login().await; + let mut pop3 = Pop3Connection::connect().await; + pop3.authenticate(account.name(), account.secret()).await; pop3.send("STAT").await; pop3.assert_read(ResponseType::Ok) .await diff --git a/tests/src/imap/search.rs b/tests/src/imap/search.rs index d87bc2d0..4ad5ad0c 100644 --- a/tests/src/imap/search.rs +++ b/tests/src/imap/search.rs @@ -4,11 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::utils::server::TestServer; + use super::{AssertResult, ImapConnection, Type}; -use crate::imap::IMAPTest; use imap_proto::ResponseType; -pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, handle: &IMAPTest) { +pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, test: &TestServer) { println!("Running SEARCH tests..."); // Searches without selecting a mailbox should fail. @@ -119,7 +120,7 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, ha .await; imap.assert_read(Type::Tagged, ResponseType::Ok) .await - .assert_contains(if !handle.server.search_store().is_mysql() { + .assert_contains(if !test.server.search_store().is_mysql() { "COUNT 10 ALL 6,4:5,1,10,3,7:8,2,9" } else { "COUNT 10 ALL 9,3,7:8,2,6,4:5,1,10" diff --git a/tests/src/imap/store.rs b/tests/src/imap/store.rs index a25f6dae..2b361715 100644 --- a/tests/src/imap/store.rs +++ b/tests/src/imap/store.rs @@ -4,13 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{AssertResult, ImapConnection, Type}; +use crate::utils::server::TestServer; use imap_proto::ResponseType; -use crate::jmap::wait_for_tasks; - -use super::{AssertResult, IMAPTest, ImapConnection, Type}; - -pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, handle: &IMAPTest) { +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, test: &TestServer) { println!("Running STORE tests..."); // Select INBOX @@ -60,7 +58,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, h .assert_contains("UIDNEXT 11"); // Store using saved searches - wait_for_tasks(&handle.server).await; + test.wait_for_tasks().await; imap.send("SEARCH RETURN (SAVE) FROM nathaniel").await; imap.assert_read(Type::Tagged, ResponseType::Ok).await; imap.send("UID STORE $ +FLAGS (\\Answered)").await; diff --git a/tests/src/imap/thread.rs b/tests/src/imap/thread.rs index 2439ba33..d934e59c 100644 --- a/tests/src/imap/thread.rs +++ b/tests/src/imap/thread.rs @@ -4,13 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{ImapConnection, Type, append::build_messages}; +use crate::{ + imap::{AssertResult, expand_uid_list}, + utils::server::TestServer, +}; use imap_proto::ResponseType; -use crate::imap::{AssertResult, IMAPTest, expand_uid_list}; - -use super::{ImapConnection, Type, append::build_messages}; - -pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, handle: &IMAPTest) { +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, test: &TestServer) { println!("Running THREAD tests..."); // Create test messages @@ -81,7 +82,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection, h .assert_contains("(9 10 11 12)"); // Filter by subject (mySQL does not support searching for short keywords) - if !handle.server.search_store().is_mysql() { + if !test.server.search_store().is_mysql() { imap.send("THREAD REFERENCES UTF-8 SUBJECT T1").await; imap.assert_read(Type::Tagged, ResponseType::Ok) .await diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 369f9d67..69f61b16 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -33,17 +33,7 @@ async fn jmap_tests() { .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; + let admin = test.create_admin_account("admin@example.com").await; // Create test users for (name, secret, description, aliases) in [ @@ -76,7 +66,7 @@ async fn jmap_tests() { .create_user_account( name, secret, - description.into(), + description, aliases, vec![Permission::UnlimitedRequests, Permission::UnlimitedUploads], ) @@ -87,7 +77,7 @@ async fn jmap_tests() { // Create test group test.insert_account( admin - .create_group_account("sales@example.com", "Sales Group".into(), &[]) + .create_group_account("sales@example.com", "Sales Group", &[]) .await, ); diff --git a/tests/src/lib.rs b/tests/src/lib.rs index bcf30d90..130460f7 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -8,8 +8,6 @@ use ::store::registry::bootstrap::Bootstrap; #[cfg(not(target_env = "msvc"))] use jemallocator::Jemalloc; -#[cfg(test)] -use trc::Collector; #[cfg(not(target_env = "msvc"))] #[global_allocator] @@ -20,17 +18,14 @@ static GLOBAL: Jemalloc = Jemalloc; pub mod cluster; #[cfg(test)] pub mod directory; -#[cfg(test)] -pub mod imap; - -#[cfg(test)] -pub mod smtp; -#[cfg(test)] -pub mod webdav; */ #[cfg(test)] +pub mod imap; +#[cfg(test)] pub mod jmap; #[cfg(test)] +pub mod smtp; +#[cfg(test)] pub mod store; #[cfg(test)] pub mod system; @@ -38,6 +33,8 @@ pub mod system; pub mod telemetry; #[cfg(test)] pub mod utils; +#[cfg(test)] +pub mod webdav; #[cfg(test)] pub trait AssertConfig { @@ -61,14 +58,3 @@ impl AssertConfig for Bootstrap { self } } - -#[cfg(test)] -pub fn enable_logging() { - use common::config::telemetry::Telemetry; - - if let Ok(level) = std::env::var("LOG") - && !Collector::is_enabled() - { - Telemetry::test_tracer(level.parse().expect("Invalid log level")); - } -} diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs deleted file mode 100644 index f7f039b2..00000000 --- a/tests/src/smtp/config.rs +++ /dev/null @@ -1,532 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use super::add_test_certs; -use common::{ - Server, - config::{ - server::{Listener, Listeners, ServerProtocol, TcpListener}, - smtp::*, - }, - expr::{functions::ResolveVariable, if_block::*, tokenizer::TokenMap, *}, -}; -use compact_str::ToCompactString; -use std::{fs, net::IpAddr, path::PathBuf, sync::Arc, time::Duration}; -use tokio::net::TcpSocket; - -struct TestEnvelope { - pub local_ip: IpAddr, - pub remote_ip: IpAddr, - pub sender_domain: String, - pub sender: String, - pub rcpt_domain: String, - pub rcpt: String, - pub helo_domain: String, - pub authenticated_as: String, - pub mx: String, - pub listener_id: String, - pub priority: i16, -} - -#[test] -fn parse_if_blocks() { - let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file.push("resources"); - file.push("smtp"); - file.push("config"); - file.push("if-blocks.toml"); - - let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); - - // Create context and add some conditions - - let token_map = TokenMap::default().with_variables(&[ - ExpressionVariable::Rcpt, - ExpressionVariable::RcptDomain, - ExpressionVariable::Sender, - ExpressionVariable::SenderDomain, - ExpressionVariable::AuthenticatedAs, - ExpressionVariable::Listener, - ExpressionVariable::RemoteIp, - ExpressionVariable::LocalIp, - ExpressionVariable::Priority, - ]); - - assert_eq!( - IfBlock::try_parse(&mut config, "durations", &token_map).unwrap(), - IfBlock { - key: "durations".into(), - if_then: vec![ - IfThen { - expr: Expression { - items: vec![ - ExpressionItem::Variable(ExpressionVariable::Sender), - ExpressionItem::Constant(Constant::String("jdoe".into())), - ExpressionItem::BinaryOperator(BinaryOperator::Eq) - ] - }, - then: Expression { - items: vec![ExpressionItem::Constant(Constant::Integer(432000000))] - } - }, - IfThen { - expr: Expression { - items: vec![ - ExpressionItem::Variable(ExpressionVariable::Priority), - ExpressionItem::Constant(Constant::Integer(1)), - ExpressionItem::UnaryOperator(UnaryOperator::Minus), - ExpressionItem::BinaryOperator(BinaryOperator::Eq), - ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(ExpressionVariable::Rcpt), - ExpressionItem::Constant(Constant::String("jane".into())), - ExpressionItem::Function { - id: 29, - num_args: 2 - }, - ExpressionItem::BinaryOperator(BinaryOperator::Or) - ] - }, - then: Expression { - items: vec![ExpressionItem::Constant(Constant::Integer(3600000))] - } - } - ], - default: Expression { - items: vec![ExpressionItem::Constant(Constant::Integer(0))] - } - } - ); - - assert_eq!( - IfBlock::try_parse(&mut config, "string-list", &token_map).unwrap(), - IfBlock { - key: "string-list".into(), - if_then: vec![ - IfThen { - expr: Expression { - items: vec![ - ExpressionItem::Variable(ExpressionVariable::Sender), - ExpressionItem::Constant(Constant::String("jdoe".into())), - ExpressionItem::BinaryOperator(BinaryOperator::Eq) - ] - }, - then: Expression { - items: vec![ - ExpressionItem::Constant(Constant::String("From".into())), - ExpressionItem::Constant(Constant::String("To".into())), - ExpressionItem::Constant(Constant::String("Date".into())), - ExpressionItem::ArrayBuild(3) - ] - } - }, - IfThen { - expr: Expression { - items: vec![ - ExpressionItem::Variable(ExpressionVariable::Priority), - ExpressionItem::Constant(Constant::Integer(1)), - ExpressionItem::UnaryOperator(UnaryOperator::Minus), - ExpressionItem::BinaryOperator(BinaryOperator::Eq), - ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(ExpressionVariable::Rcpt), - ExpressionItem::Constant(Constant::String("jane".into())), - ExpressionItem::Function { - id: 29, - num_args: 2 - }, - ExpressionItem::BinaryOperator(BinaryOperator::Or) - ] - }, - then: Expression { - items: vec![ExpressionItem::Constant(Constant::String( - "Other-ID".into() - ))] - } - } - ], - default: Expression { - items: vec![ExpressionItem::ArrayBuild(0)] - } - } - ); - - assert_eq!( - IfBlock::try_parse(&mut config, "string-list-bis", &token_map).unwrap(), - IfBlock { - key: "string-list-bis".into(), - if_then: vec![ - IfThen { - expr: Expression { - items: vec![ - ExpressionItem::Variable(ExpressionVariable::Sender), - ExpressionItem::Constant(Constant::String("jdoe".into())), - ExpressionItem::BinaryOperator(BinaryOperator::Eq) - ] - }, - then: Expression { - items: vec![ - ExpressionItem::Constant(Constant::String("From".into())), - ExpressionItem::Constant(Constant::String("To".into())), - ExpressionItem::Constant(Constant::String("Date".into())), - ExpressionItem::ArrayBuild(3) - ] - } - }, - IfThen { - expr: Expression { - items: vec![ - ExpressionItem::Variable(ExpressionVariable::Priority), - ExpressionItem::Constant(Constant::Integer(1)), - ExpressionItem::UnaryOperator(UnaryOperator::Minus), - ExpressionItem::BinaryOperator(BinaryOperator::Eq), - ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(ExpressionVariable::Rcpt), - ExpressionItem::Constant(Constant::String("jane".into())), - ExpressionItem::Function { - id: 29, - num_args: 2 - }, - ExpressionItem::BinaryOperator(BinaryOperator::Or) - ] - }, - then: Expression { - items: vec![ExpressionItem::ArrayBuild(0)] - } - } - ], - default: Expression { - items: vec![ - ExpressionItem::Constant(Constant::String("ID-Bis".into())), - ExpressionItem::ArrayBuild(1) - ] - } - } - ); - - assert_eq!( - IfBlock::try_parse(&mut config, "single-value", &token_map).unwrap(), - IfBlock { - key: "single-value".into(), - if_then: vec![], - default: Expression { - items: vec![ExpressionItem::Constant(Constant::String( - "hello world".into() - ))] - } - } - ); - - for bad_rule in [ - "bad-if-without-then", - "bad-if-without-else", - "bad-multiple-else", - ] { - if let Some(value) = IfBlock::try_parse(&mut config, bad_rule, &token_map) { - panic!("Condition {bad_rule:?} had unexpected result {value:?}"); - } - } -} - -#[test] -fn parse_throttles() { - let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file.push("resources"); - file.push("smtp"); - file.push("config"); - file.push("throttle.toml"); - - let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); - let throttle = parse_queue_rate_limiter( - &mut config, - "throttle", - &TokenMap::default().with_variables(&[ - ExpressionVariable::Rcpt, - ExpressionVariable::RcptDomain, - ExpressionVariable::Sender, - ExpressionVariable::SenderDomain, - ExpressionVariable::AuthenticatedAs, - ExpressionVariable::Listener, - ExpressionVariable::RemoteIp, - ExpressionVariable::LocalIp, - ExpressionVariable::Priority, - ]), - u16::MAX, - ); - - assert_eq!( - throttle, - vec![ - QueueRateLimiter { - id: "0000".into(), - expr: Expression { - items: vec![ - ExpressionItem::Variable(8), - ExpressionItem::Constant(Constant::String("127.0.0.1".into())), - ExpressionItem::BinaryOperator(BinaryOperator::Eq) - ] - }, - keys: THROTTLE_REMOTE_IP | THROTTLE_AUTH_AS, - rate: Rate { - requests: 50, - period: Duration::from_secs(30) - } - }, - QueueRateLimiter { - id: "0001".into(), - expr: Expression::default(), - keys: THROTTLE_SENDER_DOMAIN, - rate: Rate { - requests: 50, - period: Duration::from_secs(30) - } - } - ] - ); -} - -#[test] -fn parse_servers() { - let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file.push("resources"); - file.push("smtp"); - file.push("config"); - file.push("servers.toml"); - - let toml = add_test_certs(&fs::read_to_string(file).unwrap()); - - // Parse servers - let mut config = Config::new(toml).unwrap(); - let servers = Listeners::parse(&mut config).servers; - let id_generator = Arc::new(utils::snowflake::SnowflakeIdGenerator::new()); - let expected_servers = vec![ - Listener { - id: "smtp".into(), - protocol: ServerProtocol::Smtp, - listeners: vec![TcpListener { - socket: TcpSocket::new_v4().unwrap(), - addr: "127.0.0.1:9925".parse().unwrap(), - ttl: 3600.into(), - backlog: 1024.into(), - linger: None, - nodelay: true, - }], - max_connections: 8192, - proxy_networks: vec![], - span_id_gen: id_generator.clone(), - }, - Listener { - id: "smtps".into(), - protocol: ServerProtocol::Smtp, - listeners: vec![ - TcpListener { - socket: TcpSocket::new_v4().unwrap(), - addr: "127.0.0.1:9465".parse().unwrap(), - ttl: 4096.into(), - backlog: 1024.into(), - linger: None, - nodelay: true, - }, - TcpListener { - socket: TcpSocket::new_v4().unwrap(), - addr: "127.0.0.1:9466".parse().unwrap(), - ttl: 4096.into(), - backlog: 1024.into(), - linger: None, - nodelay: true, - }, - ], - max_connections: 1024, - proxy_networks: vec![], - span_id_gen: id_generator.clone(), - }, - Listener { - id: "submission".into(), - protocol: ServerProtocol::Smtp, - listeners: vec![TcpListener { - socket: TcpSocket::new_v4().unwrap(), - addr: "127.0.0.1:9991".parse().unwrap(), - ttl: 3600.into(), - backlog: 2048.into(), - linger: None, - nodelay: true, - }], - max_connections: 8192, - proxy_networks: vec![], - span_id_gen: id_generator.clone(), - }, - ]; - - for (server, expected_server) in servers.into_iter().zip(expected_servers) { - assert_eq!( - server.id, expected_server.id, - "failed for {}", - expected_server.id - ); - assert_eq!( - server.protocol, expected_server.protocol, - "failed for {}", - expected_server.id - ); - for (listener, expected_listener) in - server.listeners.into_iter().zip(expected_server.listeners) - { - assert_eq!( - listener.addr, expected_listener.addr, - "failed for {}", - expected_server.id - ); - assert_eq!( - listener.ttl, expected_listener.ttl, - "failed for {}", - expected_server.id - ); - assert_eq!( - listener.backlog, expected_listener.backlog, - "failed for {}", - expected_server.id - ); - } - } -} - -#[tokio::test] -async fn eval_if() { - let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file.push("resources"); - file.push("smtp"); - file.push("config"); - file.push("rules-eval.toml"); - - let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); - let envelope = TestEnvelope::from_config(&mut config); - let token_map = TokenMap::default().with_variables(&[ - ExpressionVariable::Rcpt, - ExpressionVariable::RcptDomain, - ExpressionVariable::Sender, - ExpressionVariable::SenderDomain, - ExpressionVariable::AuthenticatedAs, - ExpressionVariable::Listener, - ExpressionVariable::RemoteIp, - ExpressionVariable::LocalIp, - ExpressionVariable::Priority, - ExpressionVariable::Mx, - ]); - let core = Server::default(); - - for (key, _) in config.keys.clone() { - if !key.starts_with("rule.") { - continue; - } - - //println!("============= Testing {:?} ==================", key); - let (_, expected_result) = key.rsplit_once('-').unwrap(); - assert_eq!( - core.eval_if::( - &IfBlock { - key: key.to_string(), - if_then: vec![IfThen { - expr: Expression::try_parse(&mut config, key.as_str(), &token_map).unwrap(), - then: Expression::from(true), - }], - default: Expression::from(false), - }, - &envelope, - 0 - ) - .await - .unwrap() - .to_bool(), - expected_result.parse::().unwrap(), - "failed for {key:?}" - ); - } -} - -#[tokio::test] -async fn eval_dynvalue() { - let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file.push("resources"); - file.push("smtp"); - file.push("config"); - file.push("rules-dynvalue.toml"); - - let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); - let envelope = TestEnvelope::from_config(&mut config); - let token_map = TokenMap::default().with_variables(&[ - ExpressionVariable::Rcpt, - ExpressionVariable::RcptDomain, - ExpressionVariable::Sender, - ExpressionVariable::SenderDomain, - ExpressionVariable::AuthenticatedAs, - ExpressionVariable::Listener, - ExpressionVariable::RemoteIp, - ExpressionVariable::LocalIp, - ExpressionVariable::Priority, - ExpressionVariable::Mx, - ]); - let core = Server::default(); - - for test_name in config.sub_keys("eval", "") { - //println!("============= Testing {:?} ==================", key); - let if_block = IfBlock::try_parse( - &mut config, - ("eval", test_name.as_str(), "test"), - &token_map, - ) - .unwrap(); - let expected = config - .property_require::>(("eval", test_name.as_str(), "expect")) - .unwrap_or_else(|| panic!("Missing expect for test {test_name:?}")); - - assert_eq!( - core.eval_if::(&if_block, &envelope, 0).await, - expected, - "failed for test {test_name:?}" - ); - } -} - -impl ResolveVariable for TestEnvelope { - fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { - match variable { - ExpressionVariable::Rcpt => self.rcpt.as_str().into(), - ExpressionVariable::RcptDomain => self.rcpt_domain.as_str().into(), - ExpressionVariable::Sender => self.sender.as_str().into(), - ExpressionVariable::SenderDomain => self.sender_domain.as_str().into(), - ExpressionVariable::AuthenticatedAs => self.authenticated_as.as_str().into(), - ExpressionVariable::Listener => self.listener_id.to_compact_string().into(), - ExpressionVariable::RemoteIp => self.remote_ip.to_compact_string().into(), - ExpressionVariable::LocalIp => self.local_ip.to_compact_string().into(), - ExpressionVariable::Priority => self.priority.to_compact_string().into(), - ExpressionVariable::Mx => self.mx.as_str().into(), - ExpressionVariable::HeloDomain => self.helo_domain.as_str().into(), - _ => Default::default(), - } - } - - fn resolve_global(&self, _: &str) -> Variable<'_> { - Variable::Integer(0) - } -} - -impl TestEnvelope { - pub fn from_config(config: &mut Config) -> Self { - Self { - local_ip: config.property_require("envelope.local-ip").unwrap(), - remote_ip: config.property_require("envelope.remote-ip").unwrap(), - sender_domain: config.property_require("envelope.sender-domain").unwrap(), - sender: config.property_require("envelope.sender").unwrap(), - rcpt_domain: config.property_require("envelope.rcpt-domain").unwrap(), - rcpt: config.property_require("envelope.rcpt").unwrap(), - authenticated_as: config - .property_require("envelope.authenticated-as") - .unwrap(), - mx: config.property_require("envelope.mx").unwrap(), - listener_id: config.property_require("envelope.listener").unwrap(), - priority: config.property_require("envelope.priority").unwrap(), - helo_domain: config.property_require("envelope.helo-domain").unwrap(), - } - } -} diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index dfc0eec4..4565522c 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -171,8 +171,8 @@ allow-invalid-certs = true #[tokio::test(flavor = "multi_thread")] async fn antispam() { - // Enable logging - crate::enable_logging(); + + // Prepare config let tmp_dir = TempDir::new("smtp_antispam_test", true); diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index e91236b7..b8b4eebf 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -5,78 +5,97 @@ */ use crate::{ - AssertConfig, - smtp::{ - TempDir, TestSMTP, - session::{TestSession, VerifyResponse}, - }, + smtp::session::{TestSession, VerifyResponse}, + utils::server::TestServerBuilder, }; -use common::Core; -use smtp::core::{Session, State}; - -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" -directory = "local" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = ["john@example.org", "jdoe@example.org", "john.doe@example.org"] -email-list = ["info@example.org"] -member-of = ["sales"] - -[[directory."local".principals]] -name = "jane" -description = "Jane Doe" -secret = "p4ssw0rd" -email = "jane@example.org" -email-list = ["info@example.org"] -member-of = ["sales", "support"] - -[session.auth] -require = [{if = "remote_ip = '10.0.0.1'", then = true}, - {else = false}] -mechanisms = [{if = "remote_ip = '10.0.0.1' && is_tls", then = "[plain, login]"}, - {else = 0}] -directory = [{if = "remote_ip = '10.0.0.1'", then = "'local'"}, - {else = false}] -must-match-sender = true - -[session.auth.errors] -total = [{if = "remote_ip = '10.0.0.1'", then = 2}, - {else = 3}] -wait = "100ms" - -[session.extensions] -future-release = [{if = '!is_empty(authenticated_as)', then = '1d'}, - {else = false}] -"#; +use registry::{ + schema::structs::{Expression, ExpressionMatch, MtaExtensions, MtaStageAuth}, + types::list::List, +}; +use smtp::core::State; #[tokio::test] async fn auth() { - // Enable logging - crate::enable_logging(); + let mut test = TestServerBuilder::new("smtp_auth_test") + .await + .with_http_listener(19001) + .await + .disable_services() + .build() + .await; - let tmp_dir = TempDir::new("smtp_auth_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - config.assert_no_errors(); + // Create test users + let admin = test.account("admin"); + for (name, secret, description, aliases) in [ + ( + "john@example.org", + "12345 + extra safety", + "John Doe", + &["john.doe@example.org"][..], + ), + ( + "jane@example.org", + "abcde + extra safety", + "Jane Smith", + &["jane@example.org"], + ), + ] { + admin + .create_user_account(name, secret, description, aliases, vec![]) + .await; + } + + // Add test settings + admin + .registry_create_object(MtaStageAuth { + max_failures: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "2".into(), + }]), + else_: "3".into(), + }, + must_match_sender: Expression { + else_: "true".into(), + ..Default::default() + }, + require: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + sasl_mechanisms: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1' && is_tls".into(), + then: "[plain, login]".into(), + }]), + else_: "0".into(), + }, + wait_on_fail: Expression { + else_: "100ms".into(), + ..Default::default() + }, + }) + .await; + admin + .registry_create_object(MtaExtensions { + future_release: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "!is_empty(authenticated_as)".into(), + then: "1d".into(), + }]), + else_: "false".into(), + }, + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); // EHLO should not advertise plain text auth without TLS - let mut session = Session::test(TestSMTP::from_core(core).server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.stream.tls = false; @@ -98,7 +117,7 @@ async fn auth() { // Invalid password should be rejected session - .cmd("AUTH PLAIN AGpvaG4AY2hpbWljaGFuZ2Fz", "535 5.7.8") + .auth_plain("john@example.org", "wrong pass", "535 5.7.8") .await; // Session should be disconnected after second invalid auth attempt @@ -115,7 +134,7 @@ async fn auth() { // Successful PLAIN authentication session.data.auth_errors = 0; session - .cmd("AUTH PLAIN AGpvaG4Ac2VjcmV0", "235 2.7.0") + .auth_plain("john@example.org", "12345 + extra safety", "235 2.7.0") .await; // Users should be able to send emails only from their own email addresses @@ -125,7 +144,7 @@ async fn auth() { // Should not be able to authenticate twice session - .cmd("AUTH PLAIN AGpvaG4Ac2VjcmV0", "503 5.5.1") + .auth_plain("john@example.org", "12345 + extra safety", "503 5.5.1") .await; // FUTURERELEASE extension should be available after authenticating @@ -139,9 +158,9 @@ async fn auth() { // Successful LOGIN authentication session.data.authenticated_as.take(); - session.cmd("AUTH LOGIN", "334").await; - session.cmd("amFuZQ==", "334").await; - session.cmd("cDRzc3cwcmQ=", "235 2.7.0").await; + session + .auth_login("john@example.org", "12345 + extra safety", "235 2.7.0") + .await; // Login should not be advertised to 10.0.0.2 session.data.remote_ip_str = "10.0.0.2".into(); @@ -154,6 +173,6 @@ async fn auth() { .assert_not_contains(" PLAIN") .assert_not_contains(" LOGIN"); session - .cmd("AUTH PLAIN AGpvaG4Ac2VjcmV0", "503 5.5.1") + .auth_plain("john@example.org", "12345 + extra safety", "503 5.5.1") .await; } diff --git a/tests/src/smtp/inbound/basic.rs b/tests/src/smtp/inbound/basic.rs index 8f3c85aa..4b2d3ce0 100644 --- a/tests/src/smtp/inbound/basic.rs +++ b/tests/src/smtp/inbound/basic.rs @@ -4,20 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::Core; -use smtp::core::Session; - -use crate::smtp::{ - TestSMTP, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::session::{TestSession, VerifyResponse}, + utils::server::TestServerBuilder, }; #[tokio::test] async fn basic_commands() { - // Enable logging - crate::enable_logging(); + let test = TestServerBuilder::new("smtp_basic_test") + .await + .with_http_listener(19002) + .await + .disable_services() + .build() + .await; - let mut session = Session::test(TestSMTP::from_core(Core::default()).server); + let mut session = test.new_mta_session(); // STARTTLS should be available on clear text connections session.stream.tls = false; diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 91cddb20..b08074b9 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -5,116 +5,197 @@ */ use crate::{ - AssertConfig, smtp::{ - TempDir, TestSMTP, inbound::TestMessage, session::{TestSession, VerifyResponse, load_test_message}, }, - store::cleanup::store_assert_is_empty, + utils::server::TestServerBuilder, +}; +use registry::{ + schema::{ + enums::MtaQueueQuotaKey, + prelude::ObjectType, + structs::{ + Expression, ExpressionMatch, MtaQueueQuota, MtaStageAuth, MtaStageData, SenderAuth, + SpamSettings, + }, + }, + types::{list::List, map::Map}, }; -use common::Core; -use smtp::core::Session; - -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" -directory = "local" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[spam-filter] -enable = false - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = ["john@foobar.org", "jdoe@example.org", "john.doe@example.org"] - -[[directory."local".principals]] -name = "jane" -description = "Jane Doe" -secret = "p4ssw0rd" -email = "jane@domain.net" - -[[directory."local".principals]] -name = "bill" -description = "Bill Foobar" -secret = "p4ssw0rd" -email = "bill@foobar.org" - -[[directory."local".principals]] -name = "mike" -description = "Mike Foobar" -secret = "p4ssw0rd" -email = "mike@test.com" - -[session.rcpt] -directory = "'local'" - -[session.data.limits] -messages = [{if = "remote_ip = '10.0.0.1'", then = 1}, - {else = 100}] -received-headers = 3 - -[session.data.add-headers] -received = [{if = "remote_ip = '10.0.0.3'", then = true}, - {else = false}] -received-spf = [{if = "remote_ip = '10.0.0.3'", then = true}, - {else = false}] -auth-results = [{if = "remote_ip = '10.0.0.3'", then = true}, - {else = false}] -message-id = [{if = "remote_ip = '10.0.0.3'", then = true}, - {else = false}] -date = [{if = "remote_ip = '10.0.0.3'", then = true}, - {else = false}] -return-path = [{if = "remote_ip = '10.0.0.3'", then = true}, - {else = false}] - -[[queue.quota]] -match = "sender = 'john@doe.org'" -key = ['sender'] -messages = 1 - -[[queue.quota]] -match = "rcpt_domain = 'foobar.org'" -key = ['rcpt_domain'] -size = 450 -enable = true - -[[queue.quota]] -match = "rcpt = 'jane@domain.net'" -key = ['rcpt'] -size = 450 -enable = true - -"#; #[tokio::test] async fn data() { - // Enable logging - crate::enable_logging(); + let mut test = TestServerBuilder::new("smtp_data_test") + .await + .with_http_listener(19004) + .await + .disable_services() + .capture_queue() + .build() + .await; - // Create temp dir for queue - let tmp_dir = TempDir::new("smtp_data_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - config.assert_no_errors(); + // Create test users + let admin = test.account("admin"); + for (name, secret, description, aliases) in [ + ("john@foobar.org", "12345 + extra safety", "John Doe", &[]), + ("jane@domain.net", "abcde + extra safety", "Jane Smith", &[]), + ( + "bill@foobar.org", + "p4ssw0rd + extra safety", + "Bill Foobar", + &[], + ), + ( + "mike@test.com", + "p4ssw0rd + extra safety", + "Mike Foobar", + &[], + ), + ] { + admin + .create_user_account(name, secret, description, aliases, vec![]) + .await; + } + + // Add test settings + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SpamSettings { + enable: false, + ..Default::default() + }) + .await; + admin + .registry_create_object(SenderAuth { + dmarc_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + reverse_ip_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_ehlo_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_from_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + add_auth_results_header: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + add_date_header: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + add_message_id_header: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + add_received_header: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + add_received_spf_header: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + add_return_path_header: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + max_messages: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "1".into(), + }]), + else_: "100".into(), + }, + max_received_headers: Expression { + else_: "3".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaQueueQuota { + description: None, + enable: true, + key: Map::new(vec![MtaQueueQuotaKey::Sender]), + match_: Expression { + else_: "sender = 'john@doe.org'".into(), + ..Default::default() + }, + messages: Some(1), + size: None, + }) + .await; + admin + .registry_create_object(MtaQueueQuota { + description: None, + enable: true, + key: Map::new(vec![MtaQueueQuotaKey::RcptDomain]), + match_: Expression { + else_: "rcpt_domain = 'foobar.org'".into(), + ..Default::default() + }, + messages: None, + size: Some(450), + }) + .await; + admin + .registry_create_object(MtaQueueQuota { + description: None, + enable: true, + key: Map::new(vec![MtaQueueQuotaKey::Rcpt]), + match_: Expression { + else_: "rcpt = 'jane@domain.net'".into(), + ..Default::default() + }, + messages: None, + size: Some(450), + }) + .await; + admin.reload_settings().await; + test.reload_core(); // Test queue message builder - let test = TestSMTP::from_core(core); - let mut qr = test.queue_receiver; - let mut session = Session::test(test.server.clone()); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.test_builder().await; @@ -144,7 +225,7 @@ async fn data() { .send_message("john@test.org", &["mike@test.com"], "test:no_msgid", "250") .await; assert_eq!( - qr.expect_message().await.read_message(&qr).await, + test.expect_message().await.read_message(&test).await, load_test_message("no_msgid", "messages") ); @@ -161,9 +242,9 @@ async fn data() { session .send_message("bill@doe.org", &["mike@test.com"], "test:no_msgid", "250") .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("From: ") .assert_contains("To: ") @@ -191,7 +272,7 @@ async fn data() { .await; // Release quota - qr.clear_queue(&test.server).await; + test.clear_queue().await; // Only 1500 bytes are allowed in the queue to domain foobar.org session @@ -230,6 +311,9 @@ async fn data() { .await; // Make sure store is empty - qr.clear_queue(&test.server).await; - store_assert_is_empty(test.server.store(), test.server.blob_store().clone(), false).await; + test.clear_queue().await; + test.account("admin") + .registry_destroy_all(ObjectType::MtaQueueQuota) + .await; + test.assert_is_empty().await; } diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index 1aa271db..c1c99856 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -86,8 +86,8 @@ verify = [{if = "sender_domain = 'test.net'", then = 'relaxed'}, #[tokio::test] async fn dmarc() { - // Enable logging - crate::enable_logging(); + + let tmp_dir = TempDir::new("smtp_dmarc_test", true); let mut config = Config::new(tmp_dir.update_config(CONFIG.to_string() + SIGNATURES)).unwrap(); diff --git a/tests/src/smtp/inbound/ehlo.rs b/tests/src/smtp/inbound/ehlo.rs index b332c9a4..9d154c0a 100644 --- a/tests/src/smtp/inbound/ehlo.rs +++ b/tests/src/smtp/inbound/ehlo.rs @@ -34,8 +34,8 @@ ehlo = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, #[tokio::test] async fn ehlo() { - // Enable logging - crate::enable_logging(); + + let mut config = Config::new(CONFIG).unwrap(); let core = Core::parse(&mut config, Default::default(), Default::default()).await; diff --git a/tests/src/smtp/inbound/limits.rs b/tests/src/smtp/inbound/limits.rs index 01817026..48a2705e 100644 --- a/tests/src/smtp/inbound/limits.rs +++ b/tests/src/smtp/inbound/limits.rs @@ -25,8 +25,8 @@ duration = [{if = "remote_ip = '10.0.0.3'", then = '500ms'}, #[tokio::test] async fn limits() { - // Enable logging - crate::enable_logging(); + + let mut config = Config::new(CONFIG).unwrap(); let core = Core::parse(&mut config, Default::default(), Default::default()).await; diff --git a/tests/src/smtp/inbound/mail.rs b/tests/src/smtp/inbound/mail.rs index a8af5fc7..394f9dab 100644 --- a/tests/src/smtp/inbound/mail.rs +++ b/tests/src/smtp/inbound/mail.rs @@ -4,103 +4,177 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - DnsCache, TempDir, TestSMTP, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::session::{TestSession, VerifyResponse}, + utils::{dns::DnsCache, server::TestServerBuilder}, }; -use common::Core; use mail_auth::{IprevResult, SpfResult, common::parse::TxtRecordParser, spf::Spf}; -use smtp::core::Session; +use registry::{ + schema::{ + enums::MtaInboundThrottleKey, + structs::{ + Expression, ExpressionMatch, MtaExtensions, MtaInboundThrottle, MtaStageAuth, + MtaStageData, MtaStageEhlo, MtaStageMail, Rate, SenderAuth, + }, + }, + types::{list::List, map::Map}, +}; use smtp_proto::{MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS}; use std::time::{Duration, Instant, SystemTime}; -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/data.db" - -[session.ehlo] -require = true - -[auth.spf.verify] -ehlo = 'relaxed' -mail-from = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, - {else = 'relaxed'}] - -[auth.iprev] -verify = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, - {else = 'relaxed'}] - -[session.extensions] -future-release = [{if = "remote_ip = '10.0.0.2'", then = '1d'}, - {else = false}] -deliver-by = [{if = "remote_ip = '10.0.0.2'", then = '1d'}, - {else = false}] -requiretls = [{if = "remote_ip = '10.0.0.2'", then = true}, - {else = false}] -mt-priority = [{if = "remote_ip = '10.0.0.2'", then = 'nsep'}, - {else = false}] - -[session.mail] -is-allowed = "sender_domain != 'blocked.com'" - -[session.data.limits] -size = [{if = "remote_ip = '10.0.0.2'", then = 2048}, - {else = 1024}] - -[[queue.limiter.inbound]] -match = "remote_ip = '10.0.0.1'" -key = 'sender' -rate = '2/1s' -enable = true - -"#; - #[tokio::test] async fn mail() { - // Enable logging - crate::enable_logging(); + let mut test = TestServerBuilder::new("smtp_mail_from_test") + .await + .with_http_listener(19003) + .await + .disable_services() + .build() + .await; - let tmp_dir = TempDir::new("smtp_mail_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - let server = TestSMTP::from_core(core).server; + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageEhlo { + require: Expression { + else_: "true".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SenderAuth { + reverse_ip_verify: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "strict".into(), + }]), + else_: "relaxed".into(), + }, + spf_ehlo_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_from_verify: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "strict".into(), + }]), + else_: "relaxed".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaExtensions { + deliver_by: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "1d".into(), + }]), + else_: "false".into(), + }, + future_release: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "1d".into(), + }]), + else_: "false".into(), + }, + mt_priority: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "nsep".into(), + }]), + else_: "false".into(), + }, + require_tls: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageMail { + is_sender_allowed: Expression { + else_: "sender_domain != 'blocked.com'".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + max_message_size: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "2048".into(), + }]), + else_: "1024".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaInboundThrottle { + description: None, + enable: true, + key: Map::new(vec![MtaInboundThrottleKey::Sender]), + match_: Expression { + else_: "remote_ip = '10.0.0.1'".into(), + ..Default::default() + }, + rate: Rate { + count: 2, + period: 1000u64.into(), + }, + }) + .await; + admin.reload_settings().await; + test.reload_core(); - server.txt_add( + test.server.txt_add( "foobar.org", Spf::parse(b"v=spf1 ip4:10.0.0.1 -all").unwrap(), Instant::now() + Duration::from_secs(5), ); - server.txt_add( + test.server.txt_add( "mx1.foobar.org", Spf::parse(b"v=spf1 ip4:10.0.0.1 -all").unwrap(), Instant::now() + Duration::from_secs(5), ); - server.ptr_add( + test.server.ptr_add( "10.0.0.1".parse().unwrap(), vec!["mx1.foobar.org.".to_string()], Instant::now() + Duration::from_secs(5), ); - server.ipv4_add( + test.server.ipv4_add( "mx1.foobar.org.", vec!["10.0.0.1".parse().unwrap()], Instant::now() + Duration::from_secs(5), ); - server.ptr_add( + test.server.ptr_add( "10.0.0.2".parse().unwrap(), vec!["mx2.foobar.org.".to_string()], Instant::now() + Duration::from_secs(5), ); // Be rude and do not say EHLO - let mut session = Session::test(server.clone()); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.eval_session_params().await; @@ -190,7 +264,7 @@ async fn mail() { .unwrap(); session.response().assert_code("550 5.7.25"); session.data.iprev = None; - server.ipv4_add( + test.server.ipv4_add( "mx2.foobar.org.", vec!["10.0.0.2".parse().unwrap()], Instant::now() + Duration::from_secs(5), @@ -202,7 +276,7 @@ async fn mail() { .await .unwrap(); session.response().assert_code("550 5.7.23"); - server.txt_add( + test.server.txt_add( "foobar.org", Spf::parse(b"v=spf1 ip4:10.0.0.1 ip4:10.0.0.2 -all").unwrap(), Instant::now() + Duration::from_secs(5), diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index 7cb4b013..f0f149aa 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -93,8 +93,8 @@ stages = ["data"] #[tokio::test] async fn milter_session() { - // Enable logging - crate::enable_logging(); + + // Configure tests let tmp_dir = TempDir::new("smtp_milter_test", true); @@ -220,7 +220,7 @@ async fn milter_session() { #[tokio::test] async fn mta_hook_session() { - // Enable logging + /*let disable = "true"; tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() diff --git a/tests/src/smtp/inbound/mod.rs b/tests/src/smtp/inbound/mod.rs index a62ed6a3..75d0601b 100644 --- a/tests/src/smtp/inbound/mod.rs +++ b/tests/src/smtp/inbound/mod.rs @@ -4,11 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{QueueReceiver, ReportReceiver}; +use crate::utils::server::TestServer; use common::{ Server, ipc::{DmarcEvent, QueueEvent, QueueEventStatus, ReportingEvent, TlsEvent}, }; +use registry::{schema::prelude::ObjectType, types::ObjectImpl}; use smtp::queue::{Message, MessageWrapper, QueueId, QueuedMessage}; use std::time::Duration; use store::{ @@ -16,25 +17,29 @@ use store::{ write::{AlignedBytes, Archive, QueueClass, ValueClass, key::DeserializeBigEndian}, }; use tokio::sync::mpsc::error::TryRecvError; +use types::id::Id; -pub mod antispam; -pub mod asn; pub mod auth; pub mod basic; pub mod data; +pub mod mail; +pub mod rcpt; + +/* +pub mod antispam; +pub mod asn; pub mod dmarc; pub mod ehlo; pub mod limits; -pub mod mail; pub mod milter; -pub mod rcpt; pub mod rewrite; pub mod scripts; pub mod sign; pub mod throttle; pub mod vrfy; +*/ -impl QueueReceiver { +impl TestServer { pub async fn read_event(&mut self) -> QueueEvent { match tokio::time::timeout(Duration::from_millis(100), self.queue_rx.recv()).await { Ok(Some(event)) => event, @@ -64,53 +69,8 @@ impl QueueReceiver { assert_eq!(self.read_queued_events().await, vec![]); } - pub async fn assert_report_is_empty(&self) { - assert_eq!(self.read_report_events().await, vec![]); - - for (from_key, to_key) in [ - ( - ValueKey::from(ValueClass::Queue(QueueClass::TlsReportEvent(ReportEvent { - due: 0, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }))), - ValueKey::from(ValueClass::Queue(QueueClass::TlsReportEvent(ReportEvent { - due: u64::MAX, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }))), - ), - ( - ValueKey::from(ValueClass::Queue(QueueClass::DmarcReportEvent( - ReportEvent { - due: 0, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }, - ))), - ValueKey::from(ValueClass::Queue(QueueClass::DmarcReportEvent( - ReportEvent { - due: u64::MAX, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }, - ))), - ), - ] { - self.store - .iterate( - IterateParams::new(from_key, to_key).ascending().no_values(), - |key, _| { - panic!("Unexpected report event: {key:?}"); - }, - ) - .await - .unwrap(); - } + pub async fn assert_report_is_empty(&self) { + assert_eq!(self.read_report_events::().await, vec![]); } pub async fn expect_message(&mut self) -> MessageWrapper { @@ -160,7 +120,8 @@ impl QueueReceiver { }, ))); - self.store + self.server + .store() .iterate( IterateParams::new(from_key, to_key).ascending().no_values(), |key, _| { @@ -185,7 +146,8 @@ impl QueueReceiver { let to_key = ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))); let mut messages = Vec::new(); - self.store + self.server + .store() .iterate( IterateParams::new(from_key, to_key).descending(), |key, value| { @@ -206,44 +168,8 @@ impl QueueReceiver { messages } - pub async fn read_report_events(&self) -> Vec { - let from_key = ValueKey::from(ValueClass::Queue(QueueClass::DmarcReportHeader( - ReportEvent { - due: 0, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }, - ))); - let to_key = ValueKey::from(ValueClass::Queue(QueueClass::TlsReportHeader( - ReportEvent { - due: u64::MAX, - policy_hash: 0, - seq_id: 0, - domain: String::new(), - }, - ))); - - let mut events = Vec::new(); - self.store - .iterate( - IterateParams::new(from_key, to_key).ascending().no_values(), - |key, _| { - let event = ReportEvent::deserialize(key)?; - // Skip lock - if event.seq_id != 0 { - events.push(if *key.last().unwrap() == 0 { - QueueClass::DmarcReportHeader(event) - } else { - QueueClass::TlsReportHeader(event) - }); - } - Ok(true) - }, - ) - .await - .unwrap(); - events + pub async fn read_report_events(&self) -> Vec<(Id, T)> { + self.account("admin").registry_get_all().await } pub async fn last_queued_message(&self) -> MessageWrapper { @@ -273,15 +199,12 @@ impl QueueReceiver { .expect("No event found in queue for message") } - pub async fn clear_queue(&self, server: &Server) { - for message in self.read_queued_messages().await { - let due = self.message_due(message.queue_id).await; - message.remove(server, due.into()).await; - } + pub async fn clear_queue(&self) { + self.account("admin") + .registry_destroy_all(ObjectType::QueuedMessage) + .await; } -} -impl ReportReceiver { pub async fn read_report(&mut self) -> ReportingEvent { match tokio::time::timeout(Duration::from_millis(100), self.report_rx.recv()).await { Ok(Some(event)) => event, @@ -315,7 +238,8 @@ pub trait TestQueueEvent { impl TestQueueEvent for QueueEvent { fn assert_refresh(self) { match self { - QueueEvent::Refresh + QueueEvent::ReloadSettings + | QueueEvent::Refresh | QueueEvent::WorkerDone { status: QueueEventStatus::Deferred, .. @@ -368,14 +292,15 @@ impl TestReportingEvent for ReportingEvent { #[allow(async_fn_in_trait)] pub trait TestMessage { - async fn read_message(&self, core: &QueueReceiver) -> String; - async fn read_lines(&self, core: &QueueReceiver) -> Vec; + async fn read_message(&self, core: &TestServer) -> String; + async fn read_lines(&self, core: &TestServer) -> Vec; } impl TestMessage for MessageWrapper { - async fn read_message(&self, core: &QueueReceiver) -> String { + async fn read_message(&self, core: &TestServer) -> String { String::from_utf8( - core.blob_store + core.server + .blob_store() .get_blob(self.message.blob_hash.as_slice(), 0..usize::MAX) .await .unwrap() @@ -384,7 +309,7 @@ impl TestMessage for MessageWrapper { .unwrap() } - async fn read_lines(&self, core: &QueueReceiver) -> Vec { + async fn read_lines(&self, core: &TestServer) -> Vec { self.read_message(core) .await .split('\n') diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index 48d7312b..3ae5560f 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -4,90 +4,132 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - TempDir, TestSMTP, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::session::{TestSession, VerifyResponse}, + utils::server::TestServerBuilder, }; -use common::Core; -use smtp::core::{Session, State}; +use registry::{ + schema::{ + enums::MtaInboundThrottleKey, + structs::{ + Expression, ExpressionMatch, MtaExtensions, MtaInboundThrottle, MtaStageAuth, + MtaStageRcpt, Rate, + }, + }, + types::{list::List, map::Map}, +}; +use smtp::core::State; use smtp_proto::{RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_SUCCESS}; use std::time::Duration; -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = "john@foobar.org" - -[[directory."local".principals]] -name = "jane" -description = "Jane Doe" -secret = "p4ssw0rd" -email = "jane@foobar.org" - -[[directory."local".principals]] -name = "bill" -description = "Bill Foobar" -secret = "p4ssw0rd" -email = "bill@foobar.org" - -[[directory."local".principals]] -name = "mike" -description = "Mike Foobar" -secret = "p4ssw0rd" -email = "mike@foobar.org" - -[session.rcpt] -directory = "'local'" -max-recipients = [{if = "remote_ip = '10.0.0.1'", then = 3}, - {else = 5}] -relay = [{if = "remote_ip = '10.0.0.1'", then = false}, - {else = true}] - -[session.rcpt.errors] -total = [{if = "remote_ip = '10.0.0.1'", then = 3}, - {else = 100}] -wait = [{if = "remote_ip = '10.0.0.1'", then = '5ms'}, - {else = '1s'}] - -[session.extensions] -dsn = [{if = "remote_ip = '10.0.0.1'", then = false}, - {else = true}] - -[[queue.limiter.inbound]] -match = "remote_ip = '10.0.0.1' && !is_empty(rcpt)" -key = 'sender' -rate = '2/1s' -enable = true - -"#; - #[tokio::test] async fn rcpt() { - // Enable logging - crate::enable_logging(); + let mut test = TestServerBuilder::new("smtp_rcpt_test") + .await + .with_http_listener(19004) + .await + .disable_services() + .build() + .await; - let tmp_dir = TempDir::new("smtp_rcpt_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; + // Create test users + let admin = test.account("admin"); + for (name, secret, description, aliases) in [ + ("john@foobar.org", "12345 + extra safety", "John Doe", &[]), + ("jane@foobar.org", "abcde + extra safety", "Jane Smith", &[]), + ( + "bill@foobar.org", + "p4ssw0rd + extra safety", + "Bill Foobar", + &[], + ), + ( + "mike@foobar.org", + "p4ssw0rd + extra safety", + "Mike Foobar", + &[], + ), + ] { + admin + .create_user_account(name, secret, description, aliases, vec![]) + .await; + } + + // Add test settings + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageRcpt { + allow_relaying: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "false".into(), + }]), + else_: "true".into(), + }, + max_failures: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "3".into(), + }]), + else_: "100".into(), + }, + max_recipients: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "3".into(), + }]), + else_: "5".into(), + }, + wait_on_fail: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "5ms".into(), + }]), + else_: "1s".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaExtensions { + dsn: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "false".into(), + }]), + else_: "true".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaInboundThrottle { + description: None, + enable: true, + key: Map::new(vec![MtaInboundThrottleKey::Sender]), + match_: Expression { + else_: "remote_ip = '10.0.0.1' && !is_empty(rcpt)".into(), + ..Default::default() + }, + rate: Rate { + count: 2, + period: 1000u64.into(), + }, + }) + .await; + admin.reload_settings().await; + test.reload_core(); // RCPT without MAIL FROM - let mut session = Session::test(TestSMTP::from_core(core).server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx1.foobar.org").await; diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index 41439d0e..4abcf882 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -63,8 +63,8 @@ if allof( envelope :localpart :contains "to" ".", #[tokio::test] async fn address_rewrite() { - // Enable logging - crate::enable_logging(); + + // Prepare config let mut config = Config::new(CONFIG).unwrap(); diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index f4d9adc9..f9ec9148 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -95,7 +95,7 @@ member-of = ["sales"] #[tokio::test] async fn sieve_scripts() { - // Enable logging + enable_logging(); // Add test scripts diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index 46391162..f2ca56e4 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -118,8 +118,8 @@ verify = "relaxed" #[tokio::test] async fn sign_and_seal() { - // Enable logging - crate::enable_logging(); + + let tmp_dir = TempDir::new("smtp_sign_test", true); let mut config = Config::new(tmp_dir.update_config(CONFIG.to_string() + SIGNATURES)).unwrap(); diff --git a/tests/src/smtp/inbound/throttle.rs b/tests/src/smtp/inbound/throttle.rs index ead693b4..60738f90 100644 --- a/tests/src/smtp/inbound/throttle.rs +++ b/tests/src/smtp/inbound/throttle.rs @@ -40,8 +40,8 @@ enable = true #[tokio::test] async fn throttle_inbound() { - // Enable logging - crate::enable_logging(); + + let tmp_dir = TempDir::new("smtp_inbound_throttle", true); let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index 52df5c08..bd4b6945 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -63,8 +63,8 @@ expn = [{if = "remote_ip = '10.0.0.1'", then = true}, #[tokio::test] async fn vrfy_expn() { - // Enable logging - crate::enable_logging(); + + let tmp_dir = TempDir::new("smtp_vrfy_test", true); let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index 8850486d..7c578dcf 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -4,13 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - directory::DirectoryStore, - smtp::{ - DnsCache, TempDir, TestSMTP, - session::{TestSession, VerifyResponse}, - }, -}; +use crate::smtp::session::{TestSession, VerifyResponse}; use common::{ Core, expr::{tokenizer::TokenMap, *}, @@ -88,8 +82,8 @@ expect = "0-1-2-2" #[tokio::test] async fn lookup_sql() { - // Enable logging - crate::enable_logging(); + + // Parse settings let temp_dir = TempDir::new("smtp_lookup_tests", true); diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index 094a64e1..3b7ac588 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -78,8 +78,8 @@ schedule = "source + ' ' + received_from_ip + ' ' + received_via_port + ' ' + qu #[tokio::test] async fn strategies() { - // Enable logging - crate::enable_logging(); + + let ipv6: [IpAddr; 4] = [ "a:b::1".parse().unwrap(), diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index b4e465d4..f0fadfd9 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -63,8 +63,8 @@ pub(super) struct List { #[tokio::test] #[serial_test::serial] async fn manage_queue() { - // Enable logging - crate::enable_logging(); + + // Start remote test server let mut remote = TestSMTP::new("smtp_manage_queue_remote", REMOTE).await; diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index 1d3a0575..1af8f115 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -53,8 +53,8 @@ max-size = 1024 #[tokio::test] #[serial_test::serial] async fn manage_reports() { - // Enable logging - crate::enable_logging(); + + // Start reporting service let local = TestSMTP::new("smtp_manage_reports", CONFIG).await; diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index dc678139..bc02b7c7 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -4,101 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{AssertConfig, store::cleanup::store_destroy}; -use common::{ - Core, Data, Inner, Server, - config::{ - server::{Listeners, ServerProtocol}, - smtp::resolver::Tlsa, - }, - ipc::{QueueEvent, ReportingEvent}, - manager::boot::{IpcReceivers, build_ipc}, -}; -use http::HttpSessionManager; -use mail_auth::{MX, Txt, common::resolver::IntoFqdn}; -use session::{DummyIo, TestSession}; -use smtp::core::{Session, SmtpSessionManager}; -use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - path::PathBuf, - sync::Arc, -}; -use store::{BlobStore, Store}; -use tokio::sync::{mpsc, watch}; - -pub mod config; pub mod inbound; -pub mod lookup; +pub mod session; +/*pub mod lookup; pub mod management; pub mod outbound; pub mod queue; pub mod reporting; -pub mod session; - -pub struct TempDir { - pub temp_dir: PathBuf, - pub delete: bool, -} - -impl TempDir { - pub fn new(name: &str, delete: bool) -> TempDir { - let mut temp_dir = std::env::temp_dir(); - temp_dir.push(name); - if !temp_dir.exists() { - let _ = std::fs::create_dir(&temp_dir); - } else if delete { - let _ = std::fs::remove_dir_all(&temp_dir); - let _ = std::fs::create_dir(&temp_dir); - } - TempDir { temp_dir, delete } - } - - pub fn update_config(&self, config: impl AsRef) -> String { - config - .as_ref() - .replace("{TMP}", self.temp_dir.to_str().unwrap()) - } -} - -impl Drop for TempDir { - fn drop(&mut self) { - if self.delete { - let _ = std::fs::remove_dir_all(&self.temp_dir); - } - } -} - -pub fn add_test_certs(config: &str) -> String { - let mut cert_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - cert_path.push("resources"); - cert_path.push("smtp"); - cert_path.push("certs"); - 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()) -} - -pub struct QueueReceiver { - store: Store, - blob_store: BlobStore, - pub queue_rx: mpsc::Receiver, -} - -pub struct ReportReceiver { - pub report_rx: mpsc::Receiver, -} - -pub struct TestSMTP { - pub server: Server, - pub temp_dir: Option, - pub queue_receiver: QueueReceiver, - pub report_receiver: ReportReceiver, -} +*/ const CONFIG: &str = r#" [session.connect] @@ -161,124 +74,3 @@ user = "root" password = "password" "#; - -impl TestSMTP { - pub fn from_core(core: Core) -> Self { - Self::from_core_and_tempdir(core, Default::default(), None) - } - - pub fn inner_with_rxs(&self) -> (Arc, IpcReceivers) { - let (ipc, ipc_rxs) = build_ipc(false); - - ( - Inner { - shared_core: self.server.core.as_ref().clone().into_shared(), - data: Default::default(), - ipc, - cache: Default::default(), - } - .into(), - ipc_rxs, - ) - } - - fn from_core_and_tempdir(core: Core, data: Data, temp_dir: Option) -> Self { - let store = core.storage.data.clone(); - let blob_store = core.storage.blob.clone(); - let shared_core = core.into_shared(); - let (ipc, mut ipc_rxs) = build_ipc(false); - - TestSMTP { - queue_receiver: QueueReceiver { - store, - blob_store, - queue_rx: ipc_rxs.queue_rx.take().unwrap(), - }, - report_receiver: ReportReceiver { - report_rx: ipc_rxs.report_rx.take().unwrap(), - }, - server: Server { - core: shared_core.load_full(), - inner: Inner { - shared_core, - data, - ipc, - cache: Default::default(), - } - .into(), - }, - temp_dir, - } - } - - pub async fn new(name: &str, config: impl AsRef) -> TestSMTP { - Self::with_database(name, config, "rocksdb").await - } - - pub async fn with_database( - name: &str, - config: impl AsRef, - store_id: impl AsRef, - ) -> TestSMTP { - let temp_dir = TempDir::new(name, true); - let mut config = Config::new( - temp_dir - .update_config(add_test_certs(CONFIG) + config.as_ref()) - .replace("{STORE}", store_id.as_ref()), - ) - .unwrap(); - config.resolve_all_macros().await; - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - let data = Data::parse(&mut config); - store_destroy(&core.storage.data).await; - - Self::from_core_and_tempdir(core, data, Some(temp_dir)) - } - - pub async fn start(&self, protocols: &[ServerProtocol]) -> watch::Sender { - // Spawn listeners - let mut config = Config::new(CONFIG).unwrap(); - let mut servers = Listeners::parse(&mut config); - servers.parse_tcp_acceptors(&mut config, self.server.inner.clone()); - - // Filter out protocols - servers - .servers - .retain(|server| protocols.contains(&server.protocol)); - - // Start servers - servers.bind_and_drop_priv(&mut config); - config.assert_no_errors(); - - servers - .spawn(|server, acceptor, shutdown_rx| { - match &server.protocol { - ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn( - SmtpSessionManager::new(self.server.inner.clone()), - self.server.inner.clone(), - acceptor, - shutdown_rx, - ), - ServerProtocol::Http => server.spawn( - HttpSessionManager::new(self.server.inner.clone()), - self.server.inner.clone(), - acceptor, - shutdown_rx, - ), - ServerProtocol::Imap | ServerProtocol::Pop3 | ServerProtocol::ManageSieve => { - unreachable!() - } - }; - }) - .0 - } - - pub fn new_session(&self) -> Session { - Session::test(self.server.clone()) - } - - pub fn build_smtp(&self) -> Server { - self.server.clone() - } -} diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index 369fbcbc..e06914b5 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -74,8 +74,8 @@ return-path = false #[tokio::test] #[serial_test::serial] async fn dane_verify() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_dane_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index 0b944285..ddec2fe4 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -50,8 +50,8 @@ return-path = false #[tokio::test] #[serial_test::serial] async fn extensions() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_ext_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/fallback_relay.rs b/tests/src/smtp/outbound/fallback_relay.rs index 6aca15c9..4f8d1486 100644 --- a/tests/src/smtp/outbound/fallback_relay.rs +++ b/tests/src/smtp/outbound/fallback_relay.rs @@ -52,8 +52,8 @@ chunking = false #[tokio::test] #[serial_test::serial] async fn fallback_relay() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_fallback_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs index 7df3f178..ed0357e8 100644 --- a/tests/src/smtp/outbound/ip_lookup.rs +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -30,8 +30,8 @@ relay = true #[tokio::test] #[serial_test::serial] async fn ip_lookup_strategy() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_iplookup_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs index 0c5b25c2..07e69285 100644 --- a/tests/src/smtp/outbound/lmtp.rs +++ b/tests/src/smtp/outbound/lmtp.rs @@ -75,8 +75,8 @@ allow-invalid-certs = true #[tokio::test] #[serial_test::serial] async fn lmtp_delivery() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("lmtp_delivery_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index d167664f..a893a923 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -60,8 +60,8 @@ return-path = false #[tokio::test] #[serial_test::serial] async fn mta_sts_verify() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_mta_sts_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index abd67792..ec2881c8 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -98,8 +98,8 @@ This is a smuggled message #[tokio::test] #[serial_test::serial] async fn smtp_delivery() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_delivery_remote", REMOTE).await; diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs index 8dd64869..a9f82a2a 100644 --- a/tests/src/smtp/outbound/throttle.rs +++ b/tests/src/smtp/outbound/throttle.rs @@ -63,8 +63,8 @@ enable = true #[tokio::test] async fn throttle_outbound() { - // Enable logging - crate::enable_logging(); + + // Build test message let mut test_message = new_message(0).message; diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index a088f0a2..6815d270 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -48,8 +48,8 @@ chunking = false #[tokio::test] #[serial_test::serial] async fn starttls_optional() { - // Enable logging - crate::enable_logging(); + + // Start test server let mut remote = TestSMTP::new("smtp_starttls_remote", REMOTE).await; diff --git a/tests/src/smtp/queue/concurrent.rs b/tests/src/smtp/queue/concurrent.rs index 1b2daa65..c9f1aa29 100644 --- a/tests/src/smtp/queue/concurrent.rs +++ b/tests/src/smtp/queue/concurrent.rs @@ -51,8 +51,8 @@ const NUM_QUEUES: usize = 10; #[tokio::test(flavor = "multi_thread", worker_threads = 18)] #[serial_test::serial] async fn concurrent_queue() { - // Enable logging - crate::enable_logging(); + + // Start test server let remote = TestSMTP::new("smtp_concurrent_queue_remote", REMOTE).await; diff --git a/tests/src/smtp/queue/dsn.rs b/tests/src/smtp/queue/dsn.rs index 24328e0b..e78bcbac 100644 --- a/tests/src/smtp/queue/dsn.rs +++ b/tests/src/smtp/queue/dsn.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{QueueReceiver, TestSMTP, inbound::sign::SIGNATURES}; +use crate::{smtp::inbound::sign::SIGNATURES, utils::server::TestServer}; use common::config::smtp::queue::{QueueExpiry, QueueName}; use smtp::queue::{ Error, ErrorDetails, HostResponse, Message, MessageWrapper, Recipient, Schedule, Status, @@ -39,8 +39,8 @@ sign = "['rsa']" #[tokio::test] async fn generate_dsn() { - // Enable logging - crate::enable_logging(); + + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("resources"); @@ -171,7 +171,7 @@ async fn generate_dsn() { assert_eq!(queue.len(), 4); } -impl QueueReceiver { +impl TestServer { async fn compare_dsn(&self, message: Message, test: &str) { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("resources"); @@ -180,7 +180,8 @@ impl QueueReceiver { path.push(test); let bytes = self - .blob_store + .server + .blob_store() .get_blob(message.blob_hash.as_slice(), 0..usize::MAX) .await .unwrap() diff --git a/tests/src/smtp/queue/manager.rs b/tests/src/smtp/queue/manager.rs index 28bfed5e..3035f4f0 100644 --- a/tests/src/smtp/queue/manager.rs +++ b/tests/src/smtp/queue/manager.rs @@ -28,8 +28,8 @@ relay = true #[tokio::test] async fn queue_due() { - // Enable logging - crate::enable_logging(); + + let local = TestSMTP::new("smtp_queue_due_test", CONFIG).await; let core = local.build_smtp(); diff --git a/tests/src/smtp/queue/retry.rs b/tests/src/smtp/queue/retry.rs index 786c8817..dbc5b88d 100644 --- a/tests/src/smtp/queue/retry.rs +++ b/tests/src/smtp/queue/retry.rs @@ -51,8 +51,8 @@ schedule = [{if = "sender_domain == 'test.org'", then = "'sender-test'"}, #[tokio::test] async fn queue_retry() { - // Enable logging - crate::enable_logging(); + + // Create temp dir for queue let mut local = TestSMTP::new("smtp_queue_retry_test", CONFIG).await; diff --git a/tests/src/smtp/queue/virtualq.rs b/tests/src/smtp/queue/virtualq.rs index 9382f548..053d1c99 100644 --- a/tests/src/smtp/queue/virtualq.rs +++ b/tests/src/smtp/queue/virtualq.rs @@ -68,8 +68,8 @@ const NUM_QUEUES: usize = 10; #[tokio::test(flavor = "multi_thread", worker_threads = 18)] #[serial_test::serial] async fn virtual_queue() { - // Enable logging - crate::enable_logging(); + + // Start test server let remote = TestSMTP::new("smtp_virtual_queue_remote", REMOTE).await; diff --git a/tests/src/smtp/reporting/analyze.rs b/tests/src/smtp/reporting/analyze.rs index 4c976de7..ed6d1457 100644 --- a/tests/src/smtp/reporting/analyze.rs +++ b/tests/src/smtp/reporting/analyze.rs @@ -23,8 +23,8 @@ store = "1s" #[tokio::test(flavor = "multi_thread")] async fn report_analyze() { - // Enable logging - crate::enable_logging(); + + // Create temp dir for queue let mut local = TestSMTP::new("smtp_analyze_report_test", CONFIG).await; diff --git a/tests/src/smtp/reporting/dmarc.rs b/tests/src/smtp/reporting/dmarc.rs index 3bc9f4be..32aff22d 100644 --- a/tests/src/smtp/reporting/dmarc.rs +++ b/tests/src/smtp/reporting/dmarc.rs @@ -4,12 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - net::IpAddr, - sync::Arc, - time::{Duration, Instant}, +use crate::smtp::{ + inbound::{TestMessage, sign::SIGNATURES}, + session::VerifyResponse, }; - use common::{config::smtp::report::AggregateFrequency, ipc::DmarcEvent}; use mail_auth::{ common::parse::TxtRecordParser, @@ -17,13 +15,12 @@ use mail_auth::{ report::{ActionDisposition, Disposition, DmarcResult, Record, Report}, }; use smtp::reporting::dmarc::DmarcReporting; -use store::write::QueueClass; - -use crate::smtp::{ - DnsCache, TestSMTP, - inbound::{TestMessage, sign::SIGNATURES}, - session::VerifyResponse, +use std::{ + net::IpAddr, + sync::Arc, + time::{Duration, Instant}, }; +use store::write::QueueClass; const CONFIG: &str = r#" [session.rcpt] @@ -48,8 +45,8 @@ sign = "['rsa']" #[tokio::test] async fn report_dmarc() { - // Enable logging - crate::enable_logging(); + + // Create scheduler let mut local = TestSMTP::new("smtp_report_dmarc_test", CONFIG.to_string() + SIGNATURES).await; diff --git a/tests/src/smtp/reporting/scheduler.rs b/tests/src/smtp/reporting/scheduler.rs index a5c9f1bd..a66febaa 100644 --- a/tests/src/smtp/reporting/scheduler.rs +++ b/tests/src/smtp/reporting/scheduler.rs @@ -37,8 +37,8 @@ send = "daily" #[tokio::test] async fn report_scheduler() { - // Enable logging - crate::enable_logging(); + + // Create scheduler let local = TestSMTP::new("smtp_report_queue_test", CONFIG).await; diff --git a/tests/src/smtp/reporting/tls.rs b/tests/src/smtp/reporting/tls.rs index 7dba1f78..432fb2a8 100644 --- a/tests/src/smtp/reporting/tls.rs +++ b/tests/src/smtp/reporting/tls.rs @@ -42,8 +42,8 @@ sign = "['rsa']" #[tokio::test] async fn report_tls() { - // Enable logging - crate::enable_logging(); + + // Create scheduler let mut local = TestSMTP::new("smtp_report_tls_test", CONFIG.to_string() + SIGNATURES).await; diff --git a/tests/src/smtp/session.rs b/tests/src/smtp/session.rs index 8e3b801d..c6c11e81 100644 --- a/tests/src/smtp/session.rs +++ b/tests/src/smtp/session.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use base64::{Engine, engine::general_purpose}; use common::{ Server, config::server::ServerProtocol, @@ -87,6 +88,8 @@ pub trait TestSession { fn write_rx(&mut self, data: &str); async fn rset(&mut self); async fn cmd(&mut self, cmd: &str, expected_code: &str) -> Vec; + async fn auth_plain(&mut self, username: &str, secret: &str, expected_code: &str); + async fn auth_login(&mut self, username: &str, secret: &str, expected_code: &str); async fn ehlo(&mut self, host: &str) -> Vec; async fn mail_from(&mut self, from: &str, expected_code: &str); async fn rcpt_to(&mut self, to: &str, expected_code: &str); @@ -157,6 +160,22 @@ impl TestSession for Session { self.response().assert_code(expected_code) } + async fn auth_plain(&mut self, username: &str, secret: &str, expected_code: &str) { + let cmd = format!( + "AUTH PLAIN {}", + general_purpose::STANDARD.encode(format!("\0{username}\0{secret}")) + ); + self.cmd(&cmd, expected_code).await; + } + + async fn auth_login(&mut self, username: &str, secret: &str, expected_code: &str) { + self.cmd("AUTH LOGIN", "334").await; + self.cmd(&general_purpose::STANDARD.encode(username), "334") + .await; + self.cmd(&general_purpose::STANDARD.encode(secret), expected_code) + .await; + } + async fn ehlo(&mut self, host: &str) -> Vec { self.ingest(format!("EHLO {host}\r\n").as_bytes()) .await diff --git a/tests/src/store/import_export.rs b/tests/src/store/import_export.rs index f7dd108a..f13b47f6 100644 --- a/tests/src/store/import_export.rs +++ b/tests/src/store/import_export.rs @@ -4,12 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - store::TempDir, - utils::{ - cleanup::{store_assert_is_empty, store_destroy}, - server::TestServer, - }, +use crate::utils::{ + cleanup::{store_assert_is_empty, store_destroy}, + server::TestServer, + temp_dir::TempDir, }; use ::registry::schema::enums::CompressionAlgo; use ahash::AHashSet; diff --git a/tests/src/store/mod.rs b/tests/src/store/mod.rs index d226b9d8..fa8c1cb9 100644 --- a/tests/src/store/mod.rs +++ b/tests/src/store/mod.rs @@ -14,10 +14,6 @@ pub mod registry; use crate::utils::server::TestServerBuilder; use std::io::Read; -pub struct TempDir { - pub path: std::path::PathBuf, -} - #[tokio::test(flavor = "multi_thread")] pub async fn store_tests() { let test = TestServerBuilder::new("store_tests").await.build().await; @@ -66,19 +62,3 @@ pub fn deflate_test_resource(name: &str) -> Vec { decoder.read_to_end(&mut result).unwrap(); result } - -impl TempDir { - pub fn new(name: &str, delete_if_exists: bool) -> Self { - let mut path = std::env::temp_dir(); - path.push(name); - if delete_if_exists && path.exists() { - std::fs::remove_dir_all(&path).unwrap(); - } - std::fs::create_dir_all(&path).unwrap(); - Self { path } - } - - pub fn delete(&self) { - std::fs::remove_dir_all(&self.path).unwrap(); - } -} diff --git a/tests/src/system/antispam.rs b/tests/src/system/antispam.rs index dc2a5892..99bfe046 100644 --- a/tests/src/system/antispam.rs +++ b/tests/src/system/antispam.rs @@ -32,6 +32,7 @@ pub async fn test(test: &mut TestServer) { "jdoe@example.org", "this is a very strong password", &[], + "jdoe@example.org", ) .await; let other_account = test @@ -40,6 +41,7 @@ pub async fn test(test: &mut TestServer) { "jane@example.org", "this is a very strong password", &[], + "jane@example.org", ) .await; let client = account.jmap_client().await; diff --git a/tests/src/system/archiving.rs b/tests/src/system/archiving.rs index cdb90a64..3edb0987 100644 --- a/tests/src/system/archiving.rs +++ b/tests/src/system/archiving.rs @@ -52,6 +52,7 @@ pub async fn test(test: &mut TestServer) { "jdoe@example.org", "this is a very strong password", &[], + "jdoe@example.org", ) .await; let jane = test @@ -60,6 +61,7 @@ pub async fn test(test: &mut TestServer) { "jane@example.org", "this is a very strong password", &[], + "jane@example.org", ) .await; let mut john_imap = john.imap_client().await; diff --git a/tests/src/system/authentication.rs b/tests/src/system/authentication.rs index 01797011..ffe4f6be 100644 --- a/tests/src/system/authentication.rs +++ b/tests/src/system/authentication.rs @@ -151,6 +151,7 @@ pub async fn test(test: &TestServer) { "user@example.org", "very strong password indeed", &[], + "User", user_id, ); user.registry_query_ids( diff --git a/tests/src/system/authorization.rs b/tests/src/system/authorization.rs index c1f6d9ca..0eafc3a3 100644 --- a/tests/src/system/authorization.rs +++ b/tests/src/system/authorization.rs @@ -76,6 +76,7 @@ pub async fn test(test: &mut TestServer) { "user@example.org", "this is a very strong password", &[], + "User", user_id, ); @@ -233,6 +234,7 @@ pub async fn test(test: &mut TestServer) { "john@example.org", "this is john's secret", &[], + "john@example.org", ) .await; let jane = test @@ -241,6 +243,7 @@ pub async fn test(test: &mut TestServer) { "jane@example.org", "this is jane's secret", &[], + "jane@example.org", ) .await; let mut john_ids = AHashMap::new(); diff --git a/tests/src/system/crypto.rs b/tests/src/system/crypto.rs index a4790622..eca57aa2 100644 --- a/tests/src/system/crypto.rs +++ b/tests/src/system/crypto.rs @@ -36,6 +36,7 @@ pub async fn test(test: &mut TestServer) { "jdoe@example.org", "this is a very strong password", &[], + "jdoe@example.org", ) .await; let client = account.jmap_client().await; diff --git a/tests/src/system/delivery.rs b/tests/src/system/delivery.rs index 6d47a934..84ed3956 100644 --- a/tests/src/system/delivery.rs +++ b/tests/src/system/delivery.rs @@ -78,6 +78,7 @@ pub async fn test(test: &mut TestServer) { "jdoe@example.org", "this is a very strong password", &["john.doe@example.org"], + "jdoe@example.org", ) .await; let jane = test @@ -86,6 +87,7 @@ pub async fn test(test: &mut TestServer) { "jane.smith@example.org", "this is a very strong password", &[], + "jane.smith@example.org", ) .await; let bill = test @@ -94,6 +96,7 @@ pub async fn test(test: &mut TestServer) { "bill@example.org", "this is a very strong password", &[], + "bill@example.org", ) .await; admin @@ -609,13 +612,7 @@ impl Account { } pub async fn spam_training_samples(&self) -> Vec<(Id, SpamTrainingSample)> { - let ids = self.spam_training_sample_ids().await; - let mut results = Vec::with_capacity(ids.len()); - for id in ids { - let sample = self.registry_get::(id).await; - results.push((id, sample)); - } - results + self.registry_get_all().await } } diff --git a/tests/src/system/directory.rs b/tests/src/system/directory.rs index cb273098..f62adb15 100644 --- a/tests/src/system/directory.rs +++ b/tests/src/system/directory.rs @@ -233,8 +233,13 @@ pub async fn test(test: &TestServer) { assert!(account_cache.id_member_of.as_ref().is_empty()); // Create a masked email - let john = - crate::utils::account::Account::new("johndoe@example.com", "hello world", &[], account_id); + let john = crate::utils::account::Account::new( + "johndoe@example.com", + "hello world", + &[], + "John", + account_id, + ); let response = john .registry_create_many( ObjectType::MaskedEmail, diff --git a/tests/src/system/mod.rs b/tests/src/system/mod.rs index e9275e4d..037f165f 100644 --- a/tests/src/system/mod.rs +++ b/tests/src/system/mod.rs @@ -50,6 +50,7 @@ pub async fn system_tests() { "admin@example.org", "these_pretzels_are_making_me_thirsty", &[], + "Admin", ) .await; test.account("admin") diff --git a/tests/src/system/oidc.rs b/tests/src/system/oidc.rs index e32ad71d..8ef171dc 100644 --- a/tests/src/system/oidc.rs +++ b/tests/src/system/oidc.rs @@ -85,6 +85,7 @@ pub async fn test(test: &mut TestServer) { "user@example.org", "this is a very strong password", &[], + "user@example.org", ) .await; let user_id = user.id(); diff --git a/tests/src/system/purge.rs b/tests/src/system/purge.rs index c1d30206..8a8461cc 100644 --- a/tests/src/system/purge.rs +++ b/tests/src/system/purge.rs @@ -62,6 +62,7 @@ pub async fn test(test: &mut TestServer) { "jdoe@example.org", "this is a very strong password", &[], + "jdoe@example.org", ) .await; let client = account.jmap_client().await; diff --git a/tests/src/system/quota.rs b/tests/src/system/quota.rs index a32d3c4a..f6d75d46 100644 --- a/tests/src/system/quota.rs +++ b/tests/src/system/quota.rs @@ -85,12 +85,14 @@ pub async fn test(test: &mut TestServer) { "user1@example.org", "this is a very strong password1", &[], + "User1", account_id, ); let other_account = Account::new( "user2@example.org", "this is a very strong password2", &[], + "User2", other_account_id, ); diff --git a/tests/src/system/security.rs b/tests/src/system/security.rs index 0cfb3607..b9a821d1 100644 --- a/tests/src/system/security.rs +++ b/tests/src/system/security.rs @@ -70,6 +70,7 @@ pub async fn test(test: &mut TestServer) { "user@example.org", "this is a very strong password", &[], + "user@example.org", ) .await; let user_id = user.id(); diff --git a/tests/src/system/tenant.rs b/tests/src/system/tenant.rs index 12067764..8bc4f559 100644 --- a/tests/src/system/tenant.rs +++ b/tests/src/system/tenant.rs @@ -89,12 +89,14 @@ pub async fn test(test: &mut TestServer) { "admin@tenantx.org", "tenant x secret", &[], + "Admin X", tenant_x_ids[&ObjectType::TaskManager], ); let admin_y = crate::utils::account::Account::new( "admin@tenanty.org", "tenant y secret", &[], + "Admin Y", tenant_y_ids[&ObjectType::TaskManager], ); assert_eq!( diff --git a/tests/src/telemetry/mod.rs b/tests/src/telemetry/mod.rs index e505fd39..86201fe3 100644 --- a/tests/src/telemetry/mod.rs +++ b/tests/src/telemetry/mod.rs @@ -47,6 +47,7 @@ pub async fn telemetry_tests() { "admin@example.org", "these_pretzels_are_making_me_thirsty", &[], + "Admin", ) .await; test.account("admin") diff --git a/tests/src/telemetry/tracing.rs b/tests/src/telemetry/tracing.rs index 92a38242..df54a43a 100644 --- a/tests/src/telemetry/tracing.rs +++ b/tests/src/telemetry/tracing.rs @@ -22,9 +22,10 @@ pub async fn test(test: &TestServer) { let account = test .create_user_account( "admin@example.org", - "jdoe@example.org", + "admin@example.org", "this is a very strong password", &[], + "admin@example.org", ) .await; diff --git a/tests/src/telemetry/webhooks.rs b/tests/src/telemetry/webhooks.rs index c7f716f0..005b840f 100644 --- a/tests/src/telemetry/webhooks.rs +++ b/tests/src/telemetry/webhooks.rs @@ -81,6 +81,7 @@ pub async fn test(test: &TestServer) { "jdoe@example.org", "this is a very strong password", &["john.doe@example.org"], + "jdoe@example.org", ) .await; let mut lmtp = SmtpConnection::connect().await; diff --git a/tests/src/utils/account.rs b/tests/src/utils/account.rs index aa5e4724..515e8246 100644 --- a/tests/src/utils/account.rs +++ b/tests/src/utils/account.rs @@ -26,8 +26,10 @@ pub struct Account { name: &'static str, secret: &'static str, emails: &'static [&'static str], + description: &'static str, id: Id, id_string: String, + pub http_listener_port: u16, } impl TestServer { @@ -37,12 +39,29 @@ impl TestServer { name: &'static str, secret: &'static str, aliases: &'static [&'static str], + description: &'static str, ) -> Account { self.account(using_account) - .create_user_account(name, secret, None, aliases, vec![]) + .create_user_account(name, secret, description, aliases, vec![]) .await } + pub async fn create_admin_account(&self, name: &'static str) -> Account { + let admin = self + .create_user_account( + "admin", + name, + "these_pretzels_are_making_me_thirsty", + &[], + "Admin", + ) + .await; + self.account("admin") + .assign_roles_to_account(admin.id(), &["user", "system"]) + .await; + admin + } + pub fn insert_account(&mut self, account: Account) { self.accounts.insert(account.name(), account); } @@ -53,14 +72,17 @@ impl Account { name: &'static str, secret: &'static str, emails: &'static [&'static str], + description: &'static str, id: Id, ) -> Self { Self { name, secret, emails, + description, id, id_string: id.to_string(), + http_listener_port: 8899, } } @@ -79,6 +101,11 @@ impl Account { pub fn name(&self) -> &'static str { self.name } + + pub fn description(&self) -> &'static str { + self.description + } + pub fn secret(&self) -> &'static str { self.secret } @@ -107,7 +134,7 @@ impl Account { &self, name: &'static str, secret: &'static str, - description: Option<&'static str>, + description: &'static str, aliases: &'static [&'static str], extra_permissions: Vec, ) -> Account { @@ -128,7 +155,7 @@ impl Account { .rsplit_once('@') .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) .unwrap(); - let account_aliases = aliases.iter().map(|email| { + let account_aliases = aliases.iter().filter(|email| **email != name).map(|email| { let (name, domain_id) = email .rsplit_once('@') .map(|(name, domain)| (name.to_string(), *domains.get(domain).unwrap())) @@ -150,7 +177,7 @@ impl Account { ..Default::default() })]), aliases: List::from_iter(account_aliases), - description: description.map(|d| d.to_string()), + description: description.to_string().into(), permissions: Permissions::Merge(PermissionsList { disabled_permissions: Default::default(), enabled_permissions: Map::new(extra_permissions), @@ -159,13 +186,15 @@ impl Account { })) .await; - Account::new(name, secret, aliases, account_id) + let mut account = Account::new(name, secret, aliases, description, account_id); + account.http_listener_port = self.http_listener_port; + account } pub async fn create_group_account( &self, name: &'static str, - description: Option<&'static str>, + description: &'static str, aliases: &'static [&'static str], ) -> Account { let mut domains = AHashMap::from_iter( @@ -203,12 +232,12 @@ impl Account { name: account_name, domain_id, aliases: List::from_iter(account_aliases), - description: description.map(|d| d.to_string()), + description: description.to_string().into(), ..Default::default() })) .await; - Account::new(name, "", aliases, account_id) + Account::new(name, "", aliases, description, account_id) } pub async fn create_domain(&self, name: &'static str) -> Id { @@ -269,7 +298,7 @@ impl Account { .timeout(Duration::from_secs(3600)) .accept_invalid_certs(true) .follow_redirects(["127.0.0.1"]) - .connect("https://127.0.0.1:8899") + .connect(&format!("https://127.0.0.1:{}", self.http_listener_port)) .await .unwrap(); client.set_default_account_id(self.id_string()); diff --git a/tests/src/utils/imap.rs b/tests/src/utils/imap.rs index 921e3af8..cca19a89 100644 --- a/tests/src/utils/imap.rs +++ b/tests/src/utils/imap.rs @@ -155,6 +155,8 @@ pub trait AssertResult: Sized { fn assert_response_code(self, code: &str) -> Self; fn assert_contains(self, text: &str) -> Self; + fn assert_contains_any(self, expected_texts: &[&str]) -> Self; + fn assert_not_contains(self, expected_text: &str) -> Self; fn assert_count(self, text: &str, occurrences: usize) -> Self; fn assert_equals(self, text: &str) -> Self; fn into_response_code(self) -> String; @@ -208,13 +210,39 @@ impl AssertResult for Vec { self } - fn assert_contains(self, text: &str) -> Self { - for line in &self { - if line.contains(text) { - return self; - } + fn assert_contains(self, expected_text: &str) -> Self { + if self.iter().any(|line| line.contains(expected_text)) { + self + } else { + panic!("Expected {:?} but got {}.", expected_text, self.join("\n")); + } + } + + fn assert_contains_any(self, expected_texts: &[&str]) -> Self { + if self + .iter() + .any(|line| expected_texts.iter().any(|text| line.contains(text))) + { + self + } else { + panic!( + "Expected any of {:?} but got {}.", + expected_texts, + self.join("\n") + ); + } + } + + fn assert_not_contains(self, expected_text: &str) -> Self { + if !self.iter().any(|line| line.contains(expected_text)) { + self + } else { + panic!( + "Not expecting {:?} but got it {}.", + expected_text, + self.join("\n") + ); } - panic!("Expected response to contain {:?}, got {:?}", text, self); } fn assert_count(self, text: &str, occurrences: usize) -> Self { diff --git a/tests/src/utils/jmap.rs b/tests/src/utils/jmap.rs index 897a5d77..57c537bd 100644 --- a/tests/src/utils/jmap.rs +++ b/tests/src/utils/jmap.rs @@ -279,7 +279,10 @@ impl Account { .default_headers(headers) .build() .unwrap() - .post("https://127.0.0.1:8899/jmap") + .post(format!( + "https://127.0.0.1:{}/jmap", + self.http_listener_port + )) .body(body.to_string()) .send() .await @@ -312,7 +315,10 @@ impl Account { .default_headers(headers) .build() .unwrap() - .get("https://127.0.0.1:8899/jmap/session") + .get(format!( + "https://127.0.0.1:{}/jmap/session", + self.http_listener_port + )) .send() .await .unwrap() diff --git a/tests/src/utils/mod.rs b/tests/src/utils/mod.rs index 3e45544c..3766ba68 100644 --- a/tests/src/utils/mod.rs +++ b/tests/src/utils/mod.rs @@ -14,6 +14,8 @@ pub mod jmap; pub mod pop3; pub mod registry; pub mod server; +pub mod sieve; pub mod smtp; pub mod storage; +pub mod temp_dir; pub mod webdav; diff --git a/tests/src/utils/pop3.rs b/tests/src/utils/pop3.rs index af368926..dfa40867 100644 --- a/tests/src/utils/pop3.rs +++ b/tests/src/utils/pop3.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use base64::{Engine, engine::general_purpose}; use mail_send::smtp::tls::build_tls_connector; use rustls_pki_types::ServerName; use std::time::Duration; @@ -36,19 +37,20 @@ impl Pop3Connection { .await .unwrap(), ); - Pop3Connection { + + let mut conn = Pop3Connection { reader: BufReader::new(reader).lines(), writer, - } + }; + + conn.assert_read(ResponseType::Ok).await; + conn } - pub async fn connect_and_login() -> Self { - let mut pop3 = Self::connect().await; - pop3.assert_read(ResponseType::Ok).await; - pop3.send("AUTH PLAIN AHBvcHBlckBleGFtcGxlLmNvbQBzZWNyZXQ=") - .await; - pop3.assert_read(ResponseType::Ok).await; - pop3 + pub async fn authenticate(&mut self, user: &str, pass: &str) { + let creds = general_purpose::STANDARD.encode(format!("\0{user}\0{pass}")); + self.send(&format!("AUTH PLAIN {creds}")).await; + self.assert_read(ResponseType::Ok).await; } pub async fn assert_read(&mut self, rt: ResponseType) -> Vec { diff --git a/tests/src/utils/registry.rs b/tests/src/utils/registry.rs index 891e5312..14656a01 100644 --- a/tests/src/utils/registry.rs +++ b/tests/src/utils/registry.rs @@ -64,6 +64,28 @@ impl Account { }) } + pub async fn registry_get_all(&self) -> Vec<(Id, T)> { + let name = T::OBJECT.as_str(); + + let response = self + .jmap_get_account( + self, + format!("x:{name}"), + Vec::<&str>::new(), + Vec::::new(), + ) + .await; + let mut items = Vec::with_capacity(response.list().len()); + for item in response.list() { + let id = item.object_id(); + let item = serde_json::from_str(&item.to_string()).unwrap_or_else(|_| { + panic!("Failed to deserialize {item}"); + }); + items.push((id, item)); + } + items + } + pub async fn registry_get_many( &self, object_type: ObjectType, diff --git a/tests/src/utils/server.rs b/tests/src/utils/server.rs index 6118e2a1..7ca5e08f 100644 --- a/tests/src/utils/server.rs +++ b/tests/src/utils/server.rs @@ -6,25 +6,32 @@ use crate::{ AssertConfig, - store::TempDir, + smtp::session::{DummyIo, TestSession}, utils::{ account::Account, cleanup::{search_store_destroy, store_blob_expire_all, store_destroy}, registry::UnwrapRegistryId, storage::{RegistryEnvStores, assert_is_empty, build_data_store, wait_for_tasks}, + temp_dir::TempDir, }, }; use ahash::AHashMap; use common::{ - BuildServer, Caches, Core, Data, Inner, Server, + BuildServer, Caches, Core, Data, DavResources, Inner, Server, auth::FALLBACK_ADMIN_ID, config::{ server::{Listeners, ServerProtocol}, storage::Storage, telemetry::Telemetry, }, - manager::{boot::build_ipc, defaults::BootstrapDefaults}, + ipc::{QueueEvent, ReportingEvent}, + manager::{ + boot::{IpcReceivers, build_ipc}, + defaults::BootstrapDefaults, + }, }; +use email::message::metadata::MessageMetadata; +use groupware::cache::GroupwareCache; use http::HttpSessionManager; use imap::core::ImapSessionManager; use jmap_client::client::Client; @@ -41,25 +48,29 @@ use registry::{ use services::{SpawnServices, broadcast::subscriber::spawn_broadcast_subscriber}; use smtp::{ SpawnQueueManager, - core::SmtpSessionManager, + core::{Session, SmtpSessionManager}, queue::{ - manager::Queue, + manager::{Queue, SpawnQueue}, spool::{QueuedMessages, SmtpSpool}, }, + reporting::scheduler::SpawnReport, }; use std::{str::FromStr, sync::Arc}; use store::{ - RegistryStore, Store, + RegistryStore, Store, ValueKey, registry::{bootstrap::Bootstrap, write::RegistryWrite}, + write::{AlignedBytes, Archive}, }; use tokio::sync::{mpsc, watch}; use trc::EventType; -use types::id::Id; +use types::{collection::Collection, field::EmailField, id::Id}; pub struct TestServer { pub server: Server, pub accounts: AHashMap<&'static str, Account>, pub temp_dir: TempDir, + pub queue_rx: mpsc::Receiver, + pub report_rx: mpsc::Receiver, shutdown_tx: watch::Sender, reset: bool, } @@ -67,8 +78,12 @@ pub struct TestServer { pub struct TestServerBuilder { bootstrap: Bootstrap, temp_dir: TempDir, + http_listener_port: u16, reset: bool, logging_enabled: bool, + capture_queue: bool, + capture_reporting: bool, + disable_services: bool, } impl TestServerBuilder { @@ -99,9 +114,13 @@ impl TestServerBuilder { RegistryStore::new(&path, store, "mail.example.org".to_string(), 1, None).await, ) .await, + http_listener_port: 8899, temp_dir, reset, logging_enabled: false, + capture_queue: false, + capture_reporting: false, + disable_services: false, } } @@ -127,6 +146,21 @@ impl TestServerBuilder { .await } + pub async fn with_http_listener(mut self, port: u16) -> Self { + self.http_listener_port = port; + self.with_listener(NetworkListenerProtocol::Http, "jmap", port, true) + .await + .with_object(Http { + base_url: Expression { + else_: format!("'https://127.0.0.1:{}'", port), + + ..Default::default() + }, + ..Default::default() + }) + .await + } + pub async fn with_listener( self, protocol: NetworkListenerProtocol, @@ -158,6 +192,21 @@ impl TestServerBuilder { self } + pub fn capture_queue(mut self) -> Self { + self.capture_queue = true; + self + } + + pub fn capture_reporting(mut self) -> Self { + self.capture_reporting = true; + self + } + + pub fn disable_services(mut self) -> Self { + self.disable_services = true; + self + } + pub async fn insert_object(&self, object: impl Into) -> Id { self.bootstrap .registry @@ -244,8 +293,29 @@ impl TestServerBuilder { // Start services self.bootstrap.assert_no_errors(); - ipc_rxs.spawn_queue_manager(inner.clone()); - ipc_rxs.spawn_services(inner.clone()); + if !self.disable_services { + ipc_rxs.spawn_services(inner.clone()); + } + + // Spawn queue manager if not capturing + let (_, mut queue_rx) = mpsc::channel(100); + let (_, mut report_rx) = mpsc::channel(100); + if !self.capture_queue && !self.capture_reporting { + ipc_rxs.spawn_queue_manager(inner.clone()); + } else { + let queue_rx_ = ipc_rxs.queue_rx.take().unwrap(); + let report_rx_ = ipc_rxs.report_rx.take().unwrap(); + if !self.capture_queue { + queue_rx_.spawn(inner.clone()); + } else { + queue_rx = queue_rx_; + } + if !self.capture_reporting { + report_rx_.spawn(inner.clone()); + } else { + report_rx = report_rx_; + } + } // Spawn servers let (shutdown_tx, shutdown_rx) = servers.spawn(|server, acceptor, shutdown_rx| { @@ -284,17 +354,27 @@ impl TestServerBuilder { }); // Start broadcast subscriber - spawn_broadcast_subscriber(inner.clone(), shutdown_rx); + if !self.disable_services { + spawn_broadcast_subscriber(inner.clone(), shutdown_rx); + } tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let mut admin = Account::new( + "admin", + "popolna_zapora", + &[], + "Recovery Admin", + Id::from(FALLBACK_ADMIN_ID), + ); + admin.http_listener_port = self.http_listener_port; + TestServer { server: inner.build_server(), temp_dir: self.temp_dir, - accounts: AHashMap::from_iter([( - "admin", - Account::new("admin", "popolna_zapora", &[], Id::from(FALLBACK_ADMIN_ID)), - )]), + accounts: AHashMap::from_iter([("admin", admin)]), + queue_rx, + report_rx, shutdown_tx, reset: self.reset, } @@ -302,6 +382,10 @@ impl TestServerBuilder { } impl TestServer { + pub fn reload_core(&mut self) { + self.server = self.server.inner.build_server(); + } + pub fn account(&self, name: &str) -> &Account { self.accounts.get(name).unwrap() } @@ -339,6 +423,47 @@ impl TestServer { let _ = self.shutdown_tx.send(true); } + pub fn new_mta_session(&self) -> Session { + Session::test(self.server.clone()) + } + + pub async fn resources(&self, name: &'static str, collection: Collection) -> Arc { + let account_id = self.account(name).id().document_id(); + self.server + .fetch_dav_resources(account_id, account_id, collection.into()) + .await + .unwrap() + } + + pub async fn fetch_email(&self, account_id: u32, document_id: u32) -> Vec { + let metadata_ = self + .server + .store() + .get_value::>(ValueKey::property( + account_id, + Collection::Email, + document_id, + EmailField::Metadata, + )) + .await + .unwrap() + .unwrap(); + self.server + .blob_store() + .get_blob( + metadata_ + .unarchive::() + .unwrap() + .blob_hash + .0 + .as_slice(), + 0..usize::MAX, + ) + .await + .unwrap() + .unwrap() + } + pub async fn all_queued_messages(&self) -> QueuedMessages { self.server .next_event(&mut Queue::new( @@ -352,6 +477,23 @@ impl TestServer { self.wait_for_tasks().await; account.jmap_client().await.destroy_all_mailboxes().await; } + + pub async fn inner_with_rxs(&self) -> (Arc, IpcReceivers) { + let (ipc, ipc_rxs) = build_ipc(false); + + let mut bp = Bootstrap::new_uninitialized(self.server.registry().clone()); + + ( + Inner { + shared_core: self.server.core.as_ref().clone().into_shared(), + data: Default::default(), + ipc, + cache: Caches::parse(&mut bp).await, + } + .into(), + ipc_rxs, + ) + } } impl Account { diff --git a/tests/src/utils/sieve.rs b/tests/src/utils/sieve.rs new file mode 100644 index 00000000..8e5f1532 --- /dev/null +++ b/tests/src/utils/sieve.rs @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use base64::{Engine, engine::general_purpose}; +use imap_proto::ResponseType; +use mail_send::smtp::tls::build_tls_connector; +use rustls_pki_types::ServerName; +use std::time::Duration; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, + net::TcpStream, +}; +use tokio_rustls::client::TlsStream; + +pub struct SieveConnection { + reader: Lines>>>, + writer: WriteHalf>, +} + +impl SieveConnection { + pub async fn connect() -> Self { + let (reader, writer) = tokio::io::split( + build_tls_connector(true) + .connect( + ServerName::try_from("imap.example.org").unwrap().to_owned(), + TcpStream::connect("127.0.0.1:4190").await.unwrap(), + ) + .await + .unwrap(), + ); + SieveConnection { + reader: BufReader::new(reader).lines(), + writer, + } + } + + pub async fn authenticate(&mut self, user: &str, pass: &str) { + let creds = general_purpose::STANDARD.encode(format!("\0{user}\0{pass}")); + self.send(&format!( + "AUTHENTICATE PLAIN {{{}+}}\r\n{creds}", + creds.len() + )) + .await; + self.assert_read(ResponseType::Ok).await; + } + + pub async fn assert_read(&mut self, rt: ResponseType) -> Vec { + let lines = self.read().await; + let mut buf = Vec::with_capacity(10); + rt.serialize(&mut buf); + if lines + .last() + .unwrap() + .starts_with(&String::from_utf8(buf).unwrap()) + { + lines + } else { + panic!("Expected {:?} from server but got: {:?}", rt, lines); + } + } + + pub async fn read(&mut self) -> Vec { + let mut lines = Vec::new(); + loop { + match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await { + Ok(Ok(Some(line))) => { + let is_done = + line.starts_with("OK") || line.starts_with("NO") || line.starts_with("BYE"); + //println!("<- {:?}", line); + lines.push(line); + if is_done { + return lines; + } + } + Ok(Ok(None)) => { + panic!("Invalid response: {:?}.", lines); + } + Ok(Err(err)) => { + panic!("Connection broken: {} ({:?})", err, lines); + } + Err(_) => panic!("Timeout while waiting for server response: {:?}", lines), + } + } + } + + pub async fn send(&mut self, text: &str) { + //println!("-> {:?}", text); + self.writer.write_all(text.as_bytes()).await.unwrap(); + self.writer.write_all(b"\r\n").await.unwrap(); + } + + pub async fn send_raw(&mut self, text: &str) { + //println!("-> {:?}", text); + self.writer.write_all(text.as_bytes()).await.unwrap(); + } + + pub async fn send_literal(&mut self, text: &str, literal: &str) { + self.send(&format!("{}{{{}+}}\r\n{}", text, literal.len(), literal)) + .await; + } +} diff --git a/tests/src/utils/temp_dir.rs b/tests/src/utils/temp_dir.rs new file mode 100644 index 00000000..aeb818f5 --- /dev/null +++ b/tests/src/utils/temp_dir.rs @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub struct TempDir { + pub path: std::path::PathBuf, + pub delete: bool, +} + +impl TempDir { + pub fn new(name: &str, delete_if_exists: bool) -> Self { + let mut path = std::env::temp_dir(); + path.push(name); + if delete_if_exists && path.exists() { + std::fs::remove_dir_all(&path).unwrap(); + } + std::fs::create_dir_all(&path).unwrap(); + Self { + path, + delete: delete_if_exists, + } + } + + pub fn delete(&self) { + std::fs::remove_dir_all(&self.path).unwrap(); + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + if self.delete { + let _ = std::fs::remove_dir_all(&self.path); + } + } +} diff --git a/tests/src/utils/webdav.rs b/tests/src/utils/webdav.rs index b7dc94d0..7961f743 100644 --- a/tests/src/utils/webdav.rs +++ b/tests/src/utils/webdav.rs @@ -20,18 +20,18 @@ use store::rand::{Rng, distr::Alphanumeric, rng}; #[allow(dead_code)] #[derive(Debug)] pub struct DummyWebDavClient { - account_id: u32, - name: &'static str, - email: &'static str, - credentials: String, + pub account_id: u32, + pub name: &'static str, + pub email: &'static str, + pub credentials: String, } #[derive(Debug)] pub struct DavResponse { - headers: AHashMap, - status: StatusCode, - body: Result, - xml: Vec<(String, String)>, + pub headers: AHashMap, + pub status: StatusCode, + pub body: Result, + pub xml: Vec<(String, String)>, } #[derive(Debug)] @@ -657,6 +657,10 @@ impl DavResponse { self.header("etag") } + pub fn lock_token(&self) -> &str { + self.value("D:prop.D:lockdiscovery.D:activelock.D:locktoken.D:href") + } + pub fn sync_token(&self) -> &str { self.find_keys("D:multistatus.D:sync-token") .next() diff --git a/tests/src/webdav/acl.rs b/tests/src/webdav/acl.rs index f8b98722..b652b698 100644 --- a/tests/src/webdav/acl.rs +++ b/tests/src/webdav/acl.rs @@ -4,17 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; use dav_proto::schema::property::{DavProperty, WebDavProperty}; use groupware::DavResourceName; use hyper::StatusCode; -use crate::webdav::GenerateTestDavResource; - -use super::{DavResponse, DummyWebDavClient, WebDavTest}; - -pub async fn test(test: &WebDavTest) { - let owner_client = test.client("bill"); - let sharee_client = test.client("john"); +pub async fn test(test: &TestServer) { + let owner_client = test.account("bill@example.com").webdav_client(); + let sharee_client = test.account("john@example.com").webdav_client(); for resource_type in [ DavResourceName::File, @@ -23,10 +20,16 @@ pub async fn test(test: &WebDavTest) { ] { println!("Running ACL tests ({})...", resource_type.base_path()); let is_file = resource_type == DavResourceName::File; - let sharee_principal = format!("{}/john/", DavResourceName::Principal.base_path()); - let sharee_base_path = format!("{}/john/", resource_type.base_path()); - let owner_principal = format!("{}/bill/", DavResourceName::Principal.base_path()); - let owner_base_path = format!("{}/bill/", resource_type.base_path()); + let sharee_principal = format!( + "{}/john%40example.com/", + DavResourceName::Principal.base_path() + ); + let sharee_base_path = format!("{}/john%40example.com/", resource_type.base_path()); + let owner_principal = format!( + "{}/bill%40example.com/", + DavResourceName::Principal.base_path() + ); + let owner_base_path = format!("{}/bill%40example.com/", resource_type.base_path()); // Create a resource for the owner let owner_folder = format!("{owner_base_path}test-shared/"); diff --git a/tests/src/webdav/basic.rs b/tests/src/webdav/basic.rs index bec2f342..0e3ba73e 100644 --- a/tests/src/webdav/basic.rs +++ b/tests/src/webdav/basic.rs @@ -4,15 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::utils::server::TestServer; use dav_proto::Depth; use hyper::StatusCode; -use super::WebDavTest; - -pub async fn test(test: &WebDavTest) { +pub async fn test(test: &TestServer) { println!("Running basic tests..."); - let john = test.client("john"); - let jane = test.client("jane"); + let john = test.account("john@example.com").webdav_client(); + let jane = test.account("jane@example.com").webdav_client(); // Test OPTIONS request john.request("OPTIONS", "/dav/file", "") @@ -37,18 +36,22 @@ pub async fn test(test: &WebDavTest) { .await .with_values( "D:multistatus.D:response.D:href", - ["/dav/card/", "/dav/card/john/"], + ["/dav/card/", "/dav/card/john%40example.com/"], ); jane.request("PROPFIND", "/.well-known/caldav", "") .await .with_values( "D:multistatus.D:response.D:href", - ["/dav/cal/", "/dav/cal/jane/", "/dav/cal/support/"], + [ + "/dav/cal/", + "/dav/cal/jane%40example.com/", + "/dav/cal/support%40example.com/", + ], ); // Test 404 responses jane.sync_collection( - "/dav/cal/jane/default/", + "/dav/cal/jane%40example.com/default/", "", Depth::Infinity, None, @@ -56,19 +59,19 @@ pub async fn test(test: &WebDavTest) { ) .await; jane.sync_collection( - "/dav/cal/jane/test-404/", + "/dav/cal/jane%40example.com/test-404/", "", Depth::Infinity, None, ["D:getetag"], ) .await; - jane.request("PROPFIND", "/dav/cal/jane/default/", "") + jane.request("PROPFIND", "/dav/cal/jane%40example.com/default/", "") .await .with_status(StatusCode::MULTI_STATUS); jane.request( "REPORT", - "/dav/cal/jane/default/", + "/dav/cal/jane%40example.com/default/", concat!( r#""#, @@ -81,7 +84,7 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::MULTI_STATUS); jane.request( "REPORT", - "/dav/cal/jane/test-404/", + "/dav/cal/jane%40example.com/test-404/", concat!( r#""#, @@ -92,12 +95,13 @@ pub async fn test(test: &WebDavTest) { ) .await .with_status(StatusCode::MULTI_STATUS); - jane.request("PROPFIND", "/dav/cal/jane/test-404/", "") + jane.request("PROPFIND", "/dav/cal/jane%40example.com/test-404/", "") .await .with_status(StatusCode::NOT_FOUND); john.delete_default_containers().await; jane.delete_default_containers().await; - jane.delete_default_containers_by_account("support").await; + jane.delete_default_containers_by_account("support@example.com") + .await; test.assert_is_empty().await; } diff --git a/tests/src/webdav/cal_alarm.rs b/tests/src/webdav/cal_alarm.rs index 43203f64..aa9f9024 100644 --- a/tests/src/webdav/cal_alarm.rs +++ b/tests/src/webdav/cal_alarm.rs @@ -4,20 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; -use crate::jmap::mail::mailbox::destroy_all_mailboxes_for_account; +use crate::utils::server::TestServer; use email::cache::MessageCacheFetch; use hyper::StatusCode; use mail_parser::{DateTime, MessageParser}; use store::write::now; -pub async fn test(test: &WebDavTest) { +pub async fn test(test: &TestServer) { println!("Running calendar e-mail alarms tests..."); - let client = test.client("john"); + let account = test.account("john@example.com"); + let client = account.webdav_client(); client .request_with_headers( "PUT", - "/dav/cal/john/default/its-alarming-how-charming-i-feel.ics", + "/dav/cal/john%40example.com/default/its-alarming-how-charming-i-feel.ics", [("content-type", "text/calendar; charset=utf-8")], TEST_ALARM_1.replace( "$START", @@ -74,7 +74,7 @@ pub async fn test(test: &WebDavTest) { } assert!( contents.contains(concat!( - "/dav/cal/john/default/", + "/dav/cal/john%40example.com/default/", "its-alarming-how-charming-i-feel.ics" )), "failed for {contents}" @@ -82,7 +82,7 @@ pub async fn test(test: &WebDavTest) { } client.delete_default_containers().await; - destroy_all_mailboxes_for_account(client.account_id).await; + test.destroy_all_mailboxes(account).await; test.assert_is_empty().await } diff --git a/tests/src/webdav/cal_query.rs b/tests/src/webdav/cal_query.rs index 11754e7f..7d1c5a50 100644 --- a/tests/src/webdav/cal_query.rs +++ b/tests/src/webdav/cal_query.rs @@ -4,7 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; +use crate::utils::server::TestServer; + use ahash::AHashSet; use calcard::{common::timezone::Tz, icalendar::ICalendar}; use groupware::{ @@ -15,10 +16,10 @@ use hyper::StatusCode; use store::write::serialize::rkyv_unarchive; use types::TimeRange; -pub async fn test(test: &WebDavTest) { +pub async fn test(test: &TestServer) { println!("Running REPORT calendar-query & free-busy-query tests..."); - let client = test.client("john"); - let cal_path = format!("{}/john/default/", DavResourceName::Cal.base_path()); + let client = test.account("john@example.com").webdav_client(); + let cal_path = format!("{}/john%40example.com/default/", DavResourceName::Cal.base_path()); #[allow(clippy::never_loop)] for (num, ics) in [ @@ -324,7 +325,7 @@ fn roundtrip_expansion(ics: &str, ignore_errors: bool) { fn rfc_file_name(num: usize) -> String { format!( - "{}/john/default/abcd{num}.ics", + "{}/john%40example.com/default/abcd{num}.ics", DavResourceName::Cal.base_path() ) } diff --git a/tests/src/webdav/cal_scheduling.rs b/tests/src/webdav/cal_scheduling.rs index 983e3122..19bea54e 100644 --- a/tests/src/webdav/cal_scheduling.rs +++ b/tests/src/webdav/cal_scheduling.rs @@ -4,11 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; use crate::{ - jmap::mail::mailbox::destroy_all_mailboxes_for_account, - webdav::{DummyWebDavClient, prop::ALL_DAV_PROPERTIES}, + utils::{server::TestServer, webdav::DummyWebDavClient}, + webdav::prop::ALL_DAV_PROPERTIES, }; + use calcard::{ common::timezone::Tz, icalendar::{ @@ -16,7 +16,6 @@ use calcard::{ ICalendarProperty, ICalendarRecurrenceRule, ICalendarWeekday, }, }; -use common::{Server, auth::AccessToken}; use dav_proto::schema::property::{CalDavProperty, DavProperty, WebDavProperty}; use email::cache::MessageCacheFetch; use groupware::{ @@ -30,23 +29,30 @@ use std::str::FromStr; use store::write::now; use types::collection::SyncCollection; -pub async fn test(test: &WebDavTest) { +pub async fn test(test: &TestServer) { println!("Running calendar scheduling tests..."); - let bill_client = test.client("bill"); - let jane_client = test.client("jane"); - let john_client = test.client("john"); + let bill = test.account("bill@example.com"); + let jane = test.account("jane@example.com"); + let john = test.account("john@example.com"); + let bill_client = bill.webdav_client(); + let jane_client = jane.webdav_client(); + let john_client = john.webdav_client(); // Validate hierarchy of scheduling resources let response = jane_client - .propfind_with_headers("/dav/itip/jane/", ALL_DAV_PROPERTIES, [("depth", "1")]) + .propfind_with_headers( + "/dav/itip/jane%40example.com/", + ALL_DAV_PROPERTIES, + [("depth", "1")], + ) .await; let properties = response .with_hrefs([ - "/dav/itip/jane/", - "/dav/itip/jane/inbox/", - "/dav/itip/jane/outbox/", + "/dav/itip/jane%40example.com/", + "/dav/itip/jane%40example.com/inbox/", + "/dav/itip/jane%40example.com/outbox/", ]) - .properties("/dav/itip/jane/inbox/"); + .properties("/dav/itip/jane%40example.com/inbox/"); // Validate schedule inbox properties properties @@ -56,7 +62,7 @@ pub async fn test(test: &WebDavTest) { .get(DavProperty::CalDav( CalDavProperty::ScheduleDefaultCalendarURL, )) - .with_values(["D:href:/dav/cal/jane/default/"]) + .with_values(["D:href:/dav/cal/jane%40example.com/default/"]) .with_status(StatusCode::OK); properties .get(DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet)) @@ -101,7 +107,7 @@ pub async fn test(test: &WebDavTest) { ]); // Validate schedule outbox properties - let properties = response.properties("/dav/itip/jane/outbox/"); + let properties = response.properties("/dav/itip/jane%40example.com/outbox/"); properties .get(DavProperty::WebDav(WebDavProperty::ResourceType)) .with_values(["D:collection", "A:schedule-outbox"]); @@ -164,7 +170,7 @@ pub async fn test(test: &WebDavTest) { john_client .request_with_headers( "PUT", - "/dav/cal/john/default/itip.ics", + "/dav/cal/john%40example.com/default/itip.ics", [("content-type", "text/calendar; charset=utf-8")], &test_itip, ) @@ -174,28 +180,27 @@ pub async fn test(test: &WebDavTest) { tokio::time::sleep(std::time::Duration::from_millis(200)).await; // Check that the invitation was received by Bill and Mike - for client in [bill_client, jane_client] { + for client in [&bill_client, &jane_client] { let messages = test .server .get_cached_messages(client.account_id) .await .unwrap(); assert_eq!(messages.emails.items.len(), 1); - let access_token = test - .server - .get_access_token(client.account_id) - .await - .unwrap(); let events = test .server - .fetch_dav_resources(&access_token, client.account_id, SyncCollection::Calendar) + .fetch_dav_resources( + client.account_id, + client.account_id, + SyncCollection::Calendar, + ) .await .unwrap(); assert_eq!(events.resources.len(), 2); let events = test .server .fetch_dav_resources( - &access_token, + client.account_id, client.account_id, SyncCollection::CalendarEventNotification, ) @@ -205,7 +210,7 @@ pub async fn test(test: &WebDavTest) { } // Validate iTIP - let itips = fetch_and_remove_itips(jane_client).await; + let itips = jane_client.fetch_and_remove_itips().await; assert_eq!(itips.len(), 1); let itip = itips.first().unwrap(); assert!( @@ -214,7 +219,7 @@ pub async fn test(test: &WebDavTest) { ); // Fetch added calendar entry - let cals = fetch_icals(jane_client).await; + let cals = jane_client.fetch_icals().await; assert_eq!(cals.len(), 1); let cal = cals.into_iter().next().unwrap(); @@ -252,14 +257,14 @@ pub async fn test(test: &WebDavTest) { // Make sure that the schedule has not changed assert_eq!( - fetch_icals(jane_client).await[0].schedule_tag, + jane_client.fetch_icals().await[0].schedule_tag, cal.schedule_tag ); // Check that John received the RSVP tokio::time::sleep(std::time::Duration::from_millis(200)).await; test.wait_for_tasks().await; - let itips = fetch_and_remove_itips(john_client).await; + let itips = john_client.fetch_and_remove_itips().await; assert_eq!(itips.len(), 1); assert!( itips[0].contains("METHOD:REPLY") @@ -267,7 +272,7 @@ pub async fn test(test: &WebDavTest) { "failed for itip: {}", itips[0] ); - let cals = fetch_icals(john_client).await; + let cals = john_client.fetch_icals().await; assert_eq!(cals.len(), 1); assert!( cals[0] @@ -290,7 +295,7 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::NO_CONTENT); tokio::time::sleep(std::time::Duration::from_millis(200)).await; assert_eq!( - fetch_and_remove_itips(john_client).await, + john_client.fetch_and_remove_itips().await, Vec::::new() ); @@ -300,7 +305,7 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::NO_CONTENT); tokio::time::sleep(std::time::Duration::from_millis(200)).await; - let itips = fetch_and_remove_itips(john_client).await; + let itips = john_client.fetch_and_remove_itips().await; assert_eq!(itips.len(), 1); assert!( itips[0].contains("METHOD:REPLY") @@ -308,7 +313,7 @@ pub async fn test(test: &WebDavTest) { "failed for itip: {}", itips[0] ); - let cals = fetch_icals(john_client).await; + let cals = john_client.fetch_icals().await; assert_eq!(cals.len(), 1); let cal = cals.into_iter().next().unwrap(); assert!( @@ -358,7 +363,7 @@ pub async fn test(test: &WebDavTest) { response.contains("Lunch") && response.contains("RSVP has been recorded"), "failed for response: {response}" ); - let cals = fetch_icals(john_client).await; + let cals = john_client.fetch_icals().await; assert_eq!(cals.len(), 1); let cal = cals.into_iter().next().unwrap(); assert!( @@ -384,7 +389,7 @@ pub async fn test(test: &WebDavTest) { let response = john_client .request_with_headers( "POST", - "/dav/itip/john/outbox/", + "/dav/itip/john%40example.com/outbox/", [("content-type", "text/calendar; charset=utf-8")], &test_outbox, ) @@ -442,7 +447,7 @@ pub async fn test(test: &WebDavTest) { // Make sure that the schedule has changed assert_ne!( - fetch_icals(john_client).await[0].schedule_tag, + john_client.fetch_icals().await[0].schedule_tag, cal.schedule_tag ); let main_event_href = cal.href; @@ -450,7 +455,7 @@ pub async fn test(test: &WebDavTest) { // Check that Bill received the update tokio::time::sleep(std::time::Duration::from_millis(200)).await; test.wait_for_tasks().await; - let mut itips = fetch_and_remove_itips(bill_client).await; + let mut itips = bill_client.fetch_and_remove_itips().await; itips.sort_unstable_by(|a, _| { if a.contains("Lunch") { std::cmp::Ordering::Less @@ -469,7 +474,7 @@ pub async fn test(test: &WebDavTest) { "failed for itip: {}", itips[1] ); - let cals = fetch_icals(bill_client).await; + let cals = bill_client.fetch_icals().await; assert_eq!(cals.len(), 1); let cal = cals.into_iter().next().unwrap(); assert!( @@ -480,7 +485,7 @@ pub async fn test(test: &WebDavTest) { ); let attendee_href = cal.href; assert_eq!( - fetch_and_remove_itips(jane_client).await, + jane_client.fetch_and_remove_itips().await, Vec::::new() ); @@ -490,14 +495,14 @@ pub async fn test(test: &WebDavTest) { .await .with_status(StatusCode::NO_CONTENT); tokio::time::sleep(std::time::Duration::from_millis(200)).await; - let itips = fetch_and_remove_itips(bill_client).await; + let itips = bill_client.fetch_and_remove_itips().await; assert_eq!(itips.len(), 1); assert!( itips[0].contains("METHOD:CANCEL") && itips[0].contains("STATUS:CANCELLED"), "failed for itip: {}", itips[0] ); - let cals = fetch_icals(bill_client).await; + let cals = bill_client.fetch_icals().await; assert_eq!(cals.len(), 1); let cal = cals.into_iter().next().unwrap(); assert!( @@ -506,7 +511,7 @@ pub async fn test(test: &WebDavTest) { cal.ical ); assert_eq!( - fetch_and_remove_itips(jane_client).await, + jane_client.fetch_and_remove_itips().await, Vec::::new() ); @@ -517,40 +522,43 @@ pub async fn test(test: &WebDavTest) { .with_status(StatusCode::NO_CONTENT); tokio::time::sleep(std::time::Duration::from_millis(200)).await; assert_eq!( - fetch_and_remove_itips(john_client).await, + john_client.fetch_and_remove_itips().await, Vec::::new() ); for client in [bill_client, jane_client, john_client] { client.delete_default_containers().await; - destroy_all_mailboxes_for_account(client.account_id).await; + } + for account in [bill, jane, john] { + test.destroy_all_mailboxes(account).await; } test.assert_is_empty().await; } -async fn fetch_and_remove_itips(client: &DummyWebDavClient) -> Vec { - let inbox_href = format!("/dav/itip/{}/inbox/", client.name); - let response = client - .propfind_with_headers(&inbox_href, ALL_DAV_PROPERTIES, [("depth", "1")]) - .await; - let mut itips = vec![]; +impl DummyWebDavClient { + async fn fetch_and_remove_itips(&self) -> Vec { + let inbox_href = format!("/dav/itip/{}/inbox/", self.name.replace('@', "%40")); + let response = self + .propfind_with_headers(&inbox_href, ALL_DAV_PROPERTIES, [("depth", "1")]) + .await; + let mut itips = vec![]; - for href in response.hrefs.keys().filter(|&href| href != &inbox_href) { - let itip = client - .request("GET", href, "") - .await - .with_status(StatusCode::OK) - .body - .expect("Missing body"); - client - .request("DELETE", href, "") - .await - .with_status(StatusCode::NO_CONTENT); - itips.push(itip); + for href in response.hrefs.keys().filter(|&href| href != &inbox_href) { + let itip = self + .request("GET", href, "") + .await + .with_status(StatusCode::OK) + .body + .expect("Missing body"); + self.request("DELETE", href, "") + .await + .with_status(StatusCode::NO_CONTENT); + itips.push(itip); + } + + itips } - - itips } #[derive(Debug)] @@ -560,42 +568,46 @@ struct CalEntry { schedule_tag: String, } -async fn fetch_icals(client: &DummyWebDavClient) -> Vec { - let cal_inbox = format!("/dav/cal/{}/default/", client.name); - let response = client - .propfind_with_headers(&cal_inbox, ALL_DAV_PROPERTIES, [("depth", "1")]) - .await; - let mut cals = vec![]; +impl DummyWebDavClient { + async fn fetch_icals(&self) -> Vec { + let cal_inbox = format!("/dav/cal/{}/default/", self.name.replace('@', "%40")); + let response = self + .propfind_with_headers(&cal_inbox, ALL_DAV_PROPERTIES, [("depth", "1")]) + .await; + let mut cals = vec![]; - for href in response.hrefs.keys().filter(|&href| href != &cal_inbox) { - let ical = client - .request("GET", href, "") - .await - .with_status(StatusCode::OK) - .body - .expect("Missing body"); - let properties = response.properties(href); + for href in response.hrefs.keys().filter(|&href| href != &cal_inbox) { + let ical = self + .request("GET", href, "") + .await + .with_status(StatusCode::OK) + .body + .expect("Missing body"); + let properties = response.properties(href); - assert!( - !ical.contains("METHOD:"), - "iTIP method found in calendar entry: {ical}" - ); + assert!( + !ical.contains("METHOD:"), + "iTIP method found in calendar entry: {ical}" + ); - cals.push(CalEntry { - href: href.to_string(), - ical, - schedule_tag: properties - .get(DavProperty::CalDav(CalDavProperty::ScheduleTag)) - .value() - .to_string(), - }); + cals.push(CalEntry { + href: href.to_string(), + ical, + schedule_tag: properties + .get(DavProperty::CalDav(CalDavProperty::ScheduleTag)) + .value() + .to_string(), + }); + } + + cals } - - cals } -pub async fn test_build_itip_templates(server: &Server) { - let dummy_access_token = AccessToken::from_id(0); +pub async fn test_build_itip_templates(test: &TestServer) { + let account = test.account("john@example.com"); + let account_id = account.id().document_id(); + let account_info = test.server.account_info(account_id).await.unwrap(); for (idx, summary) in [ ItipSummary::Invite(vec![ @@ -784,19 +796,14 @@ pub async fn test_build_itip_templates(server: &Server) { .into_iter() .enumerate() { - let bytes = rkyv::to_bytes::(&summary) - .unwrap() - .to_vec(); - let summary = rkyv::access::(&bytes).unwrap(); - let html = build_itip_template( - server, - &dummy_access_token, - 0, + &test.server, + &account_info, + account_id, 1, "john.doe@example.org", "jane.smith@example.net", - summary, + &summary, "124", ) .await; diff --git a/tests/src/webdav/card_query.rs b/tests/src/webdav/card_query.rs index 0724f846..df1238b4 100644 --- a/tests/src/webdav/card_query.rs +++ b/tests/src/webdav/card_query.rs @@ -4,17 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; +use crate::utils::server::TestServer; + use dav_proto::schema::property::{CardDavProperty, DavProperty, WebDavProperty}; use groupware::DavResourceName; use hyper::StatusCode; -pub async fn test(test: &WebDavTest) { +pub async fn test(test: &TestServer) { println!("Running REPORT addressbook-query tests..."); - let client = test.client("john"); + let client = test.account("john@example.com").webdav_client(); // Create test data - let default_path = format!("{}/john/default/", DavResourceName::Card.base_path()); + let default_path = format!("{}/john%40example.com/default/", DavResourceName::Card.base_path()); let mut hrefs = Vec::with_capacity(3); for (i, vcard) in [VCARD1, VCARD2, VCARD3].iter().enumerate() { let href = format!("{default_path}contact-{i}.vcf",); diff --git a/tests/src/webdav/copy_move.rs b/tests/src/webdav/copy_move.rs index 1d6bbe7c..b9490958 100644 --- a/tests/src/webdav/copy_move.rs +++ b/tests/src/webdav/copy_move.rs @@ -4,16 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{DavResponse, WebDavTest}; -use crate::webdav::GenerateTestDavResource; +use crate::utils::{ + server::TestServer, + webdav::{DavResponse, GenerateTestDavResource}, +}; use ahash::AHashSet; use dav_proto::Depth; use groupware::DavResourceName; use hyper::StatusCode; +use registry::schema::structs::Action; -pub async fn test(test: &WebDavTest, assisted_discovery: bool) { - let client = test.client("jane"); - let mike_noquota = test.client("mike"); +pub async fn test(test: &TestServer, assisted_discovery: bool) { + let admin = test.account("admin@example.com"); + let client = test.account("jane@example.com").webdav_client(); + let mike_noquota = test.account("mike@example.com").webdav_client(); for resource_type in [ DavResourceName::File, @@ -21,8 +25,8 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { DavResourceName::Card, ] { println!("Running COPY/MOVE tests ({})...", resource_type.base_path()); - let user_base_path = format!("{}/jane", resource_type.base_path()); - let group_base_path = format!("{}/support", resource_type.base_path()); + let user_base_path = format!("{}/jane%40example.com", resource_type.base_path()); + let group_base_path = format!("{}/support%40example.com", resource_type.base_path()); let default_test_depth = if resource_type == DavResourceName::File { 2 } else { @@ -73,7 +77,7 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { client.validate_values(&hierarchy).await; // Delete cache an resync - test.clear_cache(); + admin.registry_create_object(Action::InvalidateCaches).await; let response = client .sync_collection( &user_base_path, @@ -724,7 +728,10 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { // Test 19: Quota enforcement (on CalDAV/CardDAV items are linked, not copied therefore there is no quota increase) if resource_type == DavResourceName::File { - let path = format!("{}/mike/quota-test/", resource_type.base_path()); + let path = format!( + "{}/mike%40example.com/quota-test/", + resource_type.base_path() + ); let content = resource_type.generate(); mike_noquota .mkcol("MKCOL", &path, [], []) @@ -744,7 +751,11 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { &path, [( "destination", - format!("{}/mike/quota-test{i}", resource_type.base_path()).as_str(), + format!( + "{}/mike%40example.com/quota-test{i}", + resource_type.base_path() + ) + .as_str(), )], &content, ) @@ -775,7 +786,10 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { mike_noquota .request( "DELETE", - &format!("{}/mike/quota-test{i}", resource_type.base_path()), + &format!( + "{}/mike%40example.com/quota-test{i}", + resource_type.base_path() + ), "", ) .await @@ -785,7 +799,9 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { } client.delete_default_containers().await; - client.delete_default_containers_by_account("support").await; + client + .delete_default_containers_by_account("support@example.com") + .await; mike_noquota.delete_default_containers().await; test.assert_is_empty().await; } @@ -796,7 +812,9 @@ fn assert_result(response: &DavResponse, hierarchy: &[(String, String)]) { .hrefs() .into_iter() .filter(|h| { - !h.ends_with("/jane/") && !h.ends_with("/support/") && !h.ends_with("/default/") + !h.ends_with("/jane%40example.com/") + && !h.ends_with("/support%40example.com/") + && !h.ends_with("/default/") }) .collect::>(); let hierarchy = hierarchy diff --git a/tests/src/webdav/lock.rs b/tests/src/webdav/lock.rs index f5188002..c846966d 100644 --- a/tests/src/webdav/lock.rs +++ b/tests/src/webdav/lock.rs @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{DavResponse, DummyWebDavClient, WebDavTest}; -use crate::webdav::GenerateTestDavResource; +use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; + use dav_proto::schema::property::{DavProperty, WebDavProperty}; use groupware::DavResourceName; use hyper::StatusCode; -pub async fn test(test: &WebDavTest) { - let client = test.client("john"); +pub async fn test(test: &TestServer) { + let client = test.account("john@example.com").webdav_client(); for resource_type in [ DavResourceName::File, @@ -22,7 +22,7 @@ pub async fn test(test: &WebDavTest) { "Running LOCK/UNLOCK tests ({})...", resource_type.base_path() ); - let base_path = format!("{}/john", resource_type.base_path()); + let base_path = format!("{}/john%40example.com", resource_type.base_path()); // Test 1: Creating a collection under an unmapped resource without providing a lock token should fail let path = format!("{base_path}/do-not-write"); @@ -201,9 +201,3 @@ pub async fn test(test: &WebDavTest) { client.delete_default_containers().await; test.assert_is_empty().await; } - -impl DavResponse { - pub fn lock_token(&self) -> &str { - self.value("D:prop.D:lockdiscovery.D:activelock.D:locktoken.D:href") - } -} diff --git a/tests/src/webdav/mkcol.rs b/tests/src/webdav/mkcol.rs index 9e144e16..981d9240 100644 --- a/tests/src/webdav/mkcol.rs +++ b/tests/src/webdav/mkcol.rs @@ -8,11 +8,12 @@ use hyper::StatusCode; use crate::webdav::{TEST_FILE_1, TEST_ICAL_1, TEST_VCARD_1, TEST_VTIMEZONE_1}; -use super::{DavResponse, DummyWebDavClient, WebDavTest}; +use crate::utils::server::TestServer; -pub async fn test(test: &WebDavTest) { + +pub async fn test(test: &TestServer) { println!("Running MKCOL tests..."); - let client = test.client("john"); + let client = test.account("john@example.com").webdav_client(); // Creating collections in root elements is not allowed for path in [ @@ -29,9 +30,9 @@ pub async fn test(test: &WebDavTest) { // Create collections using MKCOL (empty body) for path in [ - "/dav/file/john/my-files", - "/dav/card/john/my-cards", - "/dav/cal/john/my-events", + "/dav/file/john%40example.com/my-files", + "/dav/card/john%40example.com/my-cards", + "/dav/cal/john%40example.com/my-events", ] { client .request("MKCOL", path, "") @@ -41,9 +42,9 @@ pub async fn test(test: &WebDavTest) { // Create resources under the newly created collections for (path, content) in [ - ("/dav/file/john/my-files/file1.txt", TEST_FILE_1), - ("/dav/card/john/my-cards/card1.vcf", TEST_VCARD_1), - ("/dav/cal/john/my-events/event1.ics", TEST_ICAL_1), + ("/dav/file/john%40example.com/my-files/file1.txt", TEST_FILE_1), + ("/dav/card/john%40example.com/my-cards/card1.vcf", TEST_VCARD_1), + ("/dav/cal/john%40example.com/my-events/event1.ics", TEST_ICAL_1), ] { client .request("PUT", path, content) @@ -53,12 +54,12 @@ pub async fn test(test: &WebDavTest) { // Creating a collection on a mapped resource should fail for path in [ - "/dav/file/john/my-files", - "/dav/card/john/my-cards", - "/dav/cal/john/my-events", - "/dav/file/john/my-files/file1.txt", - "/dav/card/john/my-cards/card1.vcf", - "/dav/cal/john/my-events/event1.ics", + "/dav/file/john%40example.com/my-files", + "/dav/card/john%40example.com/my-cards", + "/dav/cal/john%40example.com/my-events", + "/dav/file/john%40example.com/my-files/file1.txt", + "/dav/card/john%40example.com/my-cards/card1.vcf", + "/dav/cal/john%40example.com/my-events/event1.ics", ] { client .request("MKCOL", path, "") @@ -68,13 +69,13 @@ pub async fn test(test: &WebDavTest) { // Creating a sub-collections is allowed in FileDAV but in CalDAV and CardDAV for (path, expected_status) in [ - ("/dav/file/john/my-files/my-sub-files", StatusCode::CREATED), + ("/dav/file/john%40example.com/my-files/my-sub-files", StatusCode::CREATED), ( - "/dav/card/john/my-cards/my-sub-cards", + "/dav/card/john%40example.com/my-cards/my-sub-cards", StatusCode::METHOD_NOT_ALLOWED, ), ( - "/dav/cal/john/my-events/my-sub-events", + "/dav/cal/john%40example.com/my-events/my-sub-events", StatusCode::METHOD_NOT_ALLOWED, ), ] { @@ -86,9 +87,9 @@ pub async fn test(test: &WebDavTest) { // Extended MKCOL with an unsupported resource types should fail for (path, resource_type) in [ - ("/dav/file/john/my-named-files", "B:addressbook"), - ("/dav/card/john/my-named-cards", "A:calendar"), - ("/dav/cal/john/my-named-events", "B:addressbook"), + ("/dav/file/john%40example.com/my-named-files", "B:addressbook"), + ("/dav/card/john%40example.com/my-named-cards", "A:calendar"), + ("/dav/cal/john%40example.com/my-named-events", "B:addressbook"), ] { client .mkcol("MKCOL", path, ["D:collection", resource_type], []) @@ -104,12 +105,12 @@ pub async fn test(test: &WebDavTest) { // Create using extended MKCOL for (path, expected_properties, resource_types) in [ ( - "/dav/file/john/my-named-files/", + "/dav/file/john%40example.com/my-named-files/", [("D:displayname", "Named Files")].as_slice(), ["D:collection"].as_slice(), ), ( - "/dav/card/john/my-named-cards/", + "/dav/card/john%40example.com/my-named-cards/", [ ("D:displayname", "Named Cards"), ("B:addressbook-description", "Some amazing contacts"), @@ -118,7 +119,7 @@ pub async fn test(test: &WebDavTest) { ["D:collection", "B:addressbook"].as_slice(), ), ( - "/dav/cal/john/my-named-events/", + "/dav/cal/john%40example.com/my-named-events/", [ ("D:displayname", "Named Events"), ("A:calendar-description", "Some amazing events"), @@ -166,7 +167,7 @@ pub async fn test(test: &WebDavTest) { client .mkcol( "MKCALENDAR", - "/dav/cal/john/my-named-events2", + "/dav/cal/john%40example.com/my-named-events2", [], [ ("D:displayname", "Named Events 2"), @@ -183,7 +184,7 @@ pub async fn test(test: &WebDavTest) { client .mkcol( "MKCALENDAR", - "/dav/cal/john/my-named-events3", + "/dav/cal/john%40example.com/my-named-events3", [], [ ("D:displayname", "Named Events 3"), @@ -203,11 +204,11 @@ pub async fn test(test: &WebDavTest) { // Check the properties of the created calendars client .propfind( - "/dav/cal/john/my-named-events2/", + "/dav/cal/john%40example.com/my-named-events2/", ["A:supported-calendar-component-set"], ) .await - .properties("/dav/cal/john/my-named-events2/") + .properties("/dav/cal/john%40example.com/my-named-events2/") .get("A:supported-calendar-component-set") .with_status(StatusCode::OK) .with_values([ @@ -227,25 +228,25 @@ pub async fn test(test: &WebDavTest) { ]); client .propfind( - "/dav/cal/john/my-named-events3/", + "/dav/cal/john%40example.com/my-named-events3/", ["A:supported-calendar-component-set"], ) .await - .properties("/dav/cal/john/my-named-events3/") + .properties("/dav/cal/john%40example.com/my-named-events3/") .get("A:supported-calendar-component-set") .with_status(StatusCode::OK) .with_values(["A:comp.[name]:VEVENT", "A:comp.[name]:VTODO"]); // Delete everything for path in [ - "/dav/file/john/my-files", - "/dav/card/john/my-cards", - "/dav/cal/john/my-events", - "/dav/file/john/my-named-files", - "/dav/card/john/my-named-cards", - "/dav/cal/john/my-named-events", - "/dav/cal/john/my-named-events2", - "/dav/cal/john/my-named-events3", + "/dav/file/john%40example.com/my-files", + "/dav/card/john%40example.com/my-cards", + "/dav/cal/john%40example.com/my-events", + "/dav/file/john%40example.com/my-named-files", + "/dav/card/john%40example.com/my-named-cards", + "/dav/cal/john%40example.com/my-named-events", + "/dav/cal/john%40example.com/my-named-events2", + "/dav/cal/john%40example.com/my-named-events3", ] { client .request("DELETE", path, "") @@ -255,4 +256,3 @@ pub async fn test(test: &WebDavTest) { client.delete_default_containers().await; test.assert_is_empty().await; } - diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs index 788ca481..f54d53d8 100644 --- a/tests/src/webdav/mod.rs +++ b/tests/src/webdav/mod.rs @@ -4,53 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - AssertConfig, TEST_USERS, add_test_certs, - directory::internal::TestInternalDirectory, - jmap::{assert_is_empty, wait_for_tasks}, - store::{ - TempDir, build_store_config, - cleanup::{search_store_destroy, store_destroy}, +use crate::utils::server::TestServerBuilder; +use ahash::AHashMap; +use common::{DavResource, DavResources}; +use groupware::DavResourceName; +use hyper::StatusCode; +use registry::{ + schema::{ + enums::{Permission, StorageQuota}, + prelude::{ObjectType, Property}, + structs::{ + CalendarAlarm, CalendarScheduling, Expression, MtaStageAuth, Sharing, SystemSettings, + WebDav, + }, }, + types::EnumImpl, }; -use ::managesieve::core::ManageSieveSessionManager; -use ahash::{AHashMap, AHashSet}; -use base64::{Engine, engine::general_purpose::STANDARD}; -use common::{ - Caches, Core, Data, DavResource, DavResources, Inner, Server, - config::{ - server::{Listeners, ServerProtocol}, - telemetry::Telemetry, - }, - manager::boot::build_ipc, -}; -use dav_proto::{ - schema::property::{DavProperty, WebDavProperty}, - xml_pretty_print, -}; -use email::message::metadata::MessageMetadata; -use groupware::{DavResourceName, cache::GroupwareCache}; -use http::HttpSessionManager; -use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION}; -use imap::core::ImapSessionManager; -use pop3::Pop3SessionManager; -use quick_xml::Reader; -use quick_xml::events::Event; -use services::SpawnServices; -use smtp::{SpawnQueueManager, core::SmtpSessionManager}; -use std::{borrow::Cow, str}; -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; -use store::{ - ValueKey, - rand::{Rng, distr::Alphanumeric, rng}, - write::{AlignedBytes, Archive}, -}; -use tokio::sync::watch; -use types::{collection::Collection, field::EmailField}; -use utils::config::Config; +use serde_json::json; +use std::str; +use std::time::Instant; pub mod acl; pub mod basic; @@ -70,8 +42,6 @@ pub mod sync; #[test] fn webdav_tests() { - //test_build_itip_templates(&handle.server).await; - tokio::runtime::Builder::new_multi_thread() .thread_stack_size(8 * 1024 * 1024) // 8MB stack .enable_all() @@ -80,25 +50,156 @@ fn webdav_tests() { .block_on(async { // Prepare settings let assisted_discovery = std::env::var("ASSISTED_DISCOVERY").unwrap_or_default() == "1"; - let start_time = Instant::now(); - let delete = true; - let handle = init_webdav_tests(assisted_discovery, delete).await; - basic::test(&handle).await; - put_get::test(&handle).await; - mkcol::test(&handle).await; - copy_move::test(&handle, assisted_discovery).await; - prop::test(&handle, assisted_discovery).await; - multiget::test(&handle).await; - sync::test(&handle).await; - lock::test(&handle).await; - principals::test(&handle, assisted_discovery).await; - acl::test(&handle).await; - card_query::test(&handle).await; - cal_query::test(&handle).await; - cal_alarm::test(&handle).await; + let mut test = TestServerBuilder::new("webdav_tests") + .await + .with_default_listeners() + .await + .build() + .await; + + // Create admin account + let admin = test.create_admin_account("admin@example.com").await; + + // Create test users + for (name, secret, description, aliases) in [ + ( + "john@example.com", + "secret2 + some more text", + "John Doe", + &["jdoe@example.com"], + ), + ( + "jane@example.com", + "secret3 + some more text", + "Jane Doe-Smith", + &["jane.smith@example.com"], + ), + ( + "bill@example.com", + "secret4 + some more text", + "Bill Foobar", + &["bill@example.com"], + ), + ( + "mike@example.com", + "secret5 + some more text", + "Mike Noquota", + &["mike@example.com"], + ), + ] { + let account = admin + .create_user_account( + name, + secret, + description, + aliases, + vec![ + Permission::UnlimitedRequests, + Permission::UnlimitedUploads, + Permission::DavPrincipalList, + Permission::DavPrincipalSearch, + ], + ) + .await; + if name == "mike@example.com" { + admin + .registry_update_object( + ObjectType::Account, + account.id(), + json!({ + Property::Quotas: { StorageQuota::MaxDiskQuota.as_str(): 1024} + }), + ) + .await; + } + + test.insert_account(account); + } + + // Create test group + test.insert_account( + admin + .create_group_account("support@example.com", "Support Group", &[]) + .await, + ); + + // Add Jane to the Support group + let support_id = test.account("support@example.com").id(); + admin + .registry_update_object( + ObjectType::Account, + test.account("jane@example.com").id(), + json!({ + "memberGroupIds": { support_id: true }, + }), + ) + .await; + + // Add test settings + admin + .registry_update_setting( + SystemSettings { + default_hostname: "webdav.example.org".to_string(), + ..Default::default() + }, + &[Property::DefaultHostname], + ) + .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(CalendarScheduling { + auto_add_invitations: true, + ..Default::default() + }) + .await; + admin + .registry_create_object(WebDav { + enable_assisted_discovery: assisted_discovery, + ..Default::default() + }) + .await; + admin.reload_settings().await; + + test.insert_account(admin); + + let start_time = Instant::now(); + //test_build_itip_templates(&test).await; + basic::test(&test).await; + put_get::test(&test).await; + mkcol::test(&test).await; + copy_move::test(&test, assisted_discovery).await; + prop::test(&test, assisted_discovery).await; + multiget::test(&test).await; + sync::test(&test).await; + lock::test(&test).await; + principals::test(&test, assisted_discovery).await; + acl::test(&test).await; + card_query::test(&test).await; + cal_query::test(&test).await; + cal_alarm::test(&test).await; cal_itip::test(); - cal_scheduling::test(&handle).await; + cal_scheduling::test(&test).await; // Print elapsed time let elapsed = start_time.elapsed(); @@ -109,179 +210,12 @@ fn webdav_tests() { ); // Remove test data - if delete { - handle.temp_dir.delete(); + if test.is_reset() { + test.temp_dir.delete(); } }); } -#[allow(dead_code)] -pub struct WebDavTest { - server: Server, - clients: AHashMap<&'static str, DummyWebDavClient>, - temp_dir: TempDir, - shutdown_tx: watch::Sender, -} - -async fn init_webdav_tests(assisted_discovery: bool, delete_if_exists: bool) -> WebDavTest { - // Load and parse config - let temp_dir = TempDir::new("webdav_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("{ASSISTED_DISCOVERY}", &assisted_discovery.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 tracers = Telemetry::parse(&mut config, &stores); - let core = Core::parse(&mut config, stores, Default::default()).await; - 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, - }); - - // 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, - ), - }; - }); - - if delete_if_exists { - store_destroy(&store).await; - search_store_destroy(&search_store).await; - } - - // Create test accounts - let mut clients = AHashMap::new(); - for (account, secret, name, email) in TEST_USERS { - let account_id = store - .create_test_user(account, secret, name, &[email]) - .await; - clients.insert( - *account, - DummyWebDavClient::new(account_id, account, secret, email), - ); - store - .add_permissions( - account, - [Permission::DavPrincipalList, Permission::DavPrincipalSearch], - ) - .await; - if *account == "mike" { - store.set_test_quota(account, 1024).await; - } - } - store - .create_test_group("support", "Support Group", &["support@example.com"]) - .await; - store.add_to_group("jane", "support").await; - - WebDavTest { - server: inner.build_server(), - clients, - temp_dir, - shutdown_tx, - } -} - -impl WebDavTest { - pub fn client(&self, name: &'static str) -> &DummyWebDavClient { - self.clients.get(name).unwrap() - } - - pub async fn resources(&self, name: &'static str, collection: Collection) -> Arc { - let account_id = self.client(name).account_id; - let access_token = self.server.get_access_token(account_id).await.unwrap(); - self.server - .fetch_dav_resources(&access_token, account_id, collection.into()) - .await - .unwrap() - } - - pub fn clear_cache(&self) { - for cache in [ - &self.server.inner.cache.events, - &self.server.inner.cache.contacts, - &self.server.inner.cache.files, - ] { - cache.clear(); - } - } - - pub async fn assert_is_empty(&self) { - assert_is_empty(&self.server).await; - self.clear_cache(); - } - - pub async fn wait_for_tasks(&self) { - wait_for_tasks(&self.server).await; - } -} - pub trait DavResourcesTest { fn items(&self) -> Vec; } @@ -388,153 +322,3 @@ END:DAYLIGHT END:VTIMEZONE END:VCALENDAR "#; - -impl WebDavTest { - pub async fn fetch_email(&self, account_id: u32, document_id: u32) -> Vec { - let metadata_ = self - .server - .store() - .get_value::>(ValueKey::property( - account_id, - Collection::Email, - document_id, - EmailField::Metadata, - )) - .await - .unwrap() - .unwrap(); - self.server - .blob_store() - .get_blob( - metadata_ - .unarchive::() - .unwrap() - .blob_hash - .0 - .as_slice(), - 0..usize::MAX, - ) - .await - .unwrap() - .unwrap() - } -} - -const SERVER: &str = r#" -[server] -hostname = "webdav.example.org" - -[spam-filter] -enable = false - -[http] -url = "'https://127.0.0.1:8899'" - -[server.listener.webdav] -bind = ["127.0.0.1:8899"] -protocol = "http" -max-connections = 81920 -tls.implicit = true - -[server.socket] -reuse-addr = true - -[server.tls] -enable = true -implicit = false -certificate = "default" - -[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" - -[resolver] -type = "system" - -[queue.strategy] -route = [ { if = "rcpt_domain == 'example.com'", then = "'local'" }, - { else = "'mx'" } ] - -[session.data.add-headers] -delivered-to = false - -[session.extensions] -future-release = [ { if = "!is_empty(authenticated_as)", then = "99999999d"}, - { else = false } ] - -[certificate.default] -cert = "%{file:{CERT}}%" -private-key = "%{file:{PK}}%" - -[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" -authentication = "100/2s" -anonymous = "100/1m" - -[calendar.alarms] -minimum-interval = "1s" - -[calendar.scheduling.inbound] -auto-add = true - -[dav.collection] -assisted-discovery = {ASSISTED_DISCOVERY} - -[sharing] -allow-directory-query = true - -[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" - -[oauth] -key = "parerga_und_paralipomena" - -[oauth.auth] -max-attempts = 1 - -[oauth.expiry] -user-code = "1s" -token = "1s" -refresh-token = "3s" -refresh-token-renew = "2s" - -[tracer.console] -type = "console" -level = "{LEVEL}" -multiline = false -ansi = true -disabled-events = ["network.*"] - -"#; diff --git a/tests/src/webdav/multiget.rs b/tests/src/webdav/multiget.rs index cce0e392..fa56302a 100644 --- a/tests/src/webdav/multiget.rs +++ b/tests/src/webdav/multiget.rs @@ -4,14 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; -use crate::webdav::{DummyWebDavClient, GenerateTestDavResource, prop::DavMultiStatus}; +use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; use dav_proto::schema::property::{CalDavProperty, CardDavProperty, DavProperty, WebDavProperty}; use groupware::DavResourceName; use hyper::StatusCode; -pub async fn test(test: &WebDavTest) { - let client = test.client("john"); +pub async fn test(test: &TestServer) { + let client = test.account("john@example.com").webdav_client(); for resource_type in [DavResourceName::Cal, DavResourceName::Card] { println!( @@ -22,7 +21,7 @@ pub async fn test(test: &WebDavTest) { let mut paths = Vec::new(); for name in ["file1", "file2"] { let contents = resource_type.generate(); - let path = format!("{}/john/default/{}", resource_type.base_path(), name); + let path = format!("{}/john%40example.com/default/{}", resource_type.base_path(), name); let etag = client .request("PUT", &path, contents.as_str()) .await @@ -33,7 +32,7 @@ pub async fn test(test: &WebDavTest) { } if resource_type == DavResourceName::Cal { - let path = format!("{}/john", resource_type.base_path()); + let path = format!("{}/john%40example.com", resource_type.base_path()); let response = client .multiget_calendar(&path, &[&paths[0].0, &paths[1].0]) .await; @@ -49,7 +48,7 @@ pub async fn test(test: &WebDavTest) { .with_values([contents.as_str()]); } } else { - let path = format!("{}/john", resource_type.base_path()); + let path = format!("{}/john%40example.com", resource_type.base_path()); let response = client .multiget_addressbook(&path, &[&paths[0].0, &paths[1].0]) .await; diff --git a/tests/src/webdav/principals.rs b/tests/src/webdav/principals.rs index d5071009..41ab70bc 100644 --- a/tests/src/webdav/principals.rs +++ b/tests/src/webdav/principals.rs @@ -4,20 +4,29 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::WebDavTest; -use crate::{TEST_USERS, webdav::prop::ALL_DAV_PROPERTIES}; +use crate::utils::server::TestServer; +use crate::webdav::prop::ALL_DAV_PROPERTIES; use dav_proto::schema::property::{DavProperty, PrincipalProperty, WebDavProperty}; use groupware::DavResourceName; use hyper::StatusCode; -pub async fn test(test: &WebDavTest, assisted_discovery: bool) { +pub async fn test(test: &TestServer, assisted_discovery: bool) { println!("Running principals tests..."); - let client = test.client("jane"); + let client = test.account("jane@example.com").webdav_client(); let principal_path = format!("D:href:{}/", DavResourceName::Principal.base_path()); - let jane_principal_path = format!("D:href:{}/jane/", DavResourceName::Principal.base_path()); + let jane_principal_path = format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Principal.base_path() + ); - let path_support_card = format!("D:href:{}/support/", DavResourceName::Card.base_path()); - let path_support_cal = format!("D:href:{}/support/", DavResourceName::Cal.base_path()); + let path_support_card = format!( + "D:href:{}/support%40example.com/", + DavResourceName::Card.base_path() + ); + let path_support_cal = format!( + "D:href:{}/support%40example.com/", + DavResourceName::Cal.base_path() + ); // Test 1: PROPFIND on /dav/pal should return all principals let response = client @@ -26,22 +35,34 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { ALL_DAV_PROPERTIES, ) .await; - for (account, _, name, email) in TEST_USERS { + for account_ in test.accounts.values().filter(|a| a.name().contains('@')) { + let account_name = account_.name().replace('@', "%40"); + let email = account_.name(); + let description = account_.description(); + let props = response.properties(&format!( "{}/{}/", DavResourceName::Principal.base_path(), - account + account_name )); let path_pal = format!( "D:href:{}/{}/", DavResourceName::Principal.base_path(), - account + account_name + ); + let path_card = format!( + "D:href:{}/{}/", + DavResourceName::Card.base_path(), + account_name + ); + let path_cal = format!( + "D:href:{}/{}/", + DavResourceName::Cal.base_path(), + account_name ); - let path_card = format!("D:href:{}/{}/", DavResourceName::Card.base_path(), account); - let path_cal = format!("D:href:{}/{}/", DavResourceName::Cal.base_path(), account); props .get(DavProperty::WebDav(WebDavProperty::DisplayName)) - .with_values([*name]) + .with_values([description]) .with_status(StatusCode::OK); props .get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)) @@ -55,7 +76,7 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .get(DavProperty::WebDav(WebDavProperty::Owner)) .with_values([path_pal.as_str()]) .with_status(StatusCode::OK); - if *account == "jane" && !assisted_discovery { + if account_name == "jane%40example.com" && !assisted_discovery { props .get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet)) .with_values([path_cal.as_str(), path_support_cal.as_str()]) @@ -104,12 +125,16 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .with_status(StatusCode::OK); props .get(DavProperty::Principal(PrincipalProperty::CalendarUserType)) - .with_values(["INDIVIDUAL"]) + .with_values([if account_name == "support%40example.com" { + "GROUP" + } else { + "INDIVIDUAL" + }]) .with_status(StatusCode::OK); props .get(DavProperty::Principal(PrincipalProperty::ScheduleInboxURL)) .with_values([format!( - "D:href:{}/{account}/inbox/", + "D:href:{}/{account_name}/inbox/", DavResourceName::Scheduling.base_path() ) .as_str()]) @@ -117,7 +142,7 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { props .get(DavProperty::Principal(PrincipalProperty::ScheduleOutboxURL)) .with_values([format!( - "D:href:{}/{account}/outbox/", + "D:href:{}/{account_name}/outbox/", DavResourceName::Scheduling.base_path() ) .as_str()]) @@ -210,24 +235,36 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { if assisted_discovery { props .get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet)) - .with_values( - [format!("D:href:{}/jane/", DavResourceName::Cal.base_path()).as_str()], + .with_values([format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Cal.base_path() ) + .as_str()]) .with_status(StatusCode::OK); props .get(DavProperty::Principal( PrincipalProperty::AddressbookHomeSet, )) - .with_values([ - format!("D:href:{}/jane/", DavResourceName::Card.base_path()).as_str(), - ]) + .with_values([format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Card.base_path() + ) + .as_str()]) .with_status(StatusCode::OK); } else { props .get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet)) .with_values([ - format!("D:href:{}/jane/", DavResourceName::Cal.base_path()).as_str(), - format!("D:href:{}/support/", DavResourceName::Cal.base_path()).as_str(), + format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Cal.base_path() + ) + .as_str(), + format!( + "D:href:{}/support%40example.com/", + DavResourceName::Cal.base_path() + ) + .as_str(), ]) .with_status(StatusCode::OK); props @@ -235,28 +272,49 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { PrincipalProperty::AddressbookHomeSet, )) .with_values([ - format!("D:href:{}/jane/", DavResourceName::Card.base_path()).as_str(), - format!("D:href:{}/support/", DavResourceName::Card.base_path()).as_str(), + format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Card.base_path() + ) + .as_str(), + format!( + "D:href:{}/support%40example.com/", + DavResourceName::Card.base_path() + ) + .as_str(), ]) .with_status(StatusCode::OK); } - for (account, _, name, _) in TEST_USERS - .iter() - .filter(|(account, _, _, _)| ["jane", "support"].contains(account)) + for account_ in test + .accounts + .values() + .filter(|account| ["jane@example.com", "support@example.com"].contains(&account.name())) { - let path_card = format!("D:href:{}/{}/", DavResourceName::Card.base_path(), account); - let path_cal = format!("D:href:{}/{}/", DavResourceName::Cal.base_path(), account); + let account_name = account_.name().replace('@', "%40"); + let description = account_.description(); + + let path_card = format!( + "D:href:{}/{}/", + DavResourceName::Card.base_path(), + account_name + ); + let path_cal = format!( + "D:href:{}/{}/", + DavResourceName::Cal.base_path(), + account_name + ); let path_pal = format!( "D:href:{}/{}/", DavResourceName::Principal.base_path(), - account + account_name ); - let props = response.properties(&format!("{}/{account}/", resource_type.base_path())); + let props = + response.properties(&format!("{}/{account_name}/", resource_type.base_path())); props .get(DavProperty::WebDav(WebDavProperty::DisplayName)) - .with_values([*name]) + .with_values([description]) .with_status(StatusCode::OK); props .get(DavProperty::WebDav(WebDavProperty::ResourceType)) @@ -286,7 +344,7 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .get(DavProperty::WebDav(WebDavProperty::Owner)) .with_values([path_pal.as_str()]) .with_status(StatusCode::OK); - if *account == "jane" && !assisted_discovery { + if account_name == "jane%40example.com" && !assisted_discovery { props .get(DavProperty::Principal(PrincipalProperty::CalendarHomeSet)) .with_values([path_cal.as_str(), path_support_cal.as_str()]) @@ -334,8 +392,8 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .with_status(StatusCode::MULTI_STATUS) .into_propfind_response(None); response.with_hrefs([ - format!("{}/jane/", resource_type.base_path()).as_str(), - format!("{}/support/", resource_type.base_path()).as_str(), + format!("{}/jane%40example.com/", resource_type.base_path()).as_str(), + format!("{}/support%40example.com/", resource_type.base_path()).as_str(), ]); } @@ -350,8 +408,16 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .with_status(StatusCode::MULTI_STATUS) .into_propfind_response(None); response.with_hrefs([ - format!("{}/jane/", DavResourceName::Principal.base_path()).as_str(), - format!("{}/support/", DavResourceName::Principal.base_path()).as_str(), + format!( + "{}/jane%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str(), + format!( + "{}/support%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str(), ]); // Test 5: principal-search-property-set REPORT @@ -384,17 +450,24 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .with_status(StatusCode::MULTI_STATUS) .into_propfind_response(None); response.with_hrefs([ - format!("{}/jane/", DavResourceName::Principal.base_path()).as_str(), - format!("{}/john/", DavResourceName::Principal.base_path()).as_str(), + format!( + "{}/jane%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str(), + format!( + "{}/john%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str(), ]); response - .properties(&format!("{}/jane/", DavResourceName::Principal.base_path())) + .properties(&format!( + "{}/jane%40example.com/", + DavResourceName::Principal.base_path() + )) .get(DavProperty::WebDav(WebDavProperty::DisplayName)) - .with_values([TEST_USERS - .iter() - .find(|(account, _, _, _)| *account == "jane") - .unwrap() - .2]) + .with_values([test.account("jane@example.com").description()]) .with_status(StatusCode::OK); client .request( @@ -405,10 +478,16 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { .await .with_status(StatusCode::MULTI_STATUS) .into_propfind_response(None) - .with_hrefs([format!("{}/support/", DavResourceName::Principal.base_path()).as_str()]); + .with_hrefs([format!( + "{}/support%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str()]); client.delete_default_containers().await; - client.delete_default_containers_by_account("support").await; + client + .delete_default_containers_by_account("support@example.com") + .await; test.assert_is_empty().await; } diff --git a/tests/src/webdav/prop.rs b/tests/src/webdav/prop.rs index 88195355..b7747fd1 100644 --- a/tests/src/webdav/prop.rs +++ b/tests/src/webdav/prop.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{DavResponse, DummyWebDavClient, WebDavTest}; -use crate::webdav::{GenerateTestDavResource, TEST_ICAL_2, TEST_VTIMEZONE_1}; -use ahash::{AHashMap, AHashSet}; +use crate::utils::server::TestServer; +use crate::utils::webdav::GenerateTestDavResource; +use crate::webdav::{TEST_ICAL_2, TEST_VTIMEZONE_1}; use dav_proto::schema::property::{ CalDavProperty, CardDavProperty, DavProperty, PrincipalProperty, WebDavProperty, }; @@ -14,8 +14,8 @@ use groupware::DavResourceName; use hyper::StatusCode; use types::dead_property::DeadElementTag; -pub async fn test(test: &WebDavTest, assisted_discovery: bool) { - let client = test.client("jane"); +pub async fn test(test: &TestServer, assisted_discovery: bool) { + let client = test.account("jane@example.com").webdav_client(); for resource_type in [ DavResourceName::File, @@ -26,8 +26,8 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { "Running PROPFIND/PROPPATCH tests ({})...", resource_type.base_path() ); - let user_base_path = format!("{}/jane", resource_type.base_path()); - let group_base_path = format!("{}/support", resource_type.base_path()); + let user_base_path = format!("{}/jane%40example.com", resource_type.base_path()); + let group_base_path = format!("{}/support%40example.com", resource_type.base_path()); // Create a new test container and file let test_base_path = format!("{user_base_path}/PropFind_Folder/"); @@ -208,14 +208,18 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { ]); properties .get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)) - .with_values([ - format!("D:href:{}/jane/", DavResourceName::Principal.base_path()).as_str(), - ]); + .with_values([format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str()]); properties .get(DavProperty::WebDav(WebDavProperty::Owner)) - .with_values([ - format!("D:href:{}/jane/", DavResourceName::Principal.base_path()).as_str(), - ]); + .with_values([format!( + "D:href:{}/jane%40example.com/", + DavResourceName::Principal.base_path() + ) + .as_str()]); properties .get(DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet)) .is_not_empty(); @@ -440,7 +444,7 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { ] { properties.get(prop).with_some_values([ format!( - "D:response.D:href:{}/jane/", + "D:response.D:href:{}/jane%40example.com/", DavResourceName::Principal.base_path(), ) .as_str(), @@ -702,7 +706,9 @@ pub async fn test(test: &WebDavTest, assisted_discovery: bool) { } client.delete_default_containers().await; - client.delete_default_containers_by_account("support").await; + client + .delete_default_containers_by_account("support@example.com") + .await; test.assert_is_empty().await; } diff --git a/tests/src/webdav/put_get.rs b/tests/src/webdav/put_get.rs index 3e7115a9..1731f679 100644 --- a/tests/src/webdav/put_get.rs +++ b/tests/src/webdav/put_get.rs @@ -6,35 +6,45 @@ use types::collection::Collection; -use super::WebDavTest; +use crate::utils::server::TestServer; + +use crate::utils::webdav::GenerateTestDavResource; use crate::webdav::*; -pub async fn test(test: &WebDavTest) { +pub async fn test(test: &TestServer) { println!("Running PUT/GET tests..."); - let client = test.client("john"); + let client = test.account("john@example.com").webdav_client(); // Simple PUT let mut files = AHashMap::new(); for (path, ct, content) in [ - ("/dav/file/john/file1.txt", "text/plain", TEST_FILE_1), - ("/dav/file/john/file2.txt", "text/x-other", TEST_FILE_2), ( - "/dav/card/john/default/card1.vcf", + "/dav/file/john%40example.com/file1.txt", + "text/plain", + TEST_FILE_1, + ), + ( + "/dav/file/john%40example.com/file2.txt", + "text/x-other", + TEST_FILE_2, + ), + ( + "/dav/card/john%40example.com/default/card1.vcf", "text/vcard; charset=utf-8", TEST_VCARD_1, ), ( - "/dav/card/john/default/card2.vcf", + "/dav/card/john%40example.com/default/card2.vcf", "text/vcard; charset=utf-8", TEST_VCARD_2, ), ( - "/dav/cal/john/default/event1.ics", + "/dav/cal/john%40example.com/default/event1.ics", "text/calendar; charset=utf-8", TEST_ICAL_1, ), ( - "/dav/cal/john/default/event2.ics", + "/dav/cal/john%40example.com/default/event2.ics", "text/calendar; charset=utf-8", TEST_ICAL_2, ), @@ -62,9 +72,9 @@ pub async fn test(test: &WebDavTest) { // PUT under a non-existing parent should fail for (path, contents) in [ - ("/dav/file/john/foo/file1.txt", TEST_FILE_1), - ("/dav/card/john/foo/card1.vcf", TEST_VCARD_1), - ("/dav/cal/john/foo/event1.ics", TEST_ICAL_1), + ("/dav/file/john%40example.com/foo/file1.txt", TEST_FILE_1), + ("/dav/card/john%40example.com/foo/card1.vcf", TEST_VCARD_1), + ("/dav/cal/john%40example.com/foo/event1.ics", TEST_ICAL_1), ] { client .request("PUT", path, contents) @@ -74,13 +84,16 @@ pub async fn test(test: &WebDavTest) { // PUT under resources should fail for (path, contents) in [ - ("/dav/file/john/file1.txt/other-file.txt", TEST_FILE_1), ( - "/dav/card/john/default/card1.vcf/other-file.vcf", + "/dav/file/john%40example.com/file1.txt/other-file.txt", + TEST_FILE_1, + ), + ( + "/dav/card/john%40example.com/default/card1.vcf/other-file.vcf", TEST_VCARD_1, ), ( - "/dav/cal/john/default/event1.ics/other-file.ical", + "/dav/cal/john%40example.com/default/event1.ics/other-file.ical", TEST_ICAL_1, ), ] { @@ -93,13 +106,13 @@ pub async fn test(test: &WebDavTest) { // PUT a non-vCard/iCalendar file should fail for (path, ct, content, precondition) in [ ( - "/dav/card/john/card3.vcf", + "/dav/card/john%40example.com/card3.vcf", "text/vcard; charset=utf-8", TEST_FILE_1, "B:supported-address-data", ), ( - "/dav/cal/john/event3.ics", + "/dav/cal/john%40example.com/event3.ics", "text/calendar; charset=utf-8", TEST_FILE_2, "A:supported-calendar-data", @@ -116,19 +129,19 @@ pub async fn test(test: &WebDavTest) { let conf = &test.server.core.groupware; for (path, contents, max_size, expect) in [ ( - "/dav/file/john/chunky-file1.txt", + "/dav/file/john%40example.com/chunky-file1.txt", TEST_FILE_1, conf.max_file_size, None, ), ( - "/dav/card/john/chunky-card1.vcf", + "/dav/card/john%40example.com/chunky-card1.vcf", TEST_VCARD_1, conf.max_vcard_size, Some("B:max-resource-size"), ), ( - "/dav/cal/john/chunky-event1.ics", + "/dav/cal/john%40example.com/chunky-event1.ics", TEST_ICAL_1, conf.max_ical_size, Some("A:max-resource-size"), @@ -152,13 +165,16 @@ pub async fn test(test: &WebDavTest) { } // PUT requests cannot exceed quota - let mike_noquota = test.client("mike"); + let mike_noquota = test.account("mike@example.com").webdav_client(); for resource_type in [ DavResourceName::File, DavResourceName::Card, DavResourceName::Cal, ] { - let path = format!("{}/mike/quota-test/", resource_type.base_path()); + let path = format!( + "{}/mike%40example.com/quota-test/", + resource_type.base_path() + ); mike_noquota .mkcol("MKCOL", &path, [], []) .await @@ -199,25 +215,29 @@ pub async fn test(test: &WebDavTest) { // PUT precondition enforcement let modseq = [ - test.resources("john", Collection::FileNode) + test.resources("john@example.com", Collection::FileNode) .await .highest_change_id, - test.resources("john", Collection::Calendar) + test.resources("john@example.com", Collection::Calendar) .await .highest_change_id, - test.resources("john", Collection::AddressBook) + test.resources("john@example.com", Collection::AddressBook) .await .highest_change_id, ]; for (path, ct, content) in [ - ("/dav/file/john/file1.txt", "text/plain", TEST_FILE_1), ( - "/dav/card/john/default/card1.vcf", + "/dav/file/john%40example.com/file1.txt", + "text/plain", + TEST_FILE_1, + ), + ( + "/dav/card/john%40example.com/default/card1.vcf", "text/vcard; charset=utf-8", TEST_VCARD_1, ), ( - "/dav/cal/john/default/event1.ics", + "/dav/cal/john%40example.com/default/event1.ics", "text/calendar; charset=utf-8", TEST_ICAL_1, ), @@ -271,13 +291,13 @@ pub async fn test(test: &WebDavTest) { } assert_eq!( [ - test.resources("john", Collection::FileNode) + test.resources("john@example.com", Collection::FileNode) .await .highest_change_id, - test.resources("john", Collection::Calendar) + test.resources("john@example.com", Collection::Calendar) .await .highest_change_id, - test.resources("john", Collection::AddressBook) + test.resources("john@example.com", Collection::AddressBook) .await .highest_change_id, ], @@ -315,18 +335,18 @@ pub async fn test(test: &WebDavTest) { // PUT requests require unique UIDs for (path, ct, content, precond_key, precond_value) in [ ( - "/dav/card/john/default/card5.vcf", + "/dav/card/john%40example.com/default/card5.vcf", "text/vcard; charset=utf-8", TEST_VCARD_1, "B:no-uid-conflict.D:href", - "/dav/card/john/default/card1.vcf", + "/dav/card/john%40example.com/default/card1.vcf", ), ( - "/dav/cal/john/default/event5.ics", + "/dav/cal/john%40example.com/default/event5.ics", "text/calendar; charset=utf-8", TEST_ICAL_1, "A:no-uid-conflict.D:href", - "/dav/cal/john/default/event1.ics", + "/dav/cal/john%40example.com/default/event1.ics", ), ] { client @@ -345,7 +365,7 @@ pub async fn test(test: &WebDavTest) { client .request_with_headers( "PUT", - "/dav/cal/john/default/invalid.ics", + "/dav/cal/john%40example.com/default/invalid.ics", [ ("content-type", "text/calendar; charset=utf-8"), ("if-none-match", "*"), @@ -375,7 +395,7 @@ END:VCALENDAR client .request_with_headers( "PUT", - "/dav/cal/john/default/invalid.ics", + "/dav/cal/john%40example.com/default/invalid.ics", [ ("content-type", "text/calendar; charset=utf-8"), ("if-none-match", "*"), @@ -403,13 +423,25 @@ END:VCALENDAR // Deleting unknown/invalid destinations should fail for (path, expect) in [ - ("/dav/file/john/unknown.txt", StatusCode::NOT_FOUND), - ("/dav/card/john/default/unknown.txt", StatusCode::NOT_FOUND), - ("/dav/cal/john/default/unknown.txt", StatusCode::NOT_FOUND), - ("/dav/file/john", StatusCode::FORBIDDEN), - ("/dav/cal/john", StatusCode::FORBIDDEN), - ("/dav/card/john", StatusCode::FORBIDDEN), - ("/dav/pal/john", StatusCode::METHOD_NOT_ALLOWED), + ( + "/dav/file/john%40example.com/unknown.txt", + StatusCode::NOT_FOUND, + ), + ( + "/dav/card/john%40example.com/default/unknown.txt", + StatusCode::NOT_FOUND, + ), + ( + "/dav/cal/john%40example.com/default/unknown.txt", + StatusCode::NOT_FOUND, + ), + ("/dav/file/john%40example.com", StatusCode::FORBIDDEN), + ("/dav/cal/john%40example.com", StatusCode::FORBIDDEN), + ("/dav/card/john%40example.com", StatusCode::FORBIDDEN), + ( + "/dav/pal/john%40example.com", + StatusCode::METHOD_NOT_ALLOWED, + ), ("/dav/file", StatusCode::FORBIDDEN), ("/dav/cal", StatusCode::FORBIDDEN), ("/dav/card", StatusCode::FORBIDDEN), diff --git a/tests/src/webdav/sync.rs b/tests/src/webdav/sync.rs index 7f4c6457..5a86511e 100644 --- a/tests/src/webdav/sync.rs +++ b/tests/src/webdav/sync.rs @@ -4,15 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{DavResponse, DummyWebDavClient, WebDavTest}; -use crate::webdav::GenerateTestDavResource; +use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; + use ahash::AHashSet; use dav_proto::Depth; use groupware::DavResourceName; use hyper::StatusCode; -pub async fn test(test: &WebDavTest) { - let client = test.client("john"); +pub async fn test(test: &TestServer) { + let client = test.account("john@example.com").webdav_client(); for resource_type in [ DavResourceName::File, @@ -23,7 +23,7 @@ pub async fn test(test: &WebDavTest) { "Running REPORT sync-collection tests ({})...", resource_type.base_path() ); - let user_base_path = format!("{}/john/", resource_type.base_path()); + let user_base_path = format!("{}/john%40example.com/", resource_type.base_path()); // Test 1: Initial sync let response = client