#![allow(clippy::unwrap_used)]
use std::panic::AssertUnwindSafe;
use crate::connection::NotifyFlags;
use crate::types::response::{EsearchResponse, UntaggedResponse as UR, UntaggedStatus};
use crate::types::validated::MailboxName;
use crate::types::CommandKind as CK;
use super::{classify, ClassificationContext, SolicitationRule};
fn all_rfc3501_command_kinds() -> Vec<CK> {
vec![
CK::Capability,
CK::Noop,
CK::Logout,
CK::Login,
CK::Authenticate,
CK::StartTls,
CK::Select,
CK::Examine,
CK::Create,
CK::Delete,
CK::Rename,
CK::Subscribe,
CK::Unsubscribe,
CK::List,
CK::Lsub,
CK::Status,
CK::Append,
CK::Check,
CK::Close,
CK::Expunge,
CK::Search,
CK::Fetch,
CK::Store,
CK::Copy,
]
}
fn all_extension_command_kinds() -> Vec<CK> {
vec![
CK::Idle, CK::Id, CK::Namespace, CK::GetMetadata, CK::SetMetadata, CK::Thread, CK::Sort, CK::NotifySet, CK::NotifyNone, CK::Compress, CK::GetQuota, CK::GetQuotaRoot, CK::SetQuota, CK::SetAcl, CK::DeleteAcl, CK::GetAcl, CK::ListRights, CK::MyRights, CK::ListStatus, CK::Move, CK::SearchReturn, CK::SearchSave, CK::Enable, CK::Unselect, CK::Unauthenticate, ]
}
fn all_command_kinds() -> Vec<CK> {
let mut all = all_rfc3501_command_kinds();
all.extend(all_extension_command_kinds());
all
}
fn all_rfc3501_untagged_variants() -> Vec<UR> {
let mailbox = MailboxName::new("INBOX").unwrap();
vec![
UR::Status {
status: UntaggedStatus::Ok,
code: None,
text: String::new(),
},
UR::Capability(vec![]),
UR::List(crate::types::mailbox::MailboxInfo::default()),
UR::Lsub(crate::types::mailbox::MailboxInfo::default()),
UR::MailboxStatus {
mailbox,
items: vec![],
},
UR::Search {
uids: vec![],
mod_seq: None,
},
UR::Flags(vec![]),
UR::Exists(1),
UR::Recent(0),
UR::Expunge(1),
UR::Fetch(Box::default()),
]
}
fn all_extension_untagged_variants() -> Vec<UR> {
let mailbox = MailboxName::new("INBOX").unwrap();
vec![
UR::Esearch(EsearchResponse::default()),
UR::Enabled(vec![]),
UR::Vanished {
earlier: true,
uids: vec![],
},
UR::Vanished {
earlier: false,
uids: vec![],
},
UR::Id(vec![]),
UR::Namespace {
personal: vec![],
other: vec![],
shared: vec![],
},
UR::Quota {
root: String::new(),
resources: vec![],
},
UR::QuotaRoot {
mailbox: mailbox.clone(),
roots: vec![],
},
UR::Acl {
mailbox: mailbox.clone(),
entries: vec![],
},
UR::MyRights {
mailbox: mailbox.clone(),
rights: String::new(),
},
UR::ListRights {
mailbox: mailbox.clone(),
identifier: String::new(),
required: String::new(),
optional: vec![],
},
UR::Metadata {
mailbox,
entries: vec![],
},
UR::Thread(vec![]),
UR::Sort {
nums: vec![],
mod_seq: None,
},
UR::Unknown(String::new()),
]
}
fn all_untagged_variants() -> Vec<UR> {
let mut all = all_rfc3501_untagged_variants();
all.extend(all_extension_untagged_variants());
all
}
#[test]
fn invariant_i5_classify_exhaustive_rfc3501() {
let target = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: Some(&target),
};
for cmd in all_rfc3501_command_kinds() {
for resp in all_rfc3501_untagged_variants() {
let cmd_copy = cmd;
let result =
std::panic::catch_unwind(AssertUnwindSafe(|| classify(cmd_copy, &resp, &ctx)));
assert!(
result.is_ok(),
"classify has no row for ({cmd:?}, {:?})",
std::mem::discriminant(&resp)
);
}
}
}
#[test]
fn invariant_i5_classify_exhaustive_extensions() {
let target = MailboxName::new("INBOX").unwrap();
let ctx_no_notify = ClassificationContext {
notify: NotifyFlags::default(),
command_target: Some(&target),
};
let ctx_with_notify = ClassificationContext {
notify: NotifyFlags {
list: true,
status: true,
metadata: true,
},
command_target: Some(&target),
};
for ctx in [&ctx_no_notify, &ctx_with_notify] {
for cmd in all_command_kinds() {
for resp in all_untagged_variants() {
let cmd_copy = cmd;
let result =
std::panic::catch_unwind(AssertUnwindSafe(|| classify(cmd_copy, &resp, ctx)));
assert!(
result.is_ok(),
"classify has no row for ({cmd:?}, {:?}) notify={:?}",
std::mem::discriminant(&resp),
ctx.notify
);
}
}
}
}
#[test]
fn notify_list_flag_classifies_unsolicited_list() {
let ctx = ClassificationContext {
notify: NotifyFlags {
list: true,
status: false,
metadata: false,
},
command_target: None,
};
let resp = UR::List(crate::types::mailbox::MailboxInfo::default());
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlyUnsolicited),
"LIST during NOOP with notify.list=true should be OnlyUnsolicited, got {result:?}"
);
}
#[test]
fn no_notify_list_flag_classifies_unsolicited_list_as_impossible() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::List(crate::types::mailbox::MailboxInfo::default());
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::Impossible),
"LIST during NOOP with notify.list=false should be Impossible, got {result:?}"
);
}
#[test]
fn notify_list_flag_does_not_affect_solicited_list() {
let ctx = ClassificationContext {
notify: NotifyFlags {
list: true,
status: false,
metadata: false,
},
command_target: None,
};
let resp = UR::List(crate::types::mailbox::MailboxInfo::default());
let result = classify(CK::List, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"LIST during LIST with notify.list=true should still be OnlySolicited, got {result:?}"
);
}
#[test]
fn notify_status_flag_classifies_unsolicited_status() {
let mailbox = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags {
list: false,
status: true,
metadata: false,
},
command_target: None,
};
let resp = UR::MailboxStatus {
mailbox,
items: vec![],
};
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlyUnsolicited),
"STATUS during NOOP with notify.status=true should be OnlyUnsolicited, got {result:?}"
);
}
#[test]
fn no_notify_status_flag_classifies_unsolicited_status_as_impossible() {
let mailbox = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::MailboxStatus {
mailbox,
items: vec![],
};
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::Impossible),
"STATUS during NOOP with notify.status=false should be Impossible, got {result:?}"
);
}
#[test]
fn notify_status_flag_does_not_affect_solicited_status() {
let mailbox = MailboxName::new("INBOX").unwrap();
let target = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags {
list: false,
status: true,
metadata: false,
},
command_target: Some(&target),
};
let resp = UR::MailboxStatus {
mailbox,
items: vec![],
};
let result = classify(CK::Status, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"STATUS during STATUS with notify.status=true should still be OnlySolicited, got {result:?}"
);
}
#[test]
fn notify_metadata_flag_classifies_unsolicited_metadata() {
let mailbox = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags {
list: false,
status: false,
metadata: true,
},
command_target: None,
};
let resp = UR::Metadata {
mailbox,
entries: vec![],
};
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlyUnsolicited),
"METADATA during NOOP with notify.metadata=true should be OnlyUnsolicited, got {result:?}"
);
}
#[test]
fn no_notify_metadata_flag_classifies_unsolicited_metadata_as_impossible() {
let mailbox = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Metadata {
mailbox,
entries: vec![],
};
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::Impossible),
"METADATA during NOOP with notify.metadata=false should be Impossible, got {result:?}"
);
}
#[test]
fn status_mismatched_mailbox_is_unsolicited() {
let mailbox = MailboxName::new("OtherFolder").unwrap();
let target = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: Some(&target),
};
let resp = UR::MailboxStatus {
mailbox,
items: vec![],
};
let result = classify(CK::Status, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlyUnsolicited),
"STATUS response for non-target mailbox should be OnlyUnsolicited, got {result:?}"
);
}
#[test]
fn notify_list_flag_does_not_affect_solicited_list_status() {
let ctx = ClassificationContext {
notify: NotifyFlags {
list: true,
status: false,
metadata: false,
},
command_target: None,
};
let resp = UR::List(crate::types::mailbox::MailboxInfo::default());
let result = classify(CK::ListStatus, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"LIST during LIST-STATUS with notify.list=true should still be OnlySolicited, got {result:?}"
);
}
#[test]
fn notify_metadata_flag_does_not_affect_solicited_metadata() {
let mailbox = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags {
list: false,
status: false,
metadata: true,
},
command_target: None,
};
let resp = UR::Metadata {
mailbox,
entries: vec![],
};
let result = classify(CK::GetMetadata, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"METADATA during GETMETADATA with notify.metadata=true should still be OnlySolicited, got {result:?}"
);
}
#[test]
fn search_return_routes_search_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Search {
uids: vec![],
mod_seq: None,
};
let result = classify(CK::SearchReturn, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"SEARCH during SearchReturn should be OnlySolicited, got {result:?}"
);
}
#[test]
fn search_save_routes_search_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Search {
uids: vec![],
mod_seq: None,
};
let result = classify(CK::SearchSave, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"SEARCH during SearchSave should be OnlySolicited, got {result:?}"
);
}
#[test]
fn search_return_routes_esearch_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Esearch(EsearchResponse::default());
let result = classify(CK::SearchReturn, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"ESEARCH during SearchReturn should be OnlySolicited, got {result:?}"
);
}
#[test]
fn search_save_routes_esearch_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Esearch(EsearchResponse::default());
let result = classify(CK::SearchSave, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"ESEARCH during SearchSave should be OnlySolicited, got {result:?}"
);
}
#[test]
fn search_routes_search_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Search {
uids: vec![],
mod_seq: None,
};
let result = classify(CK::Search, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"SEARCH during Search should be OnlySolicited, got {result:?}"
);
}
#[test]
fn search_routes_esearch_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Esearch(EsearchResponse::default());
let result = classify(CK::Search, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"ESEARCH during Search should be OnlySolicited, got {result:?}"
);
}
#[test]
fn pipeline_interleave_namespace_solicited_for_namespace() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Namespace {
personal: vec![],
other: vec![],
shared: vec![],
};
assert!(
matches!(
classify(CK::Namespace, &resp, &ctx),
SolicitationRule::OnlySolicited
),
"NAMESPACE during NAMESPACE must be OnlySolicited"
);
}
#[test]
fn pipeline_interleave_namespace_impossible_for_status() {
let target = MailboxName::new("INBOX").unwrap();
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: Some(&target),
};
let resp = UR::Namespace {
personal: vec![],
other: vec![],
shared: vec![],
};
assert!(
matches!(
classify(CK::Status, &resp, &ctx),
SolicitationRule::Impossible
),
"NAMESPACE during STATUS must be Impossible"
);
}
#[test]
fn pipeline_interleave_list_solicited_for_list() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::List(crate::types::mailbox::MailboxInfo::default());
assert!(
matches!(
classify(CK::List, &resp, &ctx),
SolicitationRule::OnlySolicited
),
"LIST during LIST must be OnlySolicited"
);
}
#[test]
fn pipeline_interleave_list_impossible_for_noop() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::List(crate::types::mailbox::MailboxInfo::default());
assert!(
matches!(
classify(CK::Noop, &resp, &ctx),
SolicitationRule::Impossible
),
"LIST during NOOP (no NOTIFY) must be Impossible"
);
}
#[test]
fn enable_routes_enabled_as_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Enabled(vec!["CONDSTORE".into()]);
let result = classify(CK::Enable, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"ENABLED during ENABLE should be OnlySolicited, got {result:?}"
);
}
#[test]
fn enabled_outside_enable_is_impossible() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Enabled(vec!["CONDSTORE".into()]);
let result = classify(CK::Noop, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::Impossible),
"ENABLED during NOOP should be Impossible, got {result:?}"
);
}
#[test]
fn unselect_expunge_is_either() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Expunge(1);
let result = classify(CK::Unselect, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::Either),
"EXPUNGE during UNSELECT should be Either, got {result:?}"
);
}
#[test]
fn close_expunge_is_solicited() {
let ctx = ClassificationContext {
notify: NotifyFlags::default(),
command_target: None,
};
let resp = UR::Expunge(1);
let result = classify(CK::Close, &resp, &ctx);
assert!(
matches!(result, SolicitationRule::OnlySolicited),
"EXPUNGE during CLOSE should be OnlySolicited, got {result:?}"
);
}