use crate::classify::Reason;
use crate::egress::ExitStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Step {
Slim,
Headed,
}
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlimAction {
Escalate,
Rotate(ExitStatus),
Fail(ExitStatus),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeadedAction {
Rotate(ExitStatus),
Fail(ExitStatus),
}
#[cfg(test)]
impl From<HeadedAction> for SlimAction {
fn from(action: HeadedAction) -> SlimAction {
match action {
HeadedAction::Rotate(status) => SlimAction::Rotate(status),
HeadedAction::Fail(status) => SlimAction::Fail(status),
}
}
}
#[cfg(test)]
pub fn decide_slim(reason: Reason, attempts_left: u32) -> SlimAction {
match reason {
Reason::Challenged => SlimAction::Escalate,
_ => decide_headed(reason, attempts_left).into(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChallengeAction {
GiveUp,
RetrySlim,
Escalate,
GiveUpMisconfigured,
}
pub fn decide_challenge(
escalate_allowed: bool,
attempts_left: u32,
domain_misconfigured: bool,
) -> ChallengeAction {
if domain_misconfigured {
ChallengeAction::GiveUpMisconfigured
} else if attempts_left > 0 {
ChallengeAction::RetrySlim
} else if escalate_allowed {
ChallengeAction::Escalate
} else {
ChallengeAction::GiveUp
}
}
pub fn decide_headed(reason: Reason, attempts_left: u32) -> HeadedAction {
let status = match reason {
Reason::Unreachable => ExitStatus::Dead,
_ => ExitStatus::Cooled,
};
if attempts_left > 0 {
HeadedAction::Rotate(status)
} else {
HeadedAction::Fail(status)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slim_challenge_escalates_headed_challenge_rotates() {
assert_eq!(decide_slim(Reason::Challenged, 3), SlimAction::Escalate);
assert_eq!(
decide_headed(Reason::Challenged, 3),
HeadedAction::Rotate(ExitStatus::Cooled)
);
}
#[test]
fn unreachable_marks_exit_dead() {
assert_eq!(
decide_slim(Reason::Unreachable, 3),
SlimAction::Rotate(ExitStatus::Dead)
);
assert_eq!(
decide_headed(Reason::Unreachable, 3),
HeadedAction::Rotate(ExitStatus::Dead)
);
}
#[test]
fn cooling_reasons_rotate_while_attempts_remain() {
for r in [
Reason::RateLimited,
Reason::Blocked,
Reason::Timeout,
Reason::Unavailable,
] {
assert_eq!(
decide_slim(r, 3),
SlimAction::Rotate(ExitStatus::Cooled),
"{r:?}"
);
assert_eq!(
decide_headed(r, 3),
HeadedAction::Rotate(ExitStatus::Cooled),
"{r:?}"
);
}
}
#[test]
fn challenge_retries_slim_while_budget_lasts() {
assert_eq!(decide_challenge(true, 3, false), ChallengeAction::RetrySlim);
assert_eq!(
decide_challenge(false, 3, false),
ChallengeAction::RetrySlim
);
}
#[test]
fn budget_exhausted_escalates_when_allowed_else_gives_up() {
assert_eq!(decide_challenge(true, 0, false), ChallengeAction::Escalate);
assert_eq!(decide_challenge(false, 0, false), ChallengeAction::GiveUp);
}
#[test]
fn misconfigured_domain_gives_up_immediately_regardless_of_budget_or_fingerprint() {
assert_eq!(
decide_challenge(true, 3, true),
ChallengeAction::GiveUpMisconfigured
);
assert_eq!(
decide_challenge(false, 0, true),
ChallengeAction::GiveUpMisconfigured
);
}
#[test]
fn no_attempts_left_fails_but_keeps_the_status() {
assert_eq!(
decide_slim(Reason::Blocked, 0),
SlimAction::Fail(ExitStatus::Cooled)
);
assert_eq!(
decide_slim(Reason::Unreachable, 0),
SlimAction::Fail(ExitStatus::Dead)
);
assert_eq!(
decide_headed(Reason::Blocked, 0),
HeadedAction::Fail(ExitStatus::Cooled)
);
}
}