#![allow(clippy::unwrap_used)]
use super::*;
fn response(code: u16) -> SmtpResponse {
SmtpResponse {
code,
enhanced_code: None,
lines: vec!["test".into()],
}
}
#[test]
fn transient_error_is_transient() {
let err = Error::Transient {
code: 421,
message: "try again".into(),
response: response(421),
};
assert!(
err.is_transient(),
"Transient error must return true for is_transient()"
);
}
#[test]
fn io_error_is_transient() {
let err = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"connection reset",
)));
assert!(
err.is_transient(),
"Io error must return true for is_transient()"
);
}
#[test]
fn timeout_error_is_transient() {
let err = Error::Timeout;
assert!(
err.is_transient(),
"Timeout error must return true for is_transient()"
);
}
#[test]
fn permanent_error_is_not_transient() {
let err = Error::Permanent {
code: 550,
message: "mailbox not found".into(),
response: response(550),
};
assert!(
!err.is_transient(),
"Permanent error must return false for is_transient()"
);
}
#[test]
fn auth_transient_is_transient() {
let err = Error::Auth {
message: "temporary auth failure".into(),
response: response(454),
};
assert!(
err.is_transient(),
"Auth error with 4xx response must return true for is_transient() \
(RFC 4954 Section 4)"
);
}
#[test]
fn auth_permanent_is_not_transient() {
let err = Error::Auth {
message: "bad credentials".into(),
response: response(535),
};
assert!(
!err.is_transient(),
"Auth error with 5xx response must return false for is_transient()"
);
}
#[test]
fn parse_error_is_not_transient() {
let err = Error::Parse("bad response".into());
assert!(
!err.is_transient(),
"Parse error must return false for is_transient()"
);
}
#[test]
fn protocol_error_is_not_transient() {
let err = Error::Protocol("violation".into());
assert!(
!err.is_transient(),
"Protocol error must return false for is_transient()"
);
}
#[test]
fn closed_error_is_not_transient() {
let err = Error::Closed;
assert!(
!err.is_transient(),
"Closed error must return false for is_transient()"
);
}
#[test]
fn permanent_error_is_permanent() {
let err = Error::Permanent {
code: 550,
message: "mailbox not found".into(),
response: response(550),
};
assert!(
err.is_permanent(),
"Permanent error must return true for is_permanent()"
);
}
#[test]
fn transient_error_is_not_permanent() {
let err = Error::Transient {
code: 421,
message: "try again".into(),
response: response(421),
};
assert!(
!err.is_permanent(),
"Transient error must return false for is_permanent()"
);
}
#[test]
fn io_error_is_not_permanent() {
let err = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"refused",
)));
assert!(
!err.is_permanent(),
"Io error must return false for is_permanent()"
);
}
#[test]
fn auth_permanent_is_permanent() {
let err = Error::Auth {
message: "bad credentials".into(),
response: response(535),
};
assert!(
err.is_permanent(),
"Auth error with 5xx response must return true for is_permanent() \
(RFC 4954 Section 4)"
);
}
#[test]
fn auth_transient_is_not_permanent() {
let err = Error::Auth {
message: "temporary failure".into(),
response: response(454),
};
assert!(
!err.is_permanent(),
"Auth error with 4xx response must return false for is_permanent()"
);
}
#[test]
fn timeout_is_not_permanent() {
let err = Error::Timeout;
assert!(
!err.is_permanent(),
"Timeout error must return false for is_permanent()"
);
}
#[test]
fn parse_error_is_not_permanent() {
let err = Error::Parse("bad response".into());
assert!(
!err.is_permanent(),
"Parse error must return false for is_permanent()"
);
}
#[test]
fn starttls_unavailable_is_not_permanent() {
let err = Error::StartTlsUnavailable;
assert!(
!err.is_permanent(),
"StartTlsUnavailable must return false for is_permanent()"
);
}
#[test]
fn equal_unit_variants() {
assert_eq!(Error::Timeout, Error::Timeout);
assert_eq!(Error::Closed, Error::Closed);
assert_eq!(Error::StartTlsUnavailable, Error::StartTlsUnavailable);
assert_eq!(Error::SmtpUtf8Required, Error::SmtpUtf8Required);
}
#[test]
fn equal_string_variants() {
assert_eq!(Error::Protocol("x".into()), Error::Protocol("x".into()));
assert_eq!(Error::Parse("y".into()), Error::Parse("y".into()));
}
#[test]
fn equal_struct_variants() {
assert_eq!(
Error::Auth {
message: "fail".into(),
response: response(535),
},
Error::Auth {
message: "fail".into(),
response: response(535),
}
);
assert_eq!(
Error::Permanent {
code: 550,
message: "no such user".into(),
response: response(550),
},
Error::Permanent {
code: 550,
message: "no such user".into(),
response: response(550),
}
);
assert_eq!(
Error::AllRecipientsFailed {
count: 2,
responses: vec![response(550), response(553)],
},
Error::AllRecipientsFailed {
count: 2,
responses: vec![response(550), response(553)],
}
);
}
#[test]
fn io_equal_by_kind() {
let a = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"message A",
)));
let b = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"message B",
)));
assert_eq!(a, b, "Io variants with same ErrorKind should be equal");
}
#[test]
fn io_not_equal_different_kind() {
let a = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"x",
)));
let b = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"x",
)));
assert_ne!(
a, b,
"Io variants with different ErrorKind should not be equal"
);
}
#[test]
fn different_variants_not_equal() {
assert_ne!(Error::Timeout, Error::Closed);
assert_ne!(Error::Protocol("x".into()), Error::Parse("x".into()));
}
#[test]
fn same_variant_different_payload_not_equal() {
assert_ne!(
Error::Permanent {
code: 550,
message: "a".into(),
response: response(550),
},
Error::Permanent {
code: 553,
message: "a".into(),
response: response(553),
}
);
}
#[test]
fn all_recipients_failed_is_not_permanent() {
let err = Error::AllRecipientsFailed {
count: 2,
responses: vec![response(550), response(550)],
};
assert!(
!err.is_permanent(),
"AllRecipientsFailed must return false for is_permanent()"
);
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
fn round_trip(err: &Error) {
let json = serde_json::to_string(err).unwrap();
let deserialized: Error = serde_json::from_str(&json).unwrap();
assert_eq!(
*err, deserialized,
"round-trip failed for {err:?}\n JSON: {json}"
);
}
#[test]
fn serde_io_round_trip() {
let err = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"connection reset by peer",
)));
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("ConnectionReset"));
round_trip(&err);
}
#[test]
fn serde_io_preserves_error_kind() {
let err = Error::Io(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"pipe broken",
)));
let json = serde_json::to_string(&err).unwrap();
let deserialized: Error = serde_json::from_str(&json).unwrap();
match &deserialized {
Error::Io(inner) => {
assert_eq!(inner.kind(), std::io::ErrorKind::BrokenPipe);
}
_ => panic!("expected Io variant after deserialization"),
}
}
#[test]
fn serde_auth_round_trip() {
round_trip(&Error::Auth {
message: "bad credentials".into(),
response: response(535),
});
}
#[test]
fn serde_permanent_round_trip() {
round_trip(&Error::Permanent {
code: 550,
message: "mailbox not found".into(),
response: response(550),
});
}
#[test]
fn serde_transient_round_trip() {
round_trip(&Error::Transient {
code: 421,
message: "try again".into(),
response: response(421),
});
}
#[test]
fn serde_protocol_round_trip() {
round_trip(&Error::Protocol("violation".into()));
}
#[test]
fn serde_parse_round_trip() {
round_trip(&Error::Parse("bad response".into()));
}
#[test]
fn serde_timeout_round_trip() {
round_trip(&Error::Timeout);
}
#[test]
fn serde_closed_round_trip() {
round_trip(&Error::Closed);
}
#[test]
fn serde_starttls_unavailable_round_trip() {
round_trip(&Error::StartTlsUnavailable);
}
#[test]
fn serde_all_recipients_failed_round_trip() {
round_trip(&Error::AllRecipientsFailed {
count: 2,
responses: vec![response(550), response(553)],
});
}
#[test]
fn serde_smtp_utf8_required_round_trip() {
round_trip(&Error::SmtpUtf8Required);
}
#[test]
fn serde_json_has_type_tag() {
let err = Error::Timeout;
let json = serde_json::to_string(&err).unwrap();
assert!(
json.contains("\"type\":\"Timeout\""),
"expected type tag in JSON: {json}"
);
}
#[test]
fn serde_unknown_error_kind_deserializes_as_other() {
let json = r#"{"type":"Io","data":{"kind":"FutureKind","message":"something new"}}"#;
let err: Error = serde_json::from_str(json).unwrap();
match &err {
Error::Io(inner) => {
assert_eq!(inner.kind(), std::io::ErrorKind::Other);
assert!(inner.to_string().contains("something new"));
}
_ => panic!("expected Io variant"),
}
}
}