From b48a3c9d12990f66f3227c77b770ba344b37eb8a Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 24 Apr 2023 17:54:35 +0300 Subject: [PATCH] Add mailpot-tests crate to reuse test code --- Cargo.lock | 16 +- Cargo.toml | 1 + cli/Cargo.toml | 1 + cli/tests/out_queue_flush.rs | 259 +++++++++++++++++++++--- core/Cargo.toml | 2 +- core/src/db/posts.rs | 1 - core/src/mail.rs | 7 +- core/tests/account.rs | 5 +- core/tests/authorizer.rs | 5 +- core/tests/creation.rs | 7 +- core/tests/error_queue.rs | 5 +- core/tests/smtp.rs | 191 +----------------- core/tests/subscription.rs | 5 +- core/tests/template_replies.rs | 5 +- core/tests/utils.rs | 34 ---- mailpot-tests/Cargo.toml | 21 ++ mailpot-tests/README.md | 3 + mailpot-tests/src/lib.rs | 347 +++++++++++++++++++++++++++++++++ 18 files changed, 643 insertions(+), 272 deletions(-) delete mode 100644 core/tests/utils.rs create mode 100644 mailpot-tests/Cargo.toml create mode 100644 mailpot-tests/README.md create mode 100644 mailpot-tests/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index eb3a46b..e9f4964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1570,7 +1570,7 @@ dependencies = [ "chrono", "error-chain", "log", - "mailin-embedded", + "mailpot-tests", "melib", "minijinja", "reqwest", @@ -1592,6 +1592,7 @@ dependencies = [ "clap_mangen", "log", "mailpot", + "mailpot-tests", "predicates", "stderrlog", "tempfile", @@ -1606,6 +1607,19 @@ dependencies = [ "warp", ] +[[package]] +name = "mailpot-tests" +version = "0.0.0+2023-04-21" +dependencies = [ + "assert_cmd", + "log", + "mailin-embedded", + "mailpot", + "predicates", + "stderrlog", + "tempfile", +] + [[package]] name = "mailpot-web" version = "0.0.0+2023-04-21" diff --git a/Cargo.toml b/Cargo.toml index 927f7d8..ffa3357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "archive-http", "cli", "core", + "mailpot-tests", "rest-http", "web", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f1b5f57..e0fa1b5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -23,6 +23,7 @@ stderrlog = "^0.5" [dev-dependencies] assert_cmd = "2" +mailpot-tests = { version = "^0.0", path = "../mailpot-tests" } predicates = "3" tempfile = "3.3" diff --git a/cli/tests/out_queue_flush.rs b/cli/tests/out_queue_flush.rs index f5eabfb..7378c21 100644 --- a/cli/tests/out_queue_flush.rs +++ b/cli/tests/out_queue_flush.rs @@ -24,9 +24,31 @@ use mailpot::{ models::{changesets::ListSubscriptionChangeset, *}, Configuration, Connection, Queue, SendMail, }; +use mailpot_tests::*; use predicates::prelude::*; use tempfile::TempDir; +fn generate_mail(from: &str, to: &str, subject: &str, body: &str, seq: &mut usize) -> String { + format!( + "From: {from}@example.com +To: +Subject: {subject} +Date: Thu, 29 Oct 2020 13:58:16 +0000 +Message-ID: + +Content-Language: en-US +Content-Type: text/plain + +{body} +", + { + let val = *seq; + *seq += 1; + val + } + ) +} + #[test] fn test_out_queue_flush() { use assert_cmd::Command; @@ -35,9 +57,9 @@ fn test_out_queue_flush() { let conf_path = tmp_dir.path().join("conf.toml"); let db_path = tmp_dir.path().join("mpot.db"); - + let smtp_handler = TestSmtpHandler::builder().address("127.0.0.1:8826").build(); let config = Configuration { - send_mail: SendMail::ShellCommand("/usr/bin/true".to_string()), + send_mail: SendMail::Smtp(smtp_handler.smtp_conf()), db_path, data_path: tmp_dir.path().to_path_buf(), administrators: vec![], @@ -47,6 +69,8 @@ fn test_out_queue_flush() { std::fs::write(&conf_path, config_str.as_bytes()).unwrap(); + log::info!("Creating foo-chat@example.com mailing list."); + let post_policy; let foo_chat = { let db = Connection::open_or_create_db(config.clone()) .unwrap() @@ -64,7 +88,7 @@ fn test_out_queue_flush() { .unwrap(); assert_eq!(foo_chat.pk(), 1); - let _post_policy = db + post_policy = db .set_list_post_policy(PostPolicy { pk: -1, list: foo_chat.pk(), @@ -78,6 +102,19 @@ fn test_out_queue_flush() { foo_chat }; + let headers_fn = |env: &melib::Envelope| { + assert!(env.subject().starts_with(&format!("[{}] ", foo_chat.id))); + let headers = env.other_headers(); + + assert_eq!(headers.get("List-Id"), Some(&foo_chat.id_header())); + assert_eq!(headers.get("List-Help"), foo_chat.help_header().as_ref()); + assert_eq!( + headers.get("List-Post"), + foo_chat.post_header(Some(&post_policy)).as_ref() + ); + }; + + log::info!("Running mpot flush-queue on empty out queue."); let mut cmd = Command::cargo_bin("mpot").unwrap(); let output = cmd .arg("-vv") @@ -93,33 +130,14 @@ fn test_out_queue_flush() { .normalize(), ); - fn generate_mail(from: &str, to: &str, subject: &str, body: &str, seq: &mut usize) -> String { - format!( - "From: {from}@example.com -To: -Subject: {subject} -Date: Thu, 29 Oct 2020 13:58:16 +0000 -Message-ID: - -Content-Language: en-US -Content-Type: text/plain - -{body} -", - { - let val = *seq; - *seq += 1; - val - } - ) - } + let mut seq = 0; // for generated emails + log::info!("Subscribe two users, Αλίκη and Χαραλάμπης to foo-chat."); { let mut db = Connection::open_or_create_db(config.clone()) .unwrap() .trusted(); - let mut seq = 0; for who in ["Αλίκη", "Χαραλάμπης"] { // = ["Alice", "Bob"] let mail = generate_mail(who, "+request", "subscribe", "", &mut seq); @@ -139,16 +157,160 @@ Content-Type: text/plain assert_eq!(out_queue.len(), 2); assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 2); assert_eq!(db.error_queue().unwrap().len(), 0); + } + + log::info!("Flush out queue, subscription confirmations should be sent to the new users."); + let mut cmd = Command::cargo_bin("mpot").unwrap(); + let output = cmd + .arg("-vv") + .arg("-c") + .arg(&conf_path) + .arg("flush-queue") + .output() + .unwrap() + .assert(); + output.code(0).stdout( + predicate::eq("Queue out has 2 messages.") + .trim() + .normalize(), + ); + + /* Check that confirmation emails are correct */ + let stored = std::mem::take(&mut *smtp_handler.stored.lock().unwrap()); + assert_eq!(stored.len(), 2); + assert_eq!(stored[0].0, "=?UTF-8?B?zpHOu86vzrrOtw==?=@example.com"); + assert_eq!( + stored[1].0, + "=?UTF-8?B?zqfOsc+BzrHOu86szrzPgM63z4I=?=@example.com" + ); + for item in stored.iter() { + assert_eq!( + item.1.subject(), + "[foo-chat] You have successfully subscribed to foobar chat." + ); + assert_eq!( + &item.1.field_from_to_string(), + "foo-chat+request@example.com" + ); + headers_fn(&item.1); + } + + log::info!( + "Χαραλάμπης submits a post to list. Flush out queue, Χαραλάμπης' post should be relayed \ + to Αλίκη, and Χαραλάμπης should receive a copy of their own post because of \ + `receive_own_posts` setting." + ); + + { + let mut db = Connection::open_or_create_db(config.clone()) + .unwrap() + .trusted(); let mail = generate_mail("Χαραλάμπης", "", "hello world", "Hello there.", &mut seq); let subenvelope = mailpot::melib::Envelope::from_bytes(mail.as_bytes(), None) .expect("Could not parse message"); db.post(&subenvelope, mail.as_bytes(), /* dry_run */ false) .unwrap(); let out_queue = db.queue(Queue::Out).unwrap(); - assert_eq!(out_queue.len(), 4); + assert_eq!(out_queue.len(), 2); } - // [ref:TODO] hook smtp dev server. + let mut cmd = Command::cargo_bin("mpot").unwrap(); + let output = cmd + .arg("-vv") + .arg("-c") + .arg(&conf_path) + .arg("flush-queue") + .output() + .unwrap() + .assert(); + output.code(0).stdout( + predicate::eq("Queue out has 2 messages.") + .trim() + .normalize(), + ); + + /* Check that user posts are correct */ + { + let db = Connection::open_or_create_db(config).unwrap().trusted(); + + let out_queue = db.queue(Queue::Out).unwrap(); + assert_eq!(out_queue.len(), 0); + } + + let stored = std::mem::take(&mut *smtp_handler.stored.lock().unwrap()); + assert_eq!(stored.len(), 2); + assert_eq!(stored[0].0, "Αλίκη@example.com"); + assert_eq!(stored[1].0, "Χαραλάμπης@example.com"); + assert_eq!(stored[0].1.message_id(), stored[1].1.message_id()); + assert_eq!(stored[0].1.other_headers(), stored[1].1.other_headers()); + headers_fn(&stored[0].1); +} + +#[test] +fn test_list_requests_submission() { + use assert_cmd::Command; + + let tmp_dir = TempDir::new().unwrap(); + + let conf_path = tmp_dir.path().join("conf.toml"); + let db_path = tmp_dir.path().join("mpot.db"); + let smtp_handler = TestSmtpHandler::builder().address("127.0.0.1:8827").build(); + let config = Configuration { + send_mail: SendMail::Smtp(smtp_handler.smtp_conf()), + db_path, + data_path: tmp_dir.path().to_path_buf(), + administrators: vec![], + }; + + let config_str = config.to_toml(); + + std::fs::write(&conf_path, config_str.as_bytes()).unwrap(); + + log::info!("Creating foo-chat@example.com mailing list."); + let post_policy; + let foo_chat = { + let db = Connection::open_or_create_db(config.clone()) + .unwrap() + .trusted(); + + let foo_chat = db + .create_list(MailingList { + pk: 0, + name: "foobar chat".into(), + id: "foo-chat".into(), + address: "foo-chat@example.com".into(), + description: None, + archive_url: None, + }) + .unwrap(); + + assert_eq!(foo_chat.pk(), 1); + post_policy = db + .set_list_post_policy(PostPolicy { + pk: -1, + list: foo_chat.pk(), + announce_only: false, + subscription_only: false, + approval_needed: false, + open: true, + custom: false, + }) + .unwrap(); + foo_chat + }; + + let headers_fn = |env: &melib::Envelope| { + let headers = env.other_headers(); + + assert_eq!(headers.get("List-Id"), Some(&foo_chat.id_header())); + assert_eq!(headers.get("List-Help"), foo_chat.help_header().as_ref()); + assert_eq!( + headers.get("List-Post"), + foo_chat.post_header(Some(&post_policy)).as_ref() + ); + }; + + log::info!("Running mpot flush-queue on empty out queue."); let mut cmd = Command::cargo_bin("mpot").unwrap(); let output = cmd .arg("-vv") @@ -159,15 +321,52 @@ Content-Type: text/plain .unwrap() .assert(); output.code(0).stderr(predicates::str::is_empty()).stdout( - predicate::eq("Queue out has 4 messages.") + predicate::eq("Queue out has 0 messages.") .trim() .normalize(), ); - { - let db = Connection::open_or_create_db(config).unwrap().trusted(); + let mut seq = 0; // for generated emails + log::info!("User Αλίκη sends to foo-chat+request with subject 'help'."); + { + let mut db = Connection::open_or_create_db(config).unwrap().trusted(); + + let mail = generate_mail("Αλίκη", "+request", "help", "", &mut seq); + let subenvelope = mailpot::melib::Envelope::from_bytes(mail.as_bytes(), None) + .expect("Could not parse message"); + db.post(&subenvelope, mail.as_bytes(), /* dry_run */ false) + .unwrap(); let out_queue = db.queue(Queue::Out).unwrap(); - assert_eq!(out_queue.len(), 0); + assert_eq!(out_queue.len(), 1); + assert_eq!(db.list_subscriptions(foo_chat.pk()).unwrap().len(), 0); + assert_eq!(db.error_queue().unwrap().len(), 0); } + + log::info!("Flush out queue, help reply should go to Αλίκη."); + let mut cmd = Command::cargo_bin("mpot").unwrap(); + let output = cmd + .arg("-vv") + .arg("-c") + .arg(&conf_path) + .arg("flush-queue") + .output() + .unwrap() + .assert(); + output.code(0).stdout( + predicate::eq("Queue out has 1 messages.") + .trim() + .normalize(), + ); + + /* Check that help email is correct */ + let stored = std::mem::take(&mut *smtp_handler.stored.lock().unwrap()); + assert_eq!(stored.len(), 1); + assert_eq!(stored[0].0, "=?UTF-8?B?zpHOu86vzrrOtw==?=@example.com"); + assert_eq!(stored[0].1.subject(), "Help for foobar chat"); + assert_eq!( + &stored[0].1.field_from_to_string(), + "foo-chat+request@example.com" + ); + headers_fn(&stored[0].1); } diff --git a/core/Cargo.toml b/core/Cargo.toml index 6e24c32..51ab865 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -24,7 +24,7 @@ toml = "^0.5" xdg = "2.4.1" [dev-dependencies] -mailin-embedded = { version = "0.7", features = ["rtls"] } +mailpot-tests = { version = "^0.0", path = "../mailpot-tests" } reqwest = { version = "0.11", default-features = false, features = ["json", "blocking"] } stderrlog = "^0.5" tempfile = "3.3" diff --git a/core/src/db/posts.rs b/core/src/db/posts.rs index dca9e1b..d46ae0e 100644 --- a/core/src/db/posts.rs +++ b/core/src/db/posts.rs @@ -286,7 +286,6 @@ impl Connection { let post_policy = self.list_post_policy(list.pk)?; match request { ListRequest::Help => { - // [ref:TODO] add test for this trace!( "help action for addresses {:?} in list {}", env.from(), diff --git a/core/src/mail.rs b/core/src/mail.rs index 09f2e60..e5257d0 100644 --- a/core/src/mail.rs +++ b/core/src/mail.rs @@ -153,9 +153,12 @@ impl> TryFrom<(S, &melib::Envelope)> for ListRequest { fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result { let val = val.as_ref(); Ok(match val { - "subscribe" | "request" if env.subject().trim() == "subscribe" => Self::Subscribe, - "unsubscribe" | "request" if env.subject().trim() == "unsubscribe" => Self::Unsubscribe, + "subscribe" => Self::Subscribe, + "request" if env.subject().trim() == "subscribe" => Self::Subscribe, + "unsubscribe" => Self::Unsubscribe, + "request" if env.subject().trim() == "unsubscribe" => Self::Unsubscribe, "help" => Self::Help, + "request" if env.subject().trim() == "help" => Self::Help, "request" => Self::Other(env.subject().trim().to_string()), _ => { // [ref:TODO] add ChangeSetting parsing diff --git a/core/tests/account.rs b/core/tests/account.rs index 3951c18..7cbf57f 100644 --- a/core/tests/account.rs +++ b/core/tests/account.rs @@ -17,14 +17,13 @@ * along with this program. If not, see . */ -mod utils; - use mailpot::{models::*, Configuration, Connection, SendMail}; +use mailpot_tests::init_stderr_logging; use tempfile::TempDir; #[test] fn test_accounts() { - utils::init_stderr_logging(); + init_stderr_logging(); const SSH_KEY: &[u8] = include_bytes!("./ssh_key.pub"); diff --git a/core/tests/authorizer.rs b/core/tests/authorizer.rs index b49e70a..12e2349 100644 --- a/core/tests/authorizer.rs +++ b/core/tests/authorizer.rs @@ -17,16 +17,15 @@ * along with this program. If not, see . */ -mod utils; - use std::error::Error; use mailpot::{models::*, Configuration, Connection, SendMail}; +use mailpot_tests::init_stderr_logging; use tempfile::TempDir; #[test] fn test_authorizer() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); let db_path = tmp_dir.path().join("mpot.db"); diff --git a/core/tests/creation.rs b/core/tests/creation.rs index be71e7c..c244181 100644 --- a/core/tests/creation.rs +++ b/core/tests/creation.rs @@ -17,14 +17,13 @@ * along with this program. If not, see . */ -mod utils; - use mailpot::{models::*, Configuration, Connection, SendMail}; +use mailpot_tests::init_stderr_logging; use tempfile::TempDir; #[test] fn test_init_empty() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); let db_path = tmp_dir.path().join("mpot.db"); @@ -42,7 +41,7 @@ fn test_init_empty() { #[test] fn test_list_creation() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); let db_path = tmp_dir.path().join("mpot.db"); diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs index 66c756a..c7525e7 100644 --- a/core/tests/error_queue.rs +++ b/core/tests/error_queue.rs @@ -17,9 +17,8 @@ * along with this program. If not, see . */ -mod utils; - use mailpot::{melib, models::*, Configuration, Connection, SendMail}; +use mailpot_tests::init_stderr_logging; use tempfile::TempDir; fn get_smtp_conf() -> melib::smtp::SmtpServerConf { @@ -36,7 +35,7 @@ fn get_smtp_conf() -> melib::smtp::SmtpServerConf { #[test] fn test_error_queue() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); let db_path = tmp_dir.path().join("mpot.db"); diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs index e79be61..31527b1 100644 --- a/core/tests/smtp.rs +++ b/core/tests/smtp.rs @@ -17,200 +17,23 @@ * along with this program. If not, see . */ -mod utils; - -use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr}; -use std::{ - sync::{Arc, Mutex}, - thread, -}; - use log::{trace, warn}; -use mailin_embedded::{Handler, Response, Server, SslConfig}; -use mailpot::{melib, models::*, Configuration, Connection, Queue, SendMail}; +use mailpot::{melib, Configuration, Connection, Queue, SendMail}; +use mailpot_tests::*; use melib::smol; use tempfile::TempDir; -const ADDRESS: &str = "127.0.0.1:8825"; -#[derive(Debug, Clone)] -enum Message { - Helo, - Mail { - from: String, - }, - Rcpt { - from: String, - to: Vec, - }, - DataStart { - from: String, - to: Vec, - }, - Data { - #[allow(dead_code)] - from: String, - to: Vec, - buf: Vec, - }, -} - -#[allow(clippy::type_complexity)] -#[derive(Debug, Clone)] -struct MyHandler { - mails: Arc>>, - stored: Arc>>, -} -use mailin_embedded::response::{INTERNAL_ERROR, OK}; - -impl Handler for MyHandler { - fn helo(&mut self, ip: IpAddr, domain: &str) -> Response { - // eprintln!("helo ip {:?} domain {:?}", ip, domain); - self.mails - .lock() - .unwrap() - .push(((ip, domain.to_string()), Message::Helo)); - OK - } - - fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response { - // eprintln!("mail() ip {:?} domain {:?} from {:?}", ip, domain, from); - if let Some((_, message)) = self - .mails - .lock() - .unwrap() - .iter_mut() - .find(|((i, d), _)| (i, d.as_str()) == (&ip, domain)) - { - if let Message::Helo = message { - *message = Message::Mail { - from: from.to_string(), - }; - return OK; - } - } - INTERNAL_ERROR - } - - fn rcpt(&mut self, _to: &str) -> Response { - // eprintln!("rcpt() to {:?}", _to); - if let Some((_, message)) = self.mails.lock().unwrap().last_mut() { - if let Message::Mail { from } = message { - *message = Message::Rcpt { - from: from.clone(), - to: vec![_to.to_string()], - }; - return OK; - } else if let Message::Rcpt { to, .. } = message { - to.push(_to.to_string()); - return OK; - } - } - INTERNAL_ERROR - } - - fn data_start( - &mut self, - _domain: &str, - _from: &str, - _is8bit: bool, - _to: &[String], - ) -> Response { - // eprintln!( "data_start() domain {:?} from {:?} is8bit {:?} to {:?}", _domain, - // _from, _is8bit, _to); - if let Some(((_, d), ref mut message)) = self.mails.lock().unwrap().last_mut() { - if d != _domain { - return INTERNAL_ERROR; - } - if let Message::Rcpt { from, to } = message { - *message = Message::DataStart { - from: from.to_string(), - to: to.to_vec(), - }; - return OK; - } - } - INTERNAL_ERROR - } - - fn data(&mut self, _buf: &[u8]) -> Result<(), std::io::Error> { - if let Some(((_, _), ref mut message)) = self.mails.lock().unwrap().last_mut() { - if let Message::DataStart { from, to } = message { - *message = Message::Data { - from: from.to_string(), - to: to.clone(), - buf: _buf.to_vec(), - }; - return Ok(()); - } else if let Message::Data { buf, .. } = message { - buf.extend(_buf.iter()); - return Ok(()); - } - } - Ok(()) - } - - fn data_end(&mut self) -> Response { - let last = self.mails.lock().unwrap().pop(); - if let Some(((ip, domain), Message::Data { from: _, to, buf })) = last { - for to in to { - match melib::Envelope::from_bytes(&buf, None) { - Ok(env) => { - self.stored.lock().unwrap().push((to.clone(), env)); - } - Err(err) => { - panic!("envelope parse error {}", err); - } - } - } - self.mails - .lock() - .unwrap() - .push(((ip, domain), Message::Helo)); - return OK; - } - panic!("last self.mails item was not Message::Data: {last:?}"); //INTERNAL_ERROR - } -} - -fn get_smtp_conf() -> melib::smtp::SmtpServerConf { - use melib::smtp::*; - SmtpServerConf { - hostname: "127.0.0.1".into(), - port: 8825, - envelope_from: "foo-chat@example.com".into(), - auth: SmtpAuth::None, - security: SmtpSecurity::None, - extensions: Default::default(), - } -} - #[test] fn test_smtp() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); - let handler = MyHandler { - mails: Arc::new(Mutex::new(vec![])), - stored: Arc::new(Mutex::new(vec![])), - }; - let handler2 = handler.clone(); - let _smtp_handle = thread::spawn(move || { - let mut server = Server::new(handler2); - - server - .with_name("example.com") - .with_ssl(SslConfig::None) - .unwrap() - .with_addr(ADDRESS) - .unwrap(); - eprintln!("Running smtp server at {}", ADDRESS); - server.serve().expect("Could not run server"); - }); + let smtp_handler = TestSmtpHandler::builder().address("127.0.0.1:8825").build(); let db_path = tmp_dir.path().join("mpot.db"); let config = Configuration { - send_mail: SendMail::Smtp(get_smtp_conf()), + send_mail: SendMail::Smtp(smtp_handler.smtp_conf()), db_path, data_path: tmp_dir.path().to_path_buf(), administrators: vec![], @@ -313,7 +136,7 @@ fn test_smtp() { .unwrap(); } })); - let stored = handler.stored.lock().unwrap(); + let stored = smtp_handler.stored.lock().unwrap(); assert_eq!(stored.len(), 3); assert_eq!(&stored[0].0, "japoeunp@example.com"); assert_eq!( @@ -335,7 +158,7 @@ fn test_smtp() { #[test] fn test_smtp_mailcrab() { use std::env; - utils::init_stderr_logging(); + init_stderr_logging(); fn get_smtp_conf() -> melib::smtp::SmtpServerConf { use melib::smtp::*; diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs index 5d3dd75..c3a3f6f 100644 --- a/core/tests/subscription.rs +++ b/core/tests/subscription.rs @@ -17,14 +17,13 @@ * along with this program. If not, see . */ -mod utils; - use mailpot::{models::*, Configuration, Connection, SendMail}; +use mailpot_tests::init_stderr_logging; use tempfile::TempDir; #[test] fn test_list_subscription() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); diff --git a/core/tests/template_replies.rs b/core/tests/template_replies.rs index 295b44e..40c6bc0 100644 --- a/core/tests/template_replies.rs +++ b/core/tests/template_replies.rs @@ -17,14 +17,13 @@ * along with this program. If not, see . */ -mod utils; - use mailpot::{models::*, Configuration, Connection, Queue, SendMail, Template}; +use mailpot_tests::init_stderr_logging; use tempfile::TempDir; #[test] fn test_template_replies() { - utils::init_stderr_logging(); + init_stderr_logging(); let tmp_dir = TempDir::new().unwrap(); diff --git a/core/tests/utils.rs b/core/tests/utils.rs deleted file mode 100644 index 8f5803d..0000000 --- a/core/tests/utils.rs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This file is part of mailpot - * - * Copyright 2020 - Manos Pitsidianakis - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -use std::sync::Once; - -static INIT_STDERR_LOGGING: Once = Once::new(); - -pub fn init_stderr_logging() { - INIT_STDERR_LOGGING.call_once(|| { - stderrlog::new() - .quiet(false) - .verbosity(15) - .show_module_names(true) - .timestamp(stderrlog::Timestamp::Millisecond) - .init() - .unwrap(); - }); -} diff --git a/mailpot-tests/Cargo.toml b/mailpot-tests/Cargo.toml new file mode 100644 index 0000000..3affcf9 --- /dev/null +++ b/mailpot-tests/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mailpot-tests" +version = "0.0.0+2023-04-21" +authors = ["Manos Pitsidianakis "] +edition = "2021" +license = "LICENSE" +readme = "README.md" +description = "test library for mailpot crates" +repository = "https://github.com/meli/mailpot" +publish = false + +[dependencies] +assert_cmd = "2" +log = "0.4" +mailin-embedded = { version = "0.7", features = ["rtls"] } +mailpot = { version = "^0.0", path = "../core" } +predicates = "3" +stderrlog = "^0.5" +tempfile = "3.3" + +[dev-dependencies] diff --git a/mailpot-tests/README.md b/mailpot-tests/README.md new file mode 100644 index 0000000..76e576c --- /dev/null +++ b/mailpot-tests/README.md @@ -0,0 +1,3 @@ +# mailpot-tests + +Re-usable testing code for all mailpot crates. diff --git a/mailpot-tests/src/lib.rs b/mailpot-tests/src/lib.rs new file mode 100644 index 0000000..20307a2 --- /dev/null +++ b/mailpot-tests/src/lib.rs @@ -0,0 +1,347 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#![allow(clippy::new_without_default)] + +use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr}; +use std::{ + borrow::Cow, + net::ToSocketAddrs, + sync::{Arc, Mutex, Once}, + thread, +}; + +pub use assert_cmd; +pub use log::{trace, warn}; +use mailin_embedded::{ + response::{INTERNAL_ERROR, OK}, + Handler, Response, Server, +}; +pub use mailpot::{ + melib::{self, smol, smtp::SmtpServerConf}, + models::{changesets::ListSubscriptionChangeset, *}, + Configuration, Connection, Queue, SendMail, +}; +pub use predicates; +pub use tempfile::{self, TempDir}; + +static INIT_STDERR_LOGGING: Once = Once::new(); + +pub fn init_stderr_logging() { + INIT_STDERR_LOGGING.call_once(|| { + stderrlog::new() + .quiet(false) + .verbosity(15) + .show_module_names(true) + .timestamp(stderrlog::Timestamp::Millisecond) + .init() + .unwrap(); + }); +} +pub const ADDRESS: &str = "127.0.0.1:8825"; + +#[derive(Debug, Clone)] +pub enum Message { + Helo, + Mail { + from: String, + }, + Rcpt { + from: String, + to: Vec, + }, + DataStart { + from: String, + to: Vec, + }, + Data { + #[allow(dead_code)] + from: String, + to: Vec, + buf: Vec, + }, +} + +#[allow(clippy::type_complexity)] +#[derive(Debug, Clone)] +pub struct TestSmtpHandler { + address: Cow<'static, str>, + ssl: SslConfig, + envelope_from: Cow<'static, str>, + auth: melib::smtp::SmtpAuth, + pub messages: Arc>>, + pub stored: Arc>>, +} + +impl Handler for TestSmtpHandler { + fn helo(&mut self, ip: IpAddr, domain: &str) -> Response { + //eprintln!("helo ip {:?} domain {:?}", ip, domain); + self.messages + .lock() + .unwrap() + .push(((ip, domain.to_string()), Message::Helo)); + OK + } + + fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response { + //eprintln!("mail() ip {:?} domain {:?} from {:?}", ip, domain, from); + if let Some((_, message)) = self + .messages + .lock() + .unwrap() + .iter_mut() + .rev() + .find(|((i, d), _)| (i, d.as_str()) == (&ip, domain)) + { + if let Message::Helo = &message { + *message = Message::Mail { + from: from.to_string(), + }; + return OK; + } + } + INTERNAL_ERROR + } + + fn rcpt(&mut self, _to: &str) -> Response { + //eprintln!("rcpt() to {:?}", _to); + if let Some((_, message)) = self.messages.lock().unwrap().last_mut() { + if let Message::Mail { from } = message { + *message = Message::Rcpt { + from: from.clone(), + to: vec![_to.to_string()], + }; + return OK; + } else if let Message::Rcpt { to, .. } = message { + to.push(_to.to_string()); + return OK; + } + } + INTERNAL_ERROR + } + + fn data_start( + &mut self, + _domain: &str, + _from: &str, + _is8bit: bool, + _to: &[String], + ) -> Response { + // eprintln!( "data_start() domain {:?} from {:?} is8bit {:?} to {:?}", _domain, + // _from, _is8bit, _to); + if let Some(((_, d), ref mut message)) = self.messages.lock().unwrap().last_mut() { + if d != _domain { + return INTERNAL_ERROR; + } + if let Message::Rcpt { from, to } = message { + *message = Message::DataStart { + from: from.to_string(), + to: to.to_vec(), + }; + return OK; + } + } + INTERNAL_ERROR + } + + fn data(&mut self, _buf: &[u8]) -> Result<(), std::io::Error> { + if let Some(((_, _), ref mut message)) = self.messages.lock().unwrap().last_mut() { + if let Message::DataStart { from, to } = message { + *message = Message::Data { + from: from.to_string(), + to: to.clone(), + buf: _buf.to_vec(), + }; + return Ok(()); + } else if let Message::Data { buf, .. } = message { + buf.extend(_buf.iter()); + return Ok(()); + } + } + Ok(()) + } + + fn data_end(&mut self) -> Response { + let last = self.messages.lock().unwrap().pop(); + if let Some(((ip, domain), Message::Data { from: _, to, buf })) = last { + for to in to { + match melib::Envelope::from_bytes(&buf, None) { + Ok(env) => { + self.stored.lock().unwrap().push((to.clone(), env)); + } + Err(err) => { + panic!("envelope parse error {}", err); + } + } + } + self.messages + .lock() + .unwrap() + .push(((ip, domain), Message::Helo)); + return OK; + } + panic!("last self.messages item was not Message::Data: {last:?}"); //INTERNAL_ERROR + } +} + +impl TestSmtpHandler { + #[inline] + pub fn smtp_conf(&self) -> melib::smtp::SmtpServerConf { + use melib::smtp::*; + let sockaddr = self + .address + .as_ref() + .to_socket_addrs() + .unwrap() + .next() + .unwrap(); + let ip = sockaddr.ip(); + let port = sockaddr.port(); + + SmtpServerConf { + hostname: ip.to_string(), + port, + envelope_from: self.envelope_from.to_string(), + auth: self.auth.clone(), + security: SmtpSecurity::None, + extensions: Default::default(), + } + } +} + +impl TestSmtpHandler { + pub fn builder() -> TestSmtpHandlerBuilder { + TestSmtpHandlerBuilder::new() + } +} + +pub struct TestSmtpHandlerBuilder { + address: Cow<'static, str>, + ssl: SslConfig, + auth: melib::smtp::SmtpAuth, + envelope_from: Cow<'static, str>, +} + +impl TestSmtpHandlerBuilder { + pub fn new() -> Self { + Self { + address: ADDRESS.into(), + ssl: SslConfig::None, + auth: melib::smtp::SmtpAuth::None, + envelope_from: "foo-chat@example.com".into(), + } + } + + pub fn address(self, address: impl Into>) -> Self { + Self { + address: address.into(), + ..self + } + } + + pub fn ssl(self, ssl: SslConfig) -> Self { + Self { ssl, ..self } + } + + pub fn build(self) -> TestSmtpHandler { + let Self { + address, + ssl, + auth, + envelope_from, + } = self; + let handler = TestSmtpHandler { + address, + ssl, + auth, + envelope_from, + messages: Arc::new(Mutex::new(vec![])), + stored: Arc::new(Mutex::new(vec![])), + }; + crate::init_stderr_logging(); + let handler2 = handler.clone(); + let _smtp_handle = thread::spawn(move || { + let address = handler2.address.clone(); + let ssl = handler2.ssl.clone(); + + let mut server = Server::new(handler2.clone()); + let sockaddr = address.as_ref().to_socket_addrs().unwrap().next().unwrap(); + let ip = sockaddr.ip(); + let port = sockaddr.port(); + let addr = std::net::SocketAddr::new(ip, port); + eprintln!("Running smtp server at {}", addr); + server + .with_name("example.com") + .with_ssl((&ssl).into()) + .unwrap() + .with_addr(addr) + .unwrap(); + server.serve().expect("Could not run server"); + }); + handler + } +} + +/// Mirror struct for [`mailin_embedded::SslConfig`] because it does not +/// implement Debug or Clone. +#[derive(Clone, Debug)] +pub enum SslConfig { + /// Do not support STARTTLS + None, + /// Use a self-signed certificate for STARTTLS + SelfSigned { + /// Certificate path + cert_path: String, + /// Path to key file + key_path: String, + }, + /// Use a certificate from an authority + Trusted { + /// Certificate path + cert_path: String, + /// Key file path + key_path: String, + /// Path to CA bundle + chain_path: String, + }, +} + +impl From<&SslConfig> for mailin_embedded::SslConfig { + fn from(val: &SslConfig) -> Self { + match val { + SslConfig::None => Self::None, + SslConfig::SelfSigned { + ref cert_path, + ref key_path, + } => Self::SelfSigned { + cert_path: cert_path.to_string(), + key_path: key_path.to_string(), + }, + SslConfig::Trusted { + ref cert_path, + ref key_path, + ref chain_path, + } => Self::Trusted { + cert_path: cert_path.to_string(), + key_path: key_path.to_string(), + chain_path: chain_path.to_string(), + }, + } + } +}