Files
bakit/src/route/contact/mod.rs
Lucien Cartier-Tilet a3abe0f716
All checks were successful
Publish Docker Images / build-and-publish (push) Successful in 8m4s
test: improve test coverage
2025-11-15 23:23:33 +01:00

1003 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Contact form endpoint for handling user submissions and sending emails.
//!
//! This module provides functionality to:
//! - Validate contact form submissions
//! - Detect spam using honeypot fields
//! - Send emails via SMTP with various TLS configurations
use lettre::{
Message, SmtpTransport, Transport, message::header::ContentType,
transport::smtp::authentication::Credentials,
};
use poem_openapi::{ApiResponse, Object, OpenApi, payload::Json};
use validator::Validate;
use super::ApiCategory;
use crate::settings::{EmailSettings, Starttls};
pub mod errors;
use errors::ContactError;
impl TryFrom<&EmailSettings> for SmtpTransport {
type Error = lettre::transport::smtp::Error;
fn try_from(settings: &EmailSettings) -> Result<Self, Self::Error> {
if settings.tls {
// Implicit TLS (SMTPS) - typically port 465
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using implicit TLS (SMTPS)");
let creds = Credentials::new(settings.user.clone(), settings.password.clone());
Ok(Self::relay(&settings.host)?
.port(settings.port)
.credentials(creds)
.build())
} else {
// STARTTLS or no encryption
match settings.starttls {
Starttls::Never => {
// For local development without TLS
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using unencrypted connection");
let builder = Self::builder_dangerous(&settings.host).port(settings.port);
if settings.user.is_empty() {
Ok(builder.build())
} else {
let creds =
Credentials::new(settings.user.clone(), settings.password.clone());
Ok(builder.credentials(creds).build())
}
}
Starttls::Opportunistic | Starttls::Always => {
// STARTTLS - typically port 587
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using STARTTLS");
let creds = Credentials::new(settings.user.clone(), settings.password.clone());
Ok(Self::starttls_relay(&settings.host)?
.port(settings.port)
.credentials(creds)
.build())
}
}
}
}
}
#[derive(Debug, Object, Validate)]
struct ContactRequest {
#[validate(length(min = 1, max = "100", code = "name"))]
name: String,
#[validate(email(code = "email"))]
email: String,
#[validate(length(min = 10, max = 5000, code = "message"))]
message: String,
/// Honeypot field - should always be empty
#[oai(rename = "website")]
honeypot: Option<String>,
}
impl TryFrom<&ContactRequest> for lettre::message::Mailbox {
type Error = ContactError;
fn try_from(value: &ContactRequest) -> Result<Self, Self::Error> {
value.email.parse().map_or_else(
|_| {
Err(ContactError::CouldNotParseRequestEmailAddress(
value.email.clone(),
))
},
|email| {
Ok(Self {
name: Some(value.name.clone()),
email,
})
},
)
}
}
#[derive(Debug, Object, serde::Deserialize)]
struct ContactResponse {
success: bool,
message: String,
}
impl From<ContactResponse> for Json<ContactResponse> {
fn from(value: ContactResponse) -> Self {
Self(value)
}
}
impl ContactResponse {
pub fn success() -> Self {
Self {
success: true,
message: "backend.contact.success".to_owned(),
}
}
pub fn honeypot_response() -> Self {
Self {
success: true,
message: "backend.contact.honeypot".to_owned(),
}
}
}
#[derive(ApiResponse)]
enum ContactApiResponse {
/// Success
#[oai(status = 200)]
Ok(Json<ContactResponse>),
/// Bad Request - validation failed
#[oai(status = 400)]
BadRequest(Json<ContactResponse>),
/// Too Many Requests - rate limit exceeded
#[oai(status = 429)]
#[allow(dead_code)]
TooManyRequests,
/// Internal Server Error
#[oai(status = 500)]
InternalServerError(Json<ContactResponse>),
}
/// API for handling contact form submissions and sending emails.
#[derive(Clone)]
pub struct ContactApi {
settings: EmailSettings,
}
impl From<EmailSettings> for ContactApi {
fn from(settings: EmailSettings) -> Self {
Self { settings }
}
}
#[OpenApi(tag = "ApiCategory::Contact")]
impl ContactApi {
/// Submit a contact form
///
/// Send a message through the contact form. Rate limited to prevent spam.
#[oai(path = "/contact", method = "post")]
async fn submit_contact(
&self,
body: Json<ContactRequest>,
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
) -> ContactApiResponse {
let body = body.0;
if body.honeypot.is_some() {
tracing::event!(
target: "backend::contact",
tracing::Level::INFO,
"Honeypot triggered, rejecting request silently. IP: {}",
remote_addr.map_or_else(|| "No remote address found".to_owned(), |ip| ip.0.to_string())
);
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
}
if let Err(e) = body.validate() {
return ContactApiResponse::BadRequest(
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
.into(),
);
}
match self.send_emails(&body).await {
Ok(()) => {
tracing::event!(
target: "backend::contact",
tracing::Level::INFO, "Message from \"{} <{}>\" sent successfully",
body.name,
body.email
);
ContactApiResponse::Ok(ContactResponse::success().into())
}
Err(e) => {
tracing::event!(target: "backend::contact", tracing::Level::ERROR, "Failed to send email: {}", e);
ContactApiResponse::InternalServerError(e.into())
}
}
}
fn make_email_sender(&self, request: &ContactRequest) -> Result<Message, ContactError> {
let email_body = format!(
"You submitted the following email:\n\nMessage:\n{}\n\nIll try to reply to it as soon as possible. Take care!\n\nBest\n\n***\nThis is an automated email. Please do not reply to it.\n***",
request.message
);
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Sending email content to sender: {}", email_body);
let email = Message::builder()
.from(self.settings.try_sender_into_mailbox()?)
.to(request.try_into()?)
.subject("You sent a contact request!".to_string())
.header(ContentType::TEXT_PLAIN)
.body(email_body)?;
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}"));
Ok(email)
}
fn make_email_recipient(&self, request: &ContactRequest) -> Result<Message, ContactError> {
let email_body = format!(
"New contact form submission:\n\nName: {}\nEmail: {}\n\nMessage:\n{}",
request.name, request.email, request.message
);
tracing::event!(target: "email", tracing::Level::DEBUG, "Sending email content to recipient: {}", email_body);
let email = Message::builder()
.from(self.settings.try_sender_into_mailbox()?)
.reply_to(request.try_into()?)
.to(self.settings.try_recpient_into_mailbox()?)
.subject(format!("Contact Form: {}", request.name))
.header(ContentType::TEXT_PLAIN)
.body(email_body)?;
tracing::event!(target: "contact", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}"));
Ok(email)
}
async fn send_emails(&self, request: &ContactRequest) -> Result<(), ContactError> {
let mailer = SmtpTransport::try_from(&self.settings)?;
let email_to_sender = self.make_email_sender(request)?;
let email_to_recipient = self.make_email_recipient(request)?;
mailer
.send(&email_to_sender)
.and_then(|_| mailer.send(&email_to_recipient))?;
Ok(())
}
/// Internal method for testing - sends emails using a provided transport
#[cfg(test)]
fn send_emails_with_transport<T: Transport>(
&self,
request: &ContactRequest,
transport: &T,
) -> Result<(), ContactError>
where
T::Error: std::fmt::Debug + std::fmt::Display,
{
let email_to_sender = self.make_email_sender(request)?;
let email_to_recipient = self.make_email_recipient(request)?;
transport
.send(&email_to_sender)
.map_err(|e| ContactError::CouldNotSendEmail(format!("{e:?}")))?;
transport
.send(&email_to_recipient)
.map_err(|e| ContactError::CouldNotSendEmail(format!("{e:?}")))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use lettre::transport::stub::StubTransport;
// Tests for ContactRequest validation
#[test]
fn contact_request_valid() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "This is a test message that is long enough.".to_string(),
honeypot: None,
};
assert!(request.validate().is_ok());
}
#[test]
fn contact_request_name_too_short() {
let request = ContactRequest {
name: String::new(),
email: "john@example.com".to_string(),
message: "This is a test message that is long enough.".to_string(),
honeypot: None,
};
assert!(request.validate().is_err());
}
#[test]
fn contact_request_name_too_long() {
let request = ContactRequest {
name: "a".repeat(101),
email: "john@example.com".to_string(),
message: "This is a test message that is long enough.".to_string(),
honeypot: None,
};
assert!(request.validate().is_err());
}
#[test]
fn contact_request_name_at_max_length() {
let request = ContactRequest {
name: "a".repeat(100),
email: "john@example.com".to_string(),
message: "This is a test message that is long enough.".to_string(),
honeypot: None,
};
assert!(request.validate().is_ok());
}
#[test]
fn contact_request_invalid_email() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "not-an-email".to_string(),
message: "This is a test message that is long enough.".to_string(),
honeypot: None,
};
assert!(request.validate().is_err());
}
#[test]
fn contact_request_message_too_short() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Short".to_string(),
honeypot: None,
};
assert!(request.validate().is_err());
}
#[test]
fn contact_request_message_too_long() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "a".repeat(5001),
honeypot: None,
};
assert!(request.validate().is_err());
}
#[test]
fn contact_request_message_at_min_length() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "a".repeat(10),
honeypot: None,
};
assert!(request.validate().is_ok());
}
#[test]
fn contact_request_message_at_max_length() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "a".repeat(5000),
honeypot: None,
};
assert!(request.validate().is_ok());
}
// Tests for SmtpTransport TryFrom implementation
#[test]
fn smtp_transport_implicit_tls() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 465,
user: "user@example.com".to_string(),
password: "password".to_string(),
from: "from@example.com".to_string(),
recipient: "to@example.com".to_string(),
tls: true,
starttls: Starttls::Never,
};
let result = SmtpTransport::try_from(&settings);
assert!(result.is_ok());
}
#[test]
fn smtp_transport_starttls_always() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
password: "password".to_string(),
from: "from@example.com".to_string(),
recipient: "to@example.com".to_string(),
tls: false,
starttls: Starttls::Always,
};
let result = SmtpTransport::try_from(&settings);
assert!(result.is_ok());
}
#[test]
fn smtp_transport_starttls_opportunistic() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
password: "password".to_string(),
from: "from@example.com".to_string(),
recipient: "to@example.com".to_string(),
tls: false,
starttls: Starttls::Opportunistic,
};
let result = SmtpTransport::try_from(&settings);
assert!(result.is_ok());
}
#[test]
fn smtp_transport_no_encryption_with_credentials() {
let settings = EmailSettings {
host: "localhost".to_string(),
port: 1025,
user: "user@example.com".to_string(),
password: "password".to_string(),
from: "from@example.com".to_string(),
recipient: "to@example.com".to_string(),
tls: false,
starttls: Starttls::Never,
};
let result = SmtpTransport::try_from(&settings);
assert!(result.is_ok());
}
#[test]
fn smtp_transport_no_encryption_no_credentials() {
let settings = EmailSettings {
host: "localhost".to_string(),
port: 1025,
user: String::new(),
password: String::new(),
from: "from@example.com".to_string(),
recipient: "to@example.com".to_string(),
tls: false,
starttls: Starttls::Never,
};
let result = SmtpTransport::try_from(&settings);
assert!(result.is_ok());
}
// Integration tests for contact API endpoint
#[tokio::test]
async fn contact_endpoint_honeypot_triggered() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let body = serde_json::json!({
"name": "Bot Name",
"email": "bot@example.com",
"message": "This is a spam message from a bot.",
"website": "http://spam.com"
});
let resp = cli.post("/api/contact").body_json(&body).send().await;
resp.assert_status_is_ok();
let json_text = resp.0.into_body().into_string().await.unwrap();
let json: ContactResponse = serde_json::from_str(&json_text).unwrap();
assert!(json.success);
assert!(json.message.eq("backend.contact.honeypot"));
}
#[tokio::test]
async fn contact_endpoint_validation_error_empty_name() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let body = serde_json::json!({
"name": "",
"email": "test@example.com",
"message": "This is a valid message that is long enough."
});
let resp = cli.post("/api/contact").body_json(&body).send().await;
resp.assert_status(poem::http::StatusCode::BAD_REQUEST);
let json_text = resp.0.into_body().into_string().await.unwrap();
let json: ContactResponse = serde_json::from_str(&json_text).unwrap();
assert!(!json.success);
assert!(json.message.eq("backend.contact.errors.validation.name"));
}
#[tokio::test]
async fn contact_endpoint_validation_error_invalid_email() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let body = serde_json::json!({
"name": "Test User",
"email": "not-an-email",
"message": "This is a valid message that is long enough."
});
let resp = cli.post("/api/contact").body_json(&body).send().await;
resp.assert_status(poem::http::StatusCode::BAD_REQUEST);
let json_text = resp.0.into_body().into_string().await.unwrap();
let json: ContactResponse = serde_json::from_str(&json_text).unwrap();
assert!(!json.success);
assert!(json.message.eq("backend.contact.errors.validation.email"));
}
#[tokio::test]
async fn contact_endpoint_validation_error_message_too_short() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let body = serde_json::json!({
"name": "Test User",
"email": "test@example.com",
"message": "Short"
});
let resp = cli.post("/api/contact").body_json(&body).send().await;
resp.assert_status(poem::http::StatusCode::BAD_REQUEST);
let json_text = resp.0.into_body().into_string().await.unwrap();
let json: ContactResponse = serde_json::from_str(&json_text).unwrap();
assert!(!json.success);
assert!(json.message.eq("backend.contact.errors.validation.message"));
}
#[tokio::test]
async fn contact_endpoint_validation_error_name_too_long() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let body = serde_json::json!({
"name": "a".repeat(101),
"email": "test@example.com",
"message": "This is a valid message that is long enough."
});
let resp = cli.post("/api/contact").body_json(&body).send().await;
resp.assert_status(poem::http::StatusCode::BAD_REQUEST);
let json_text = resp.0.into_body().into_string().await.unwrap();
let json: ContactResponse = serde_json::from_str(&json_text).unwrap();
assert!(!json.success);
assert!(json.message.eq("backend.contact.errors.validation.name"));
}
#[tokio::test]
async fn contact_endpoint_validation_error_message_too_long() {
let app = crate::get_test_app();
let cli = poem::test::TestClient::new(app);
let body = serde_json::json!({
"name": "Test User",
"email": "test@example.com",
"message": "a".repeat(5001)
});
let resp = cli.post("/api/contact").body_json(&body).send().await;
resp.assert_status(poem::http::StatusCode::BAD_REQUEST);
let json_text = resp.0.into_body().into_string().await.unwrap();
let json: ContactResponse = serde_json::from_str(&json_text).unwrap();
assert!(!json.success);
assert!(json.message.eq("backend.contact.errors.validation.message"));
}
// Tests for ContactRequest TryFrom to Mailbox
#[test]
fn contact_request_to_mailbox_success() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "This is a test message.".to_string(),
honeypot: None,
};
let result: Result<lettre::message::Mailbox, ContactError> = (&request).try_into();
assert!(result.is_ok());
let mailbox = result.unwrap();
assert_eq!(mailbox.name, Some("John Doe".to_string()));
assert_eq!(mailbox.email.to_string(), "john@example.com");
}
#[test]
fn contact_request_to_mailbox_invalid_email() {
let request = ContactRequest {
name: "John Doe".to_string(),
email: "not-an-email".to_string(),
message: "This is a test message.".to_string(),
honeypot: None,
};
let result: Result<lettre::message::Mailbox, ContactError> = (&request).try_into();
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotParseRequestEmailAddress(email) => {
assert_eq!(email, "not-an-email");
}
_ => panic!("Expected CouldNotParseRequestEmailAddress error"),
}
}
// Tests for ContactResponse factory methods
#[test]
fn contact_response_success_creates_correct_response() {
let response = ContactResponse::success();
assert!(response.success);
assert_eq!(response.message, "backend.contact.success");
}
#[test]
fn contact_response_honeypot_creates_correct_response() {
let response = ContactResponse::honeypot_response();
assert!(response.success);
assert_eq!(response.message, "backend.contact.honeypot");
}
// Tests for ContactResponse to Json conversion
#[test]
fn contact_response_to_json() {
let response = ContactResponse::success();
let json: Json<ContactResponse> = response.into();
assert!(json.0.success);
assert_eq!(json.0.message, "backend.contact.success");
}
// Tests for email building methods
#[test]
fn make_email_sender_builds_correct_message() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message content".to_string(),
honeypot: None,
};
let result = api.make_email_sender(&request);
assert!(result.is_ok());
let message = result.unwrap();
let message_str = format!("{message:?}");
// Check that the message contains key elements
assert!(message_str.contains("john@example.com"));
assert!(message_str.contains("John Doe"));
assert!(message_str.contains("noreply@example.com"));
}
#[test]
fn make_email_sender_fails_with_invalid_from_address() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "invalid-email".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
let result = api.make_email_sender(&request);
assert!(result.is_err());
}
#[test]
fn make_email_sender_fails_with_invalid_request_email() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "invalid-email".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
let result = api.make_email_sender(&request);
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotParseRequestEmailAddress(_) => (),
_ => panic!("Expected CouldNotParseRequestEmailAddress error"),
}
}
#[test]
fn make_email_recipient_builds_correct_message() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message content".to_string(),
honeypot: None,
};
let result = api.make_email_recipient(&request);
assert!(result.is_ok());
let message = result.unwrap();
let message_str = format!("{message:?}");
// Check that the message contains key elements
assert!(message_str.contains("admin@example.com"));
assert!(message_str.contains("john@example.com")); // Reply-to
assert!(message_str.contains("noreply@example.com"));
assert!(message_str.contains("Contact Form: John Doe"));
}
#[test]
fn make_email_recipient_fails_with_invalid_recipient() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "invalid-email".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
let result = api.make_email_recipient(&request);
assert!(result.is_err());
}
#[test]
fn make_email_recipient_fails_with_invalid_from_address() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "invalid-email".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
let result = api.make_email_recipient(&request);
assert!(result.is_err());
}
#[test]
fn make_email_recipient_includes_message_content() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "Jane Smith".to_string(),
email: "jane@example.com".to_string(),
message: "This is a unique test message with specific content".to_string(),
honeypot: None,
};
let result = api.make_email_recipient(&request);
assert!(result.is_ok());
}
#[test]
fn contact_api_from_email_settings() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Always,
tls: false,
};
let api = ContactApi::from(settings.clone());
assert_eq!(api.settings.host, settings.host);
assert_eq!(api.settings.port, settings.port);
assert_eq!(api.settings.from, settings.from);
}
// Tests for send_emails with mock transport
#[test]
fn send_emails_with_stub_transport_success() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message content".to_string(),
honeypot: None,
};
let transport = StubTransport::new_ok();
let result = api.send_emails_with_transport(&request, &transport);
assert!(result.is_ok());
}
#[test]
fn send_emails_with_stub_transport_sends_two_emails() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "Jane Smith".to_string(),
email: "jane@example.com".to_string(),
message: "Another test message".to_string(),
honeypot: None,
};
let transport = StubTransport::new_ok();
api.send_emails_with_transport(&request, &transport)
.unwrap();
// StubTransport doesn't provide a way to count messages, but we verified it succeeded
// If either email failed to build or send, the test would fail
}
#[test]
fn send_emails_with_stub_transport_fails_with_invalid_from() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "invalid-email".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
let transport = StubTransport::new_ok();
let result = api.send_emails_with_transport(&request, &transport);
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotParseSettingsEmail(_) => (),
e => panic!("Expected CouldNotParseSettingsEmail, got {:?}", e),
}
}
#[test]
fn send_emails_with_stub_transport_fails_with_invalid_request_email() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "not-an-email".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
let transport = StubTransport::new_ok();
let result = api.send_emails_with_transport(&request, &transport);
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotParseRequestEmailAddress(_) => (),
e => panic!("Expected CouldNotParseRequestEmailAddress, got {:?}", e),
}
}
#[test]
fn send_emails_with_failing_transport() {
let settings = EmailSettings {
host: "smtp.example.com".to_string(),
port: 587,
user: "user@example.com".to_string(),
from: "noreply@example.com".to_string(),
password: "password".to_string(),
recipient: "admin@example.com".to_string(),
starttls: Starttls::Never,
tls: false,
};
let api = ContactApi::from(settings);
let request = ContactRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
message: "Test message".to_string(),
honeypot: None,
};
// Create a transport that always fails
let transport = StubTransport::new_error();
let result = api.send_emails_with_transport(&request, &transport);
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotSendEmail(_) => (),
e => panic!("Expected CouldNotSendEmail, got {:?}", e),
}
}
}