use crate::error::{AuthError, InvalidInputError, IoError, ProtocolError, SmtpError, SmtpOp};
use std::error::Error;
#[test]
fn smtp_error_display_protocol_includes_code_and_message() {
let e = SmtpError::Protocol(ProtocolError::UnexpectedCode {
during: SmtpOp::MailFrom,
expected_class: 2,
actual: 451,
enhanced: None,
message: "temporary local problem".into(),
});
let s = format!("{e}");
assert!(s.contains("451"), "should include actual code: {s}");
assert!(
s.contains("temporary local problem"),
"should include server text: {s}"
);
assert!(
s.contains("MAIL FROM"),
"should mention the SMTP operation in progress: {s}"
);
}
#[test]
fn smtp_op_display_uses_wire_keyword() {
for (op, expected) in [
(SmtpOp::Greeting, "greeting"),
(SmtpOp::Ehlo, "EHLO"),
(SmtpOp::StartTls, "STARTTLS"),
(SmtpOp::AuthPlain, "AUTH PLAIN"),
(SmtpOp::AuthLogin, "AUTH LOGIN"),
(SmtpOp::AuthXOAuth2, "AUTH XOAUTH2"),
(SmtpOp::MailFrom, "MAIL FROM"),
(SmtpOp::RcptTo, "RCPT TO"),
(SmtpOp::Data, "DATA"),
(SmtpOp::Quit, "QUIT"),
] {
assert_eq!(format!("{op}"), expected);
assert_eq!(op.as_str(), expected);
}
}
#[test]
fn auth_rejected_carries_server_code_and_text() {
let e = SmtpError::Auth(AuthError::Rejected {
code: 535,
enhanced: None,
message: "5.7.8 invalid".into(),
});
let s = format!("{e}");
assert!(s.contains("535"));
assert!(s.contains("5.7.8 invalid"));
}
#[test]
fn invalid_input_takes_only_static_strings() {
let e = InvalidInputError::new("test reason");
assert_eq!(e.reason(), "test reason");
assert_eq!(format!("{e}"), "test reason");
}
#[test]
fn from_conversions_wrap_in_correct_variant() {
let e: SmtpError = IoError::new("transport gone").into();
assert!(matches!(e, SmtpError::Io(_)));
let e: SmtpError = ProtocolError::UnexpectedClose.into();
assert!(matches!(e, SmtpError::Protocol(_)));
let e: SmtpError = AuthError::UnsupportedMechanism.into();
assert!(matches!(e, SmtpError::Auth(_)));
let e: SmtpError = InvalidInputError::new("x").into();
assert!(matches!(e, SmtpError::InvalidInput(_)));
}
#[test]
fn smtp_error_source_chains_to_inner_variant() {
let e: SmtpError = IoError::new("inner").into();
let src = e.source().expect("should have source");
assert!(format!("{src}").contains("inner"));
}
#[test]
fn io_error_new_has_no_source() {
let e = IoError::new("simple message");
assert_eq!(e.message(), "simple message");
assert_eq!(format!("{e}"), "simple message");
assert!(e.source().is_none(), "new() must not synthesize a source");
}
#[test]
fn io_error_with_source_preserves_inner() {
use std::io;
let inner = io::Error::new(io::ErrorKind::ConnectionRefused, "no listener at port 1");
let outer = IoError::with_source("TCP connect failed", inner);
assert_eq!(format!("{outer}"), "TCP connect failed");
let src = outer.source().expect("source must be present");
let src_str = format!("{src}");
assert!(
src_str.contains("no listener at port 1"),
"source should preserve original message: {src_str}"
);
}
#[test]
fn io_error_with_source_accepts_arbitrary_error_types() {
use std::io;
#[derive(Debug)]
struct CustomError(&'static str);
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl Error for CustomError {}
let _ = IoError::with_source("io", io::Error::other("x"));
let _ = IoError::with_source("custom", CustomError("oops"));
}
#[test]
fn io_error_from_io_error_carries_source() {
use std::io;
let original = io::Error::new(io::ErrorKind::TimedOut, "read timed out");
let wrapped: IoError = original.into();
assert!(
format!("{wrapped}").contains("read timed out"),
"From<io::Error> should use the io::Error's Display as message",
);
assert!(
wrapped.source().is_some(),
"From<io::Error> should preserve source"
);
}
#[test]
fn io_error_chains_through_smtp_error_source() {
use std::io;
let inner = io::Error::new(io::ErrorKind::BrokenPipe, "EPIPE");
let io = IoError::with_source("write failed", inner);
let smtp: SmtpError = io.into();
let level1 = smtp.source().expect("SmtpError should have source");
assert!(format!("{level1}").contains("write failed"));
let level2 = level1.source().expect("IoError should have source too");
assert!(format!("{level2}").contains("EPIPE"));
}
#[test]
fn io_error_send_sync_bounds_compile() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<IoError>();
assert_send_sync::<SmtpError>();
}
#[test]
fn io_kind_extracts_kind_from_direct_io_error() {
use std::io;
let inner = io::Error::new(io::ErrorKind::TimedOut, "took too long");
let wrapped = IoError::with_source("connect failed", inner);
assert_eq!(wrapped.io_kind(), Some(io::ErrorKind::TimedOut));
}
#[test]
fn io_kind_extracts_kind_through_nested_source_chain() {
use std::io;
#[derive(Debug)]
struct Outer {
inner: io::Error,
}
impl std::fmt::Display for Outer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("outer wrapper")
}
}
impl std::error::Error for Outer {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.inner)
}
}
let outer = Outer {
inner: io::Error::new(io::ErrorKind::ConnectionRefused, "no listener"),
};
let wrapped = IoError::with_source("smtp connect failed", outer);
assert_eq!(wrapped.io_kind(), Some(io::ErrorKind::ConnectionRefused));
}
#[test]
fn io_kind_returns_none_when_no_io_error_in_chain() {
#[derive(Debug)]
struct NotAnIoError;
impl std::fmt::Display for NotAnIoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("custom")
}
}
impl std::error::Error for NotAnIoError {}
let wrapped = IoError::with_source("certificate parse failed", NotAnIoError);
assert_eq!(wrapped.io_kind(), None);
}
#[test]
fn io_kind_returns_none_when_no_source() {
let wrapped = IoError::new("plain message");
assert_eq!(wrapped.io_kind(), None);
}
#[test]
fn is_timeout_recognizes_timed_out() {
use std::io;
let inner = io::Error::new(io::ErrorKind::TimedOut, "deadline");
assert!(IoError::with_source("write failed", inner).is_timeout());
}
#[test]
fn is_timeout_rejects_other_kinds() {
use std::io;
let inner = io::Error::new(io::ErrorKind::ConnectionRefused, "no");
assert!(!IoError::with_source("write failed", inner).is_timeout());
}
#[test]
fn is_connection_refused_recognizes_kind() {
use std::io;
let inner = io::Error::new(io::ErrorKind::ConnectionRefused, "no");
assert!(IoError::with_source("connect failed", inner).is_connection_refused());
}
#[test]
fn is_connection_reset_recognizes_kind() {
use std::io;
let inner = io::Error::new(io::ErrorKind::ConnectionReset, "rst");
assert!(IoError::with_source("read failed", inner).is_connection_reset());
}
#[test]
fn is_connection_aborted_recognizes_kind() {
use std::io;
let inner = io::Error::new(io::ErrorKind::ConnectionAborted, "abort");
assert!(IoError::with_source("session failed", inner).is_connection_aborted());
}
#[test]
fn is_helpers_all_false_when_no_source() {
let wrapped = IoError::new("just a message");
assert!(!wrapped.is_timeout());
assert!(!wrapped.is_connection_refused());
assert!(!wrapped.is_connection_reset());
assert!(!wrapped.is_connection_aborted());
}