From a3abe0f71648369f8269c4752d9a7eae4fad5bc1 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Sat, 15 Nov 2025 23:18:03 +0100 Subject: [PATCH] test: improve test coverage --- src/route/contact/errors.rs | 251 ++++++++++++++++++++ src/route/contact/mod.rs | 451 ++++++++++++++++++++++++++++++++++++ src/settings.rs | 72 ++++++ 3 files changed, 774 insertions(+) diff --git a/src/route/contact/errors.rs b/src/route/contact/errors.rs index 40d3bdb..2eeee09 100644 --- a/src/route/contact/errors.rs +++ b/src/route/contact/errors.rs @@ -165,3 +165,254 @@ impl From for Json { response.into() } } + +#[cfg(test)] +mod tests { + use super::*; + use lettre::address::AddressError; + + #[test] + fn contact_error_display_could_not_parse_request_email() { + let error = ContactError::CouldNotParseRequestEmailAddress("invalid".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to parse requester's email address")); + assert!(display.contains("invalid")); + } + + #[test] + fn contact_error_display_could_not_parse_settings_email() { + let error = ContactError::CouldNotParseSettingsEmail("invalid".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to parse email address in settings")); + assert!(display.contains("invalid")); + } + + #[test] + fn contact_error_display_failed_to_build_message() { + let error = ContactError::FailedToBuildMessage("build error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to build the message to be sent")); + assert!(display.contains("build error")); + } + + #[test] + fn contact_error_display_could_not_send_email() { + let error = ContactError::CouldNotSendEmail("send error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to send the email")); + assert!(display.contains("send error")); + } + + #[test] + fn contact_error_display_validation_error() { + let error = ContactError::ValidationError("validation error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to validate request")); + assert!(display.contains("validation error")); + } + + #[test] + fn contact_error_display_validation_name_error() { + let error = ContactError::ValidationNameError("name error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to validate name")); + assert!(display.contains("name error")); + } + + #[test] + fn contact_error_display_validation_email_error() { + let error = ContactError::ValidationEmailError("email error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to validate email")); + assert!(display.contains("email error")); + } + + #[test] + fn contact_error_display_validation_message_error() { + let error = ContactError::ValidationMessageError("message error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Failed to validate message")); + assert!(display.contains("message error")); + } + + #[test] + fn contact_error_display_other_error() { + let error = ContactError::OtherError("other error".to_string()); + let display = format!("{error}"); + assert!(display.contains("Other internal error")); + assert!(display.contains("other error")); + } + + #[test] + fn from_address_error_creates_could_not_parse_settings_email() { + let address_error: Result = "invalid email".parse(); + let error: ContactError = address_error.unwrap_err().into(); + match error { + ContactError::CouldNotParseSettingsEmail(_) => (), + _ => panic!("Expected CouldNotParseSettingsEmail variant"), + } + } + + #[test] + fn from_lettre_error_creates_failed_to_build_message() { + // Create an invalid message to trigger a lettre error + let result = lettre::Message::builder().body(String::new()); + assert!(result.is_err()); + let lettre_error = result.unwrap_err(); + let error: ContactError = lettre_error.into(); + match error { + ContactError::FailedToBuildMessage(_) => (), + _ => panic!("Expected FailedToBuildMessage variant"), + } + } + + #[test] + fn from_validation_errors_with_name_error() { + use validator::{Validate, ValidationError}; + + #[derive(Validate)] + struct TestStruct { + #[validate(length(min = 1))] + name: String, + } + + let test = TestStruct { + name: String::new(), + }; + let validation_errors = test.validate().unwrap_err(); + let error: ContactError = validation_errors.into(); + match error { + ContactError::ValidationNameError(msg) => { + assert_eq!(msg, "backend.contact.errors.validation.name"); + } + _ => panic!("Expected ValidationNameError variant"), + } + } + + #[test] + fn from_validation_errors_with_email_error() { + use validator::Validate; + + #[derive(Validate)] + struct TestStruct { + #[validate(email)] + email: String, + } + + let test = TestStruct { + email: "invalid".to_string(), + }; + let validation_errors = test.validate().unwrap_err(); + let error: ContactError = validation_errors.into(); + match error { + ContactError::ValidationEmailError(msg) => { + assert_eq!(msg, "backend.contact.errors.validation.email"); + } + _ => panic!("Expected ValidationEmailError variant"), + } + } + + #[test] + fn from_validation_errors_with_message_error() { + use validator::Validate; + + #[derive(Validate)] + struct TestStruct { + #[validate(length(min = 10))] + message: String, + } + + let test = TestStruct { + message: "short".to_string(), + }; + let validation_errors = test.validate().unwrap_err(); + let error: ContactError = validation_errors.into(); + match error { + ContactError::ValidationMessageError(msg) => { + assert_eq!(msg, "backend.contact.errors.validation.message"); + } + _ => panic!("Expected ValidationMessageError variant"), + } + } + + #[test] + fn contact_error_to_response_email_validation() { + let error = ContactError::ValidationEmailError("test".to_string()); + let response: ContactResponse = error.into(); + assert!(!response.success); + assert_eq!(response.message, "backend.contact.errors.validation.email"); + } + + #[test] + fn contact_error_to_response_name_validation() { + let error = ContactError::ValidationNameError("test".to_string()); + let response: ContactResponse = error.into(); + assert!(!response.success); + assert_eq!(response.message, "backend.contact.errors.validation.name"); + } + + #[test] + fn contact_error_to_response_message_validation() { + let error = ContactError::ValidationMessageError("test".to_string()); + let response: ContactResponse = error.into(); + assert!(!response.success); + assert_eq!( + response.message, + "backend.contact.errors.validation.message" + ); + } + + #[test] + fn contact_error_to_response_internal_errors() { + let test_cases = vec![ + ContactError::CouldNotParseSettingsEmail("test".to_string()), + ContactError::FailedToBuildMessage("test".to_string()), + ContactError::CouldNotSendEmail("test".to_string()), + ContactError::OtherError("test".to_string()), + ]; + + for error in test_cases { + let response: ContactResponse = error.into(); + assert!(!response.success); + assert_eq!(response.message, "backend.contact.errors.internal"); + } + } + + #[test] + fn contact_error_to_response_other_validation() { + let error = ContactError::ValidationError("test".to_string()); + let response: ContactResponse = error.into(); + assert!(!response.success); + assert_eq!(response.message, "backend.contact.errors.validation.other"); + } + + #[test] + fn contact_error_to_json_response() { + let error = ContactError::ValidationEmailError("test".to_string()); + let json_response: Json = error.into(); + assert!(!json_response.0.success); + assert_eq!( + json_response.0.message, + "backend.contact.errors.validation.email" + ); + } + + #[test] + fn validation_errors_to_response() { + use validator::Validate; + + #[derive(Validate)] + struct TestStruct { + #[validate(email)] + email: String, + } + + let test = TestStruct { + email: "invalid".to_string(), + }; + let validation_errors = test.validate().unwrap_err(); + let response: ContactResponse = validation_errors.into(); + assert!(!response.success); + assert_eq!(response.message, "backend.contact.errors.validation.email"); + } +} diff --git a/src/route/contact/mod.rs b/src/route/contact/mod.rs index cc9b4fc..959b9d7 100644 --- a/src/route/contact/mod.rs +++ b/src/route/contact/mod.rs @@ -235,11 +235,35 @@ impl ContactApi { .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( + &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] @@ -548,4 +572,431 @@ mod tests { 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 = (&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 = (&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 = 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), + } + } } diff --git a/src/settings.rs b/src/settings.rs index 83cc627..a5a98ac 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -646,4 +646,76 @@ mod tests { assert!(debug_output.contains("smtp.example.com")); assert!(debug_output.contains("user@example.com")); } + + #[test] + fn email_settings_try_sender_into_mailbox_success() { + let settings = EmailSettings { + host: "smtp.example.com".to_string(), + port: 587, + user: "user@example.com".to_string(), + from: "sender@example.com".to_string(), + password: "password".to_string(), + recipient: "recipient@example.com".to_string(), + starttls: Starttls::Always, + tls: false, + }; + + let result = settings.try_sender_into_mailbox(); + assert!(result.is_ok()); + let mailbox = result.unwrap(); + assert_eq!(mailbox.email.to_string(), "sender@example.com"); + } + + #[test] + fn email_settings_try_sender_into_mailbox_invalid() { + 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: "recipient@example.com".to_string(), + starttls: Starttls::Always, + tls: false, + }; + + let result = settings.try_sender_into_mailbox(); + assert!(result.is_err()); + } + + #[test] + fn email_settings_try_recipient_into_mailbox_success() { + let settings = EmailSettings { + host: "smtp.example.com".to_string(), + port: 587, + user: "user@example.com".to_string(), + from: "sender@example.com".to_string(), + password: "password".to_string(), + recipient: "recipient@example.com".to_string(), + starttls: Starttls::Always, + tls: false, + }; + + let result = settings.try_recpient_into_mailbox(); + assert!(result.is_ok()); + let mailbox = result.unwrap(); + assert_eq!(mailbox.email.to_string(), "recipient@example.com"); + } + + #[test] + fn email_settings_try_recipient_into_mailbox_invalid() { + let settings = EmailSettings { + host: "smtp.example.com".to_string(), + port: 587, + user: "user@example.com".to_string(), + from: "sender@example.com".to_string(), + password: "password".to_string(), + recipient: "invalid-email".to_string(), + starttls: Starttls::Always, + tls: false, + }; + + let result = settings.try_recpient_into_mailbox(); + assert!(result.is_err()); + } }