Add tests for Request; Fix some errors revealed in testing; Fix some
clippy lints;
This commit is contained in:
parent
f87299ad34
commit
c68b539af5
5 changed files with 134 additions and 50 deletions
28
src/host.rs
28
src/host.rs
|
@ -38,7 +38,7 @@ impl FromStr for Host {
|
||||||
type Err = ParseHostError;
|
type Err = ParseHostError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s.contains(|x: char| x.is_whitespace()) {
|
if s.contains(char::is_whitespace) {
|
||||||
return Err(ParseHostError::IllegalWhitespace);
|
return Err(ParseHostError::IllegalWhitespace);
|
||||||
}
|
}
|
||||||
if let Some((domain, tld)) = s.rsplit_once('.') {
|
if let Some((domain, tld)) = s.rsplit_once('.') {
|
||||||
|
@ -55,7 +55,7 @@ impl FromStr for Host {
|
||||||
Ok(Host {
|
Ok(Host {
|
||||||
subdomain: Some(subdomain.to_string()),
|
subdomain: Some(subdomain.to_string()),
|
||||||
domain: domain.to_string(),
|
domain: domain.to_string(),
|
||||||
tld: tld.to_string()
|
tld: tld.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -85,18 +85,12 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_tld() {
|
fn parse_empty_tld() {
|
||||||
assert_eq!(
|
assert_eq!("example.".parse::<Host>(), Err(ParseHostError::EmptyTld));
|
||||||
"example.".parse::<Host>(),
|
|
||||||
Err(ParseHostError::EmptyTld)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_empty_domain() {
|
fn parse_empty_domain() {
|
||||||
assert_eq!(
|
assert_eq!(".com".parse::<Host>(), Err(ParseHostError::EmptyDomain));
|
||||||
".com".parse::<Host>(),
|
|
||||||
Err(ParseHostError::EmptyDomain)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"example..com".parse::<Host>(),
|
"example..com".parse::<Host>(),
|
||||||
Err(ParseHostError::EmptyDomain)
|
Err(ParseHostError::EmptyDomain)
|
||||||
|
@ -127,19 +121,13 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_host_with_subdomain() {
|
fn parse_host_with_subdomain() {
|
||||||
let host: Host = "mail.example.com".parse().unwrap();
|
let host: Host = "mail.example.com".parse().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(host.subdomain, Some(String::from("mail")));
|
||||||
host.subdomain,
|
|
||||||
Some(String::from("mail"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_complex_subdomain() {
|
fn parse_complex_subdomain() {
|
||||||
let host: Host = "mail.sender.example.com".parse().unwrap();
|
let host: Host = "mail.sender.example.com".parse().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(host.subdomain, Some(String::from("mail.sender")));
|
||||||
host.subdomain,
|
|
||||||
Some(String::from("mail.sender"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -147,7 +135,7 @@ mod tests {
|
||||||
let host = Host {
|
let host = Host {
|
||||||
subdomain: None,
|
subdomain: None,
|
||||||
domain: String::from("example"),
|
domain: String::from("example"),
|
||||||
tld: String::from("com")
|
tld: String::from("com"),
|
||||||
};
|
};
|
||||||
let host_str = host.to_string();
|
let host_str = host.to_string();
|
||||||
assert_eq!(host_str.as_str(), "example.com");
|
assert_eq!(host_str.as_str(), "example.com");
|
||||||
|
@ -158,7 +146,7 @@ mod tests {
|
||||||
let host = Host {
|
let host = Host {
|
||||||
subdomain: Some(String::from("mail")),
|
subdomain: Some(String::from("mail")),
|
||||||
domain: String::from("example"),
|
domain: String::from("example"),
|
||||||
tld: String::from("com")
|
tld: String::from("com"),
|
||||||
};
|
};
|
||||||
let host_str = host.to_string();
|
let host_str = host.to_string();
|
||||||
assert_eq!(host_str.as_str(), "mail.example.com");
|
assert_eq!(host_str.as_str(), "mail.example.com");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
#![warn(clippy::all, clippy::pedantic)]
|
||||||
pub mod host;
|
pub mod host;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
|
|
115
src/request.rs
115
src/request.rs
|
@ -10,7 +10,11 @@ pub struct Request {
|
||||||
|
|
||||||
impl fmt::Display for Request {
|
impl fmt::Display for Request {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "misfin://{}@{} {}\r\n", self.user, self.host, self.message)
|
write!(
|
||||||
|
f,
|
||||||
|
"misfin://{}@{} {}\r\n",
|
||||||
|
self.user, self.host, self.message
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +32,7 @@ impl fmt::Display for ParseRequestError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::ParseHostError(e) => write!(f, "{e}"),
|
Self::ParseHostError(e) => write!(f, "{e}"),
|
||||||
_ => write!(f, "{self:?}")
|
_ => write!(f, "{self:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,23 +56,28 @@ impl FromStr for Request {
|
||||||
type Err = ParseRequestError;
|
type Err = ParseRequestError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if !s.ends_with("\r\n") {
|
|
||||||
return Err(ParseRequestError::Malformed);
|
|
||||||
}
|
|
||||||
if let Some((user, message)) = s.split_once(' ') {
|
if let Some((user, message)) = s.split_once(' ') {
|
||||||
let message = message.trim_end().to_string();
|
let Some(message) = message.strip_suffix("\r\n").map(ToString::to_string) else {
|
||||||
|
return Err(ParseRequestError::Malformed);
|
||||||
|
};
|
||||||
if message.is_empty() {
|
if message.is_empty() {
|
||||||
return Err(ParseRequestError::EmptyMessage);
|
return Err(ParseRequestError::EmptyMessage);
|
||||||
}
|
}
|
||||||
if let Some((user, host)) = user.rsplit_once('@') {
|
if let Some((user, host)) = user.rsplit_once('@') {
|
||||||
if host.is_empty() {
|
if host.is_empty() {
|
||||||
return Err(ParseRequestError::EmptyHost);
|
return Err(ParseRequestError::EmptyHost);
|
||||||
} else if user.is_empty() {
|
} else if user == "misfin://" {
|
||||||
return Err(ParseRequestError::EmptyUser);
|
return Err(ParseRequestError::EmptyUser);
|
||||||
}
|
}
|
||||||
let host = host.parse()?;
|
let host = host.parse()?;
|
||||||
let user = user.to_string();
|
let Some(user) = user.strip_prefix("misfin://").map(ToString::to_string) else {
|
||||||
Ok(Request { user, host, message })
|
return Err(ParseRequestError::Malformed);
|
||||||
|
};
|
||||||
|
Ok(Request {
|
||||||
|
user,
|
||||||
|
host,
|
||||||
|
message,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(ParseRequestError::MissingSeparator)
|
Err(ParseRequestError::MissingSeparator)
|
||||||
}
|
}
|
||||||
|
@ -77,3 +86,91 @@ impl FromStr for Request {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const REQ_STR: &'static str = "misfin://john@misfin.example.com Anyone seen Jane?\r\n";
|
||||||
|
|
||||||
|
fn req() -> Request {
|
||||||
|
Request {
|
||||||
|
user: "john".to_string(),
|
||||||
|
host: Host {
|
||||||
|
subdomain: Some("misfin".into()),
|
||||||
|
domain: "example".into(),
|
||||||
|
tld: "com".into(),
|
||||||
|
},
|
||||||
|
message: "Anyone seen Jane?".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_string() {
|
||||||
|
assert_eq!(req().to_string().as_str(), REQ_STR,);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
assert_eq!(REQ_STR.parse::<Request>().unwrap(), req());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_missing_sep() {
|
||||||
|
let req = "misfin://john@example.comHelloWorld!\r\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::MissingSeparator)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_malformed() {
|
||||||
|
let req = "misfin://john@example.com Hello World!\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::Malformed)
|
||||||
|
);
|
||||||
|
let req = "mail://john@example.com Hello World!\r\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::Malformed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty_user() {
|
||||||
|
let req = "misfin://@example.com Hello World!\r\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::EmptyUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty_host() {
|
||||||
|
let req = "misfin://john@ Hello World!\r\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::EmptyHost)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty_msg() {
|
||||||
|
let req = "misfin://john@example.com \r\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::EmptyMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_host_illegal_whitespace() {
|
||||||
|
let req = "misfin://john@example\tfairy.com Hello World!\r\n".parse::<Request>();
|
||||||
|
assert_eq!(
|
||||||
|
req,
|
||||||
|
Err(ParseRequestError::ParseHostError(ParseHostError::IllegalWhitespace))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{fmt, str::FromStr, num::ParseIntError};
|
use std::{fmt, num::ParseIntError, str::FromStr};
|
||||||
|
|
||||||
use crate::status::{Status, StatusError};
|
use crate::status::{Status, ParseStatusError};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
|
@ -48,8 +48,8 @@ impl From<ParseIntError> for ParseResponseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StatusError> for ParseResponseError {
|
impl From<ParseStatusError> for ParseResponseError {
|
||||||
fn from(_value: StatusError) -> Self {
|
fn from(_value: ParseStatusError) -> Self {
|
||||||
Self::StatusError
|
Self::StatusError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,9 +97,8 @@ mod tests {
|
||||||
fn parse_badint() {
|
fn parse_badint() {
|
||||||
let response = "twenty message deliverred\r\n".parse::<Response>();
|
let response = "twenty message deliverred\r\n".parse::<Response>();
|
||||||
match response {
|
match response {
|
||||||
Err(ParseResponseError::ParseInt(_)) => {},
|
Err(ParseResponseError::ParseInt(_)) => {}
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct StatusError;
|
pub struct ParseStatusError;
|
||||||
|
|
||||||
impl fmt::Display for StatusError {
|
impl fmt::Display for ParseStatusError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{self:?}")
|
write!(f, "{self:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for StatusError {}
|
impl std::error::Error for ParseStatusError {}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
|
@ -46,7 +46,7 @@ impl From<Status> for u8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Status {
|
impl TryFrom<u8> for Status {
|
||||||
type Error = StatusError;
|
type Error = ParseStatusError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value / 10 {
|
match value / 10 {
|
||||||
|
@ -56,7 +56,7 @@ impl TryFrom<u8> for Status {
|
||||||
4 => Ok(Self::TemporaryFailure((value % 40).try_into()?)),
|
4 => Ok(Self::TemporaryFailure((value % 40).try_into()?)),
|
||||||
5 => Ok(Self::PermanentFailure((value % 50).try_into()?)),
|
5 => Ok(Self::PermanentFailure((value % 50).try_into()?)),
|
||||||
6 => Ok(Self::AuthenticationFailure((value % 60).try_into()?)),
|
6 => Ok(Self::AuthenticationFailure((value % 60).try_into()?)),
|
||||||
_ => Err(StatusError),
|
_ => Err(ParseStatusError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,14 +73,14 @@ pub enum Redirect {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Redirect {
|
impl TryFrom<u8> for Redirect {
|
||||||
type Error = StatusError;
|
type Error = ParseStatusError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
0 => Ok(Self::Temporary),
|
0 => Ok(Self::Temporary),
|
||||||
1 => Ok(Self::Permanent),
|
1 => Ok(Self::Permanent),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(StatusError),
|
_ => Err(ParseStatusError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ pub enum TemporaryFailure {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for TemporaryFailure {
|
impl TryFrom<u8> for TemporaryFailure {
|
||||||
type Error = StatusError;
|
type Error = ParseStatusError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -116,7 +116,7 @@ impl TryFrom<u8> for TemporaryFailure {
|
||||||
4 => Ok(Self::RateLimit),
|
4 => Ok(Self::RateLimit),
|
||||||
5 => Ok(Self::MailboxFull),
|
5 => Ok(Self::MailboxFull),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(StatusError),
|
_ => Err(ParseStatusError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ pub enum PermanentFailure {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for PermanentFailure {
|
impl TryFrom<u8> for PermanentFailure {
|
||||||
type Error = StatusError;
|
type Error = ParseStatusError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -142,7 +142,7 @@ impl TryFrom<u8> for PermanentFailure {
|
||||||
3 => Ok(Self::DomainNotServiced),
|
3 => Ok(Self::DomainNotServiced),
|
||||||
9 => Ok(Self::BadRequest),
|
9 => Ok(Self::BadRequest),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(StatusError),
|
_ => Err(ParseStatusError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ pub enum AuthenticationFailure {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for AuthenticationFailure {
|
impl TryFrom<u8> for AuthenticationFailure {
|
||||||
type Error = StatusError;
|
type Error = ParseStatusError;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -168,7 +168,7 @@ impl TryFrom<u8> for AuthenticationFailure {
|
||||||
3 => Ok(Self::IdentityMismatch),
|
3 => Ok(Self::IdentityMismatch),
|
||||||
4 => Ok(Self::ProofRequired),
|
4 => Ok(Self::ProofRequired),
|
||||||
n if n < 10 => Ok(Self::Other),
|
n if n < 10 => Ok(Self::Other),
|
||||||
_ => Err(StatusError),
|
_ => Err(ParseStatusError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ mod tests {
|
||||||
Status::PermanentFailure(PermanentFailure::Other)
|
Status::PermanentFailure(PermanentFailure::Other)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_auth_err() {
|
fn parse_auth_err() {
|
||||||
assert!(AuthenticationFailure::try_from(42).is_err());
|
assert!(AuthenticationFailure::try_from(42).is_err());
|
||||||
|
|
Loading…
Add table
Reference in a new issue