Add mailpot-tests crate to reuse test code

grcov
Manos Pitsidianakis 2023-04-24 17:54:35 +03:00
parent 503e214801
commit b48a3c9d12
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
18 changed files with 643 additions and 272 deletions

16
Cargo.lock generated
View File

@ -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"

View File

@ -3,6 +3,7 @@ members = [
"archive-http",
"cli",
"core",
"mailpot-tests",
"rest-http",
"web",
]

View File

@ -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"

View File

@ -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: <foo-chat{to}@example.com>
Subject: {subject}
Date: Thu, 29 Oct 2020 13:58:16 +0000
Message-ID:
<aaa{}@example.com>
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: <foo-chat{to}@example.com>
Subject: {subject}
Date: Thu, 29 Oct 2020 13:58:16 +0000
Message-ID:
<aaa{}@example.com>
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);
}

View File

@ -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"

View File

@ -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(),

View File

@ -153,9 +153,12 @@ impl<S: AsRef<str>> TryFrom<(S, &melib::Envelope)> for ListRequest {
fn try_from((val, env): (S, &melib::Envelope)) -> std::result::Result<Self, Self::Error> {
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

View File

@ -17,14 +17,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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");

View File

@ -17,16 +17,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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");

View File

@ -17,14 +17,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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");

View File

@ -17,9 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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");

View File

@ -17,200 +17,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<String>,
},
DataStart {
from: String,
to: Vec<String>,
},
Data {
#[allow(dead_code)]
from: String,
to: Vec<String>,
buf: Vec<u8>,
},
}
#[allow(clippy::type_complexity)]
#[derive(Debug, Clone)]
struct MyHandler {
mails: Arc<Mutex<Vec<((IpAddr, String), Message)>>>,
stored: Arc<Mutex<Vec<(String, melib::Envelope)>>>,
}
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::*;

View File

@ -17,14 +17,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();

View File

@ -17,14 +17,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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();
});
}

View File

@ -0,0 +1,21 @@
[package]
name = "mailpot-tests"
version = "0.0.0+2023-04-21"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
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]

View File

@ -0,0 +1,3 @@
# mailpot-tests
Re-usable testing code for all mailpot crates.

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#![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<String>,
},
DataStart {
from: String,
to: Vec<String>,
},
Data {
#[allow(dead_code)]
from: String,
to: Vec<String>,
buf: Vec<u8>,
},
}
#[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<Mutex<Vec<((IpAddr, String), Message)>>>,
pub stored: Arc<Mutex<Vec<(String, melib::Envelope)>>>,
}
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<Cow<'static, str>>) -> 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(),
},
}
}
}