#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use super::*;
#[test]
fn io_variant_debug() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let err = Error::Io(Arc::new(io_err));
let dbg = format!("{err:?}");
assert!(dbg.contains("Io"));
assert!(dbg.contains("refused"));
}
#[test]
fn auth_variant_debug() {
let err = Error::Auth {
text: "bad credentials".into(),
code: None,
};
let dbg = format!("{err:?}");
assert!(dbg.contains("Auth"));
assert!(dbg.contains("bad credentials"));
}
#[test]
fn no_variant_debug() {
let err = Error::no_with_code("SELECT failed".into(), None);
let dbg = format!("{err:?}");
assert!(dbg.contains("No"));
assert!(dbg.contains("SELECT failed"));
}
#[test]
fn bad_variant_debug() {
let err = Error::bad_with_code("unknown command".into(), None);
let dbg = format!("{err:?}");
assert!(dbg.contains("Bad"));
assert!(dbg.contains("unknown command"));
}
#[test]
fn bye_variant_debug() {
let err = Error::Bye {
text: "server shutting down".into(),
code: None,
};
let dbg = format!("{err:?}");
assert!(dbg.contains("Bye"));
assert!(dbg.contains("server shutting down"));
}
#[test]
fn protocol_variant_debug() {
let err = Error::Protocol("unexpected tag".into());
let dbg = format!("{err:?}");
assert!(dbg.contains("Protocol"));
assert!(dbg.contains("unexpected tag"));
}
#[test]
fn parse_variant_debug() {
let err = Error::Parse("invalid envelope".into());
let dbg = format!("{err:?}");
assert!(dbg.contains("Parse"));
assert!(dbg.contains("invalid envelope"));
}
#[test]
fn timeout_variant_debug() {
let err = Error::Timeout;
let dbg = format!("{err:?}");
assert!(dbg.contains("Timeout"));
}
#[test]
fn closed_variant_debug() {
let err = Error::Closed;
let dbg = format!("{err:?}");
assert!(dbg.contains("Closed"));
}
#[test]
fn starttls_unavailable_variant_debug() {
let err = Error::StartTlsUnavailable;
let dbg = format!("{err:?}");
assert!(dbg.contains("StartTlsUnavailable"));
}
#[test]
fn missing_capability_variant_debug() {
let err = Error::MissingCapability("IDLE".into());
let dbg = format!("{err:?}");
assert!(dbg.contains("MissingCapability"));
assert!(dbg.contains("IDLE"));
}
#[test]
fn append_limit_variant_debug() {
let err = Error::AppendLimit {
size: 50_000_000,
limit: 25_000_000,
};
let dbg = format!("{err:?}");
assert!(dbg.contains("AppendLimit"));
assert!(dbg.contains("50000000"));
assert!(dbg.contains("25000000"));
}
#[test]
fn invalid_append_date_variant_debug() {
let err = Error::InvalidAppendDate("not a date".into());
let dbg = format!("{err:?}");
assert!(dbg.contains("InvalidAppendDate"));
assert!(dbg.contains("not a date"));
}
#[test]
fn display_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken");
let err = Error::Io(Arc::new(io_err));
let msg = err.to_string();
assert!(msg.contains("I/O error"));
assert!(msg.contains("pipe broken"));
}
#[test]
fn display_auth() {
let err = Error::Auth {
text: "LOGIN denied".into(),
code: None,
};
let msg = err.to_string();
assert_eq!(msg, "authentication failed: LOGIN denied");
}
#[test]
fn display_no() {
let err = Error::no_with_code("[NOPERM] SELECT not allowed".into(), None);
let msg = err.to_string();
assert_eq!(msg, "server rejected command: [NOPERM] SELECT not allowed");
}
#[test]
fn display_bad() {
let err = Error::bad_with_code("syntax error in FETCH".into(), None);
let msg = err.to_string();
assert_eq!(msg, "server reported bad command: syntax error in FETCH");
}
#[test]
fn display_bye() {
let err = Error::Bye {
text: "Too many connections".into(),
code: None,
};
let msg = err.to_string();
assert_eq!(msg, "server closing connection: Too many connections");
}
#[test]
fn display_protocol() {
let err = Error::Protocol("missing CRLF".into());
let msg = err.to_string();
assert_eq!(msg, "protocol error: missing CRLF");
}
#[test]
fn display_parse() {
let err = Error::Parse("unexpected NIL in address".into());
let msg = err.to_string();
assert_eq!(msg, "parse error: unexpected NIL in address");
}
#[test]
fn display_timeout() {
let err = Error::Timeout;
assert_eq!(err.to_string(), "operation timed out");
}
#[test]
fn display_closed() {
let err = Error::Closed;
assert_eq!(err.to_string(), "connection closed");
}
#[test]
fn display_starttls_unavailable() {
let err = Error::StartTlsUnavailable;
assert_eq!(err.to_string(), "STARTTLS not supported by server");
}
#[test]
fn display_missing_capability() {
let err = Error::MissingCapability("COMPRESS=DEFLATE".into());
assert_eq!(
err.to_string(),
"missing required capability: COMPRESS=DEFLATE"
);
}
#[test]
fn display_append_limit() {
let err = Error::AppendLimit {
size: 10_000,
limit: 5_000,
};
assert_eq!(
err.to_string(),
"message size 10000 exceeds server APPENDLIMIT of 5000"
);
}
#[test]
fn display_invalid_append_date() {
let err = Error::InvalidAppendDate("32-Jan-2025 00:00:00 +0000".into());
assert_eq!(
err.to_string(),
"invalid APPEND date-time: 32-Jan-2025 00:00:00 +0000"
);
}
#[test]
fn from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "connect timed out");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
assert!(err.to_string().contains("connect timed out"));
}
#[test]
fn from_io_error_preserves_kind() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let err: Error = io_err.into();
match &err {
Error::Io(inner) => {
assert_eq!(inner.kind(), std::io::ErrorKind::PermissionDenied);
}
_ => panic!("expected Io variant"),
}
}
#[test]
fn io_variant_has_source() {
use std::error::Error as StdError;
let io_err = std::io::Error::other("disk full");
let err = Error::Io(Arc::new(io_err));
assert!(err.source().is_some());
}
#[test]
fn non_io_variants_have_no_source() {
use std::error::Error as StdError;
let variants: Vec<Error> = vec![
Error::auth_with_code("fail".into(), None),
Error::no_with_code("no".into(), None),
Error::bad_with_code("bad".into(), None),
Error::Bye {
text: "bye".into(),
code: None,
},
Error::Protocol("proto".into()),
Error::Parse("parse".into()),
Error::Timeout,
Error::Closed,
Error::StartTlsUnavailable,
Error::MissingCapability("CAP".into()),
Error::AppendLimit { size: 1, limit: 0 },
Error::InvalidAppendDate("bad".into()),
Error::Internal("test".into()),
Error::DriverPanicked("test panic".into()),
Error::DriverGone,
];
for variant in &variants {
assert!(
variant.source().is_none(),
"expected no source for: {variant:?}"
);
}
}
#[test]
fn pattern_match_io() {
let err = Error::Io(Arc::new(std::io::Error::other("oops")));
match &err {
Error::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::Other),
_ => panic!("expected Io"),
}
}
#[test]
fn pattern_match_auth() {
let err = Error::Auth {
text: "AUTHENTICATIONFAILED".into(),
code: None,
};
match &err {
Error::Auth { text, .. } => assert_eq!(text, "AUTHENTICATIONFAILED"),
_ => panic!("expected Auth"),
}
}
#[test]
fn pattern_match_no_extracts_response_text() {
let err = Error::no_with_code("[NOPERM] Cannot delete INBOX".into(), None);
match &err {
Error::No { text, .. } => {
assert!(text.contains("NOPERM"));
assert!(text.contains("Cannot delete INBOX"));
}
_ => panic!("expected No"),
}
}
#[test]
fn pattern_match_bad_extracts_response_text() {
let err = Error::bad_with_code("Command unknown: XYZZY".into(), None);
match &err {
Error::Bad { text, .. } => assert!(text.contains("XYZZY")),
_ => panic!("expected Bad"),
}
}
#[test]
fn pattern_match_bye_extracts_response_text() {
let err = Error::Bye {
text: "Autologout; idle for too long".into(),
code: None,
};
match &err {
Error::Bye { text, .. } => assert!(text.contains("Autologout")),
_ => panic!("expected Bye"),
}
}
#[test]
fn pattern_match_protocol() {
let err = Error::Protocol("unexpected continuation".into());
match &err {
Error::Protocol(msg) => assert!(msg.contains("continuation")),
_ => panic!("expected Protocol"),
}
}
#[test]
fn pattern_match_parse() {
let err = Error::Parse("malformed literal count".into());
match &err {
Error::Parse(msg) => assert!(msg.contains("literal")),
_ => panic!("expected Parse"),
}
}
#[test]
fn pattern_match_unit_variants() {
assert!(matches!(Error::Timeout, Error::Timeout));
assert!(matches!(Error::Closed, Error::Closed));
assert!(matches!(
Error::StartTlsUnavailable,
Error::StartTlsUnavailable
));
}
#[test]
fn pattern_match_missing_capability() {
let err = Error::MissingCapability("MOVE".into());
match &err {
Error::MissingCapability(cap) => assert_eq!(cap, "MOVE"),
_ => panic!("expected MissingCapability"),
}
}
#[test]
fn pattern_match_append_limit() {
let err = Error::AppendLimit {
size: 100_000,
limit: 50_000,
};
match &err {
Error::AppendLimit { size, limit } => {
assert_eq!(*size, 100_000);
assert_eq!(*limit, 50_000);
}
_ => panic!("expected AppendLimit"),
}
}
#[test]
fn pattern_match_invalid_append_date() {
let err = Error::InvalidAppendDate("missing timezone".into());
match &err {
Error::InvalidAppendDate(msg) => assert!(msg.contains("missing timezone")),
_ => panic!("expected InvalidAppendDate"),
}
}
#[test]
fn display_messages_contain_context_strings() {
let cases: Vec<(Error, &str)> = vec![
(
Error::auth_with_code("PLAIN rejected".into(), None),
"PLAIN rejected",
),
(
Error::no_with_code("SELECT denied for INBOX".into(), None),
"SELECT denied for INBOX",
),
(
Error::bad_with_code("Unrecognized argument XATTR".into(), None),
"Unrecognized argument XATTR",
),
(
Error::Bye {
text: "BYE Logging out".into(),
code: None,
},
"BYE Logging out",
),
(
Error::Protocol("tag mismatch: expected A001 got A002".into()),
"tag mismatch: expected A001 got A002",
),
(
Error::Parse("incomplete BODYSTRUCTURE at offset 42".into()),
"incomplete BODYSTRUCTURE at offset 42",
),
(Error::MissingCapability("UIDPLUS".into()), "UIDPLUS"),
];
for (err, expected_substr) in &cases {
let msg = err.to_string();
assert!(
msg.contains(expected_substr),
"expected {msg:?} to contain {expected_substr:?}"
);
}
}
#[test]
fn append_limit_display_contains_both_values() {
let err = Error::AppendLimit {
size: 1_048_576,
limit: 524_288,
};
let msg = err.to_string();
assert!(msg.contains("1048576"), "should contain the message size");
assert!(msg.contains("524288"), "should contain the server limit");
}
#[test]
fn bye_with_alert_code_preserved() {
let err = Error::bye_with_code("Server shutting down".into(), Some(ResponseCode::Alert));
match &err {
Error::Bye { code, text } => {
assert_eq!(text, "Server shutting down");
assert_eq!(*code, Some(ResponseCode::Alert));
}
_ => panic!("expected Bye variant"),
}
}
#[test]
fn bye_without_code_preserved() {
let err = Error::bye_with_code("Logging out".into(), None);
match &err {
Error::Bye { code, text } => {
assert_eq!(text, "Logging out");
assert_eq!(*code, None);
}
_ => panic!("expected Bye variant"),
}
}
#[test]
fn bye_with_unavailable_code_preserved() {
let err = Error::bye_with_code("Try again later".into(), Some(ResponseCode::Unavailable));
match &err {
Error::Bye {
code: Some(ResponseCode::Unavailable),
..
} => {}
_ => panic!("expected Bye with Unavailable code"),
}
}
#[test]
fn no_with_response_code_construction() {
let err = Error::no_with_code("not allowed".into(), Some(ResponseCode::NoPerm));
match &err {
Error::No { text, code } => {
assert_eq!(text, "not allowed");
assert_eq!(*code, Some(ResponseCode::NoPerm));
}
_ => panic!("expected No"),
}
}
#[test]
fn bad_with_response_code_construction() {
let err = Error::bad_with_code("syntax error".into(), Some(ResponseCode::ClientBug));
match &err {
Error::Bad { text, code } => {
assert_eq!(text, "syntax error");
assert_eq!(*code, Some(ResponseCode::ClientBug));
}
_ => panic!("expected Bad"),
}
}
#[test]
fn auth_with_response_code_construction() {
let err = Error::auth_with_code("bad creds".into(), Some(ResponseCode::AuthenticationFailed));
match &err {
Error::Auth { text, code } => {
assert_eq!(text, "bad creds");
assert_eq!(*code, Some(ResponseCode::AuthenticationFailed));
}
_ => panic!("expected Auth"),
}
}
#[test]
fn no_with_code_display_shows_text_only() {
let err = Error::no_with_code("not allowed".into(), Some(ResponseCode::NoPerm));
assert_eq!(err.to_string(), "server rejected command: not allowed");
}
#[test]
fn bad_with_code_display_shows_text_only() {
let err = Error::bad_with_code("bad syntax".into(), Some(ResponseCode::ClientBug));
assert_eq!(err.to_string(), "server reported bad command: bad syntax");
}
#[test]
fn auth_with_code_display_shows_text_only() {
let err = Error::auth_with_code("denied".into(), Some(ResponseCode::AuthenticationFailed));
assert_eq!(err.to_string(), "authentication failed: denied");
}
#[test]
fn pattern_match_no_extracts_response_code() {
let err = Error::no_with_code("over quota".into(), Some(ResponseCode::OverQuota));
match &err {
Error::No {
code: Some(ResponseCode::OverQuota),
..
} => {}
_ => panic!("expected No with OverQuota code"),
}
}
#[test]
fn pattern_match_bad_extracts_response_code() {
let err = Error::bad_with_code("server bug".into(), Some(ResponseCode::ServerBug));
match &err {
Error::Bad {
code: Some(ResponseCode::ServerBug),
..
} => {}
_ => panic!("expected Bad with ServerBug code"),
}
}
#[test]
fn pattern_match_auth_extracts_response_code() {
let err = Error::auth_with_code("expired".into(), Some(ResponseCode::AuthenticationFailed));
match &err {
Error::Auth {
code: Some(ResponseCode::AuthenticationFailed),
..
} => {}
_ => panic!("expected Auth with AuthenticationFailed code"),
}
}
#[test]
fn equal_unit_variants() {
assert_eq!(Error::Timeout, Error::Timeout);
assert_eq!(Error::Closed, Error::Closed);
assert_eq!(Error::StartTlsUnavailable, Error::StartTlsUnavailable);
}
#[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()));
assert_eq!(
Error::MissingCapability("IDLE".into()),
Error::MissingCapability("IDLE".into())
);
assert_eq!(
Error::InvalidAppendDate("d".into()),
Error::InvalidAppendDate("d".into())
);
assert_eq!(Error::Internal("msg".into()), Error::Internal("msg".into()));
}
#[test]
fn equal_struct_variants() {
assert_eq!(
Error::Auth {
text: "fail".into(),
code: Some(ResponseCode::AuthenticationFailed),
},
Error::Auth {
text: "fail".into(),
code: Some(ResponseCode::AuthenticationFailed),
}
);
assert_eq!(
Error::No {
text: "no".into(),
code: None,
},
Error::No {
text: "no".into(),
code: None,
}
);
assert_eq!(
Error::AppendLimit {
size: 100,
limit: 50,
},
Error::AppendLimit {
size: 100,
limit: 50,
}
);
}
#[test]
fn io_equal_by_kind() {
let a = Error::Io(Arc::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"message A",
)));
let b = Error::Io(Arc::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"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(Arc::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"x",
)));
let b = Error::Io(Arc::new(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"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::No {
text: "a".into(),
code: None,
},
Error::No {
text: "b".into(),
code: None,
}
);
}
#[test]
fn all_variants_are_distinguishable() {
let errors: Vec<Error> = vec![
Error::Io(Arc::new(std::io::Error::other("test"))),
Error::auth_with_code("a".into(), None),
Error::no_with_code("n".into(), None),
Error::bad_with_code("b".into(), None),
Error::Bye {
text: "y".into(),
code: None,
},
Error::Protocol("p".into()),
Error::Parse("r".into()),
Error::Timeout,
Error::Closed,
Error::StartTlsUnavailable,
Error::MissingCapability("c".into()),
Error::AppendLimit { size: 1, limit: 0 },
Error::InvalidAppendDate("bad date".into()),
Error::Internal("internal err".into()),
Error::DriverPanicked("test panic".into()),
Error::DriverGone,
];
for err in &errors {
let label = match err {
Error::Io(_) => "io",
Error::Auth { .. } => "auth",
Error::No { .. } => "no",
Error::Bad { .. } => "bad",
Error::Bye { .. } => "bye",
Error::Protocol(_) => "protocol",
Error::Parse(_) => "parse",
Error::Timeout => "timeout",
Error::Closed => "closed",
Error::StartTlsUnavailable => "starttls",
Error::MissingCapability(_) => "capability",
Error::AppendLimit { .. } => "appendlimit",
Error::InvalidAppendDate(_) => "invalidappenddate",
Error::Internal(_) => "internal",
Error::DriverPanicked(_) => "driverpanicked",
Error::DriverGone => "drivergone",
};
assert!(!label.is_empty());
}
}
#[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(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(Arc::new(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"access denied",
)));
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::PermissionDenied);
}
_ => panic!("expected Io variant after deserialization"),
}
}
#[test]
fn serde_auth_round_trip() {
round_trip(&Error::Auth {
text: "bad credentials".into(),
code: None,
});
}
#[test]
fn serde_auth_with_code_round_trip() {
round_trip(&Error::Auth {
text: "expired".into(),
code: Some(ResponseCode::AuthenticationFailed),
});
}
#[test]
fn serde_no_round_trip() {
round_trip(&Error::No {
text: "SELECT denied".into(),
code: Some(ResponseCode::NoPerm),
});
}
#[test]
fn serde_bad_round_trip() {
round_trip(&Error::Bad {
text: "syntax error".into(),
code: None,
});
}
#[test]
fn serde_bye_round_trip() {
round_trip(&Error::Bye {
text: "server shutting down".into(),
code: Some(ResponseCode::Unavailable),
});
}
#[test]
fn serde_protocol_round_trip() {
round_trip(&Error::Protocol("unexpected tag".into()));
}
#[test]
fn serde_parse_round_trip() {
round_trip(&Error::Parse("malformed envelope".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_missing_capability_round_trip() {
round_trip(&Error::MissingCapability("IDLE".into()));
}
#[test]
fn serde_append_limit_round_trip() {
round_trip(&Error::AppendLimit {
size: 50_000_000,
limit: 25_000_000,
});
}
#[test]
fn serde_invalid_append_date_round_trip() {
round_trip(&Error::InvalidAppendDate("not a date".into()));
}
#[test]
fn serde_internal_round_trip() {
round_trip(&Error::Internal("driver stub".into()));
}
#[test]
fn serde_driver_panicked_round_trip() {
round_trip(&Error::DriverPanicked("driver panicked".into()));
}
#[test]
fn serde_driver_gone_round_trip() {
round_trip(&Error::DriverGone);
}
#[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"),
}
}
}