Files
bakit/src/route/contact/errors.rs
T

427 lines
16 KiB
Rust
Raw Normal View History

2025-11-15 14:08:37 +01:00
use std::error::Error;
use lettre::address::AddressError;
use poem_openapi::payload::Json;
use validator::ValidationErrors;
use super::ContactResponse;
/// Errors that can occur during contact form processing and email sending.
#[derive(Debug)]
pub enum ContactError {
/// The email address provided in the contact form request could not be parsed.
///
/// This typically indicates the user submitted an invalid email address format.
CouldNotParseRequestEmailAddress(String),
/// The email address configured in application settings could not be parsed.
///
/// This indicates a configuration error with the sender or recipient email addresses.
CouldNotParseSettingsEmail(String),
/// Failed to construct the email message.
///
/// This can occur due to invalid message content or headers.
FailedToBuildMessage(String),
/// Failed to send the email through the SMTP server.
///
/// This can occur due to network issues, authentication failures, or SMTP server errors.
CouldNotSendEmail(String),
/// A general validation error occurred that doesn't fit specific field validation.
///
/// This is used for validation errors that don't map to a specific form field.
ValidationError(String),
/// The name field in the contact form failed validation.
///
/// This typically occurs when the name is empty, too short, or contains invalid characters.
ValidationNameError(String),
/// The email field in the contact form failed validation.
///
/// This typically occurs when the email address format is invalid.
ValidationEmailError(String),
/// The message field in the contact form failed validation.
///
/// This typically occurs when the message is empty, too short.
ValidationMessageError(String),
/// An unspecified internal error occurred.
OtherError(String),
}
impl Error for ContactError {}
/// Converts a lettre SMTP transport error into a `ContactError`.
///
/// SMTP errors are logged at ERROR level with full details, then
/// mapped to `OtherError` as they represent server-side or network
/// issues beyond the client's control.
impl From<lettre::transport::smtp::Error> for ContactError {
fn from(value: lettre::transport::smtp::Error) -> Self {
tracing::event!(target: "contact", tracing::Level::ERROR, "SMTP Error details: {}", format!("{value:?}"));
Self::OtherError(value.to_string())
}
}
impl std::fmt::Display for ContactError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::CouldNotParseRequestEmailAddress(e) => {
format!("Failed to parse requester's email address: {e:?}")
}
Self::CouldNotParseSettingsEmail(e) => {
format!("Failed to parse email address in settings: {e:?}")
}
Self::FailedToBuildMessage(e) => {
format!("Failed to build the message to be sent: {e:?}")
}
Self::CouldNotSendEmail(e) => format!("Failed to send the email: {e:?}"),
Self::ValidationError(e) => format!("Failed to validate request: {e:?}"),
Self::ValidationNameError(e) => format!("Failed to validate name: {e:?}"),
Self::ValidationEmailError(e) => format!("Failed to validate email: {e:?}"),
Self::ValidationMessageError(e) => format!("Failed to validate message: {e:?}"),
Self::OtherError(e) => format!("Other internal error: {e:?}"),
};
write!(f, "{message}")
}
}
/// Converts validation errors into a `ContactError`.
///
/// This implementation inspects the validation errors to determine which specific field
/// failed validation (name, email, or message) and returns the appropriate variant.
/// If no specific field can be identified, returns a generic `ValidationError`.
impl From<ValidationErrors> for ContactError {
fn from(value: ValidationErrors) -> Self {
if validator::ValidationErrors::has_error(&Err(value.clone()), "name") {
return Self::ValidationNameError("backend.contact.errors.validation.name".to_owned());
}
if validator::ValidationErrors::has_error(&Err(value.clone()), "email") {
return Self::ValidationEmailError(
"backend.contact.errors.validation.email".to_owned(),
);
2025-11-15 14:08:37 +01:00
}
if validator::ValidationErrors::has_error(&Err(value), "message") {
return Self::ValidationMessageError(
"backend.contact.errors.validation.message".to_owned(),
);
2025-11-15 14:08:37 +01:00
}
Self::ValidationError("backend.contact.errors.validation.other".to_owned())
}
}
/// Converts a `ContactError` into a `ContactResponse`.
///
/// This maps error variants to user-facing error message keys for internationalization.
/// Validation errors map to specific field error keys, while internal errors
/// (settings, email building, SMTP issues) all map to a generic internal error key.
impl From<ContactError> for ContactResponse {
fn from(value: ContactError) -> Self {
Self {
success: false,
message: match value {
ContactError::CouldNotParseRequestEmailAddress(_)
| ContactError::ValidationEmailError(_) => {
"backend.contact.errors.validation.email"
}
2025-11-15 14:08:37 +01:00
ContactError::ValidationNameError(_) => "backend.contact.errors.validation.name",
ContactError::ValidationMessageError(_) => {
"backend.contact.errors.validation.message"
}
2025-11-15 14:08:37 +01:00
ContactError::CouldNotParseSettingsEmail(_)
| ContactError::FailedToBuildMessage(_)
| ContactError::CouldNotSendEmail(_)
| ContactError::OtherError(_) => "backend.contact.errors.internal",
ContactError::ValidationError(_) => "backend.contact.errors.validation.other",
}
.to_string(),
}
}
}
/// Converts validation errors directly into a `ContactResponse`.
///
/// This is a convenience implementation that first converts `ValidationErrors` to
/// `ContactError`, then converts that to `ContactResponse`. This allows validation
/// errors to be returned directly from handlers as responses.
impl From<ValidationErrors> for ContactResponse {
fn from(value: ValidationErrors) -> Self {
let error: ContactError = value.into();
error.into()
}
}
/// Converts a lettre `AddressError` into a `ContactError`.
///
/// Address parsing errors from lettre are mapped to `CouldNotParseSettingsEmail`
/// as they typically occur when parsing email addresses from application settings.
impl From<AddressError> for ContactError {
fn from(value: AddressError) -> Self {
Self::CouldNotParseSettingsEmail(value.to_string())
}
}
/// Converts a lettre `Error` into a `ContactError`.
///
/// Lettre errors during message construction are mapped to `FailedToBuildMessage`.
/// These errors typically occur when building email messages with invalid headers or content.
impl From<lettre::error::Error> for ContactError {
fn from(value: lettre::error::Error) -> Self {
Self::FailedToBuildMessage(value.to_string())
}
}
impl From<ContactError> for Json<ContactResponse> {
fn from(value: ContactError) -> Self {
let response: ContactResponse = value.into();
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<lettre::Address, AddressError> = "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() {
2025-11-20 10:50:47 +01:00
use validator::Validate;
2025-11-15 14:08:37 +01:00
#[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<ContactResponse> = 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");
}
}