use http::HeaderMap;
use parlov_core::{OracleResult, OracleVerdict, Severity, SignalKind, StrategyOutcome, Technique};
use crate::aggregation::modifiers::ModifierResult;
use crate::signals::header::rate_limit_diff;
#[must_use]
pub fn burst_result(
baseline_429: usize,
probe_429: usize,
technique: &Technique,
modifiers: ModifierResult,
) -> StrategyOutcome {
let evidence = format!("baseline 429 count: {baseline_429}, probe 429 count: {probe_429}");
if baseline_429 > 0 && probe_429 == 0 {
let result = OracleResult::from_technique(
OracleVerdict::Confirmed,
Some(Severity::Medium),
evidence,
SignalKind::StatusCodeDiff,
80,
technique,
);
StrategyOutcome::Positive(result)
} else if probe_429 > 0 && baseline_429 == 0 {
let result = OracleResult::from_technique(
OracleVerdict::NotPresent,
None,
evidence,
SignalKind::StatusCodeDiff,
0,
technique,
);
contradictory_with_modifiers(result, technique.inverted_signal_weight, modifiers)
} else {
let result = OracleResult::from_technique(
OracleVerdict::NotPresent,
None,
evidence,
SignalKind::StatusCodeDiff,
0,
technique,
);
StrategyOutcome::NoSignal(result)
}
}
#[must_use]
pub fn header_diff_result(
baseline: &HeaderMap,
probe: &HeaderMap,
technique: &Technique,
modifiers: ModifierResult,
) -> StrategyOutcome {
let baseline_only = rate_limit_diff(baseline, probe);
let probe_only = rate_limit_diff(probe, baseline);
if !baseline_only.is_empty() {
let evidence = format!(
"rate-limit headers in baseline only: {}",
baseline_only.join(", ")
);
let result = OracleResult::from_technique(
OracleVerdict::Confirmed,
Some(Severity::Low),
evidence,
SignalKind::HeaderPresence,
80,
technique,
);
StrategyOutcome::Positive(result)
} else if !probe_only.is_empty() {
let evidence = format!(
"rate-limit headers in probe only: {}",
probe_only.join(", ")
);
let result = OracleResult::from_technique(
OracleVerdict::NotPresent,
None,
evidence,
SignalKind::HeaderPresence,
0,
technique,
);
contradictory_with_modifiers(result, technique.inverted_signal_weight, modifiers)
} else {
let result = OracleResult::from_technique(
OracleVerdict::NotPresent,
None,
"no rate-limit header differential".to_owned(),
SignalKind::HeaderPresence,
0,
technique,
);
StrategyOutcome::NoSignal(result)
}
}
fn contradictory_with_modifiers(
result: OracleResult,
inverted_signal_weight: Option<f32>,
modifiers: ModifierResult,
) -> StrategyOutcome {
if modifiers.is_blocked() {
let reason = modifiers
.block_reason
.map_or("modifier blocked", |r| r.as_str());
return StrategyOutcome::Inapplicable(std::borrow::Cow::Borrowed(reason));
}
let base = inverted_signal_weight.unwrap_or(0.0);
#[allow(clippy::cast_possible_truncation)]
let effective = base * modifiers.modifiers.total() as f32;
StrategyOutcome::Contradictory(result, effective)
}
#[cfg(test)]
mod tests {
use super::*;
use http::{HeaderMap, HeaderName, HeaderValue};
use parlov_core::{
always_applicable, NormativeStrength, OracleClass, OracleVerdict, SignalKind,
SignalSurface, Vector,
};
use proptest::prelude::*;
fn test_technique() -> Technique {
Technique {
id: "rate-limit-burst",
name: "Rate Limit Burst",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::May,
normalization_weight: Some(0.2),
inverted_signal_weight: Some(0.2),
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
}
}
fn no_gating() -> ModifierResult {
ModifierResult {
modifiers: crate::aggregation::EvidenceModifiers::default(),
block_reason: None,
}
}
fn make_header_map(pairs: &[(&str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for &(name, value) in pairs {
map.insert(
HeaderName::from_bytes(name.as_bytes()).expect("valid header name"),
HeaderValue::from_str(value).expect("valid header value"),
);
}
map
}
fn inner_result(outcome: &StrategyOutcome) -> &OracleResult {
match outcome {
StrategyOutcome::Positive(r)
| StrategyOutcome::NoSignal(r)
| StrategyOutcome::Contradictory(r, _) => r,
StrategyOutcome::Inapplicable(_) => panic!("unexpected Inapplicable"),
}
}
#[test]
fn burst_baseline_429_probe_zero_is_positive() {
let t = test_technique();
let outcome = burst_result(1, 0, &t, no_gating());
let StrategyOutcome::Positive(r) = &outcome else {
panic!("expected Positive, got {outcome:?}");
};
assert_eq!(r.verdict, OracleVerdict::Confirmed);
assert_eq!(r.severity, Some(Severity::Medium));
assert_eq!(r.confidence, 80);
assert_eq!(r.signals[0].kind, SignalKind::StatusCodeDiff);
}
#[test]
fn burst_both_zero_is_no_signal() {
let t = test_technique();
let outcome = burst_result(0, 0, &t, no_gating());
let StrategyOutcome::NoSignal(r) = &outcome else {
panic!("expected NoSignal, got {outcome:?}");
};
assert_eq!(r.verdict, OracleVerdict::NotPresent);
assert_eq!(r.severity, None);
assert_eq!(r.confidence, 0);
}
#[test]
fn burst_both_nonzero_is_no_signal() {
let t = test_technique();
let outcome = burst_result(2, 3, &t, no_gating());
assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
}
#[test]
fn burst_probe_429_baseline_zero_is_contradictory() {
let t = test_technique();
let outcome = burst_result(0, 1, &t, no_gating());
let StrategyOutcome::Contradictory(r, w) = &outcome else {
panic!("expected Contradictory, got {outcome:?}");
};
assert_eq!(r.verdict, OracleVerdict::NotPresent);
assert_eq!(r.severity, None);
assert!(
(w - t.inverted_signal_weight.unwrap_or(0.0)).abs() < f32::EPSILON,
"weight must equal technique inverted_signal_weight"
);
}
#[test]
fn burst_multiple_baseline_429_is_positive() {
let t = test_technique();
let outcome = burst_result(3, 0, &t, no_gating());
let StrategyOutcome::Positive(r) = &outcome else {
panic!("expected Positive");
};
assert_eq!(r.severity, Some(Severity::Medium));
}
#[test]
fn burst_evidence_contains_both_counts() {
let t = test_technique();
let outcome = burst_result(2, 0, &t, no_gating());
let evidence = &inner_result(&outcome).signals[0].evidence;
assert!(
evidence.contains('2'),
"evidence must contain baseline count"
);
assert!(evidence.contains('0'), "evidence must contain probe count");
}
#[test]
fn burst_contradictory_evidence_contains_both_counts() {
let t = test_technique();
let outcome = burst_result(0, 5, &t, no_gating());
let evidence = &inner_result(&outcome).signals[0].evidence;
assert!(
evidence.contains('0'),
"evidence must contain baseline count"
);
assert!(evidence.contains('5'), "evidence must contain probe count");
}
#[test]
fn header_diff_baseline_only_is_positive() {
let t = test_technique();
let baseline = make_header_map(&[("ratelimit-remaining", "99")]);
let probe = HeaderMap::new();
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
let StrategyOutcome::Positive(r) = &outcome else {
panic!("expected Positive, got {outcome:?}");
};
assert_eq!(r.verdict, OracleVerdict::Confirmed);
assert_eq!(r.severity, Some(Severity::Low));
assert_eq!(r.confidence, 80);
assert_eq!(r.signals[0].kind, SignalKind::HeaderPresence);
}
#[test]
fn header_diff_no_headers_is_no_signal() {
let t = test_technique();
let outcome = header_diff_result(&HeaderMap::new(), &HeaderMap::new(), &t, no_gating());
let StrategyOutcome::NoSignal(r) = &outcome else {
panic!("expected NoSignal, got {outcome:?}");
};
assert_eq!(r.verdict, OracleVerdict::NotPresent);
assert_eq!(r.severity, None);
assert_eq!(r.confidence, 0);
}
#[test]
fn header_diff_headers_in_both_is_no_signal() {
let t = test_technique();
let baseline = make_header_map(&[("ratelimit-remaining", "99")]);
let probe = make_header_map(&[("ratelimit-remaining", "99")]);
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
}
#[test]
fn header_diff_probe_only_is_contradictory() {
let t = test_technique();
let baseline = HeaderMap::new();
let probe = make_header_map(&[("ratelimit-remaining", "99")]);
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
let StrategyOutcome::Contradictory(r, w) = &outcome else {
panic!("expected Contradictory, got {outcome:?}");
};
assert_eq!(r.verdict, OracleVerdict::NotPresent);
assert!(
(w - t.inverted_signal_weight.unwrap_or(0.0)).abs() < f32::EPSILON,
"weight must equal technique inverted_signal_weight"
);
}
#[test]
fn header_diff_positive_evidence_contains_header_name() {
let t = test_technique();
let baseline = make_header_map(&[("ratelimit-remaining", "99")]);
let probe = HeaderMap::new();
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
assert!(inner_result(&outcome).signals[0]
.evidence
.contains("ratelimit-remaining"));
}
#[test]
fn header_diff_contradictory_evidence_contains_header_name() {
let t = test_technique();
let baseline = HeaderMap::new();
let probe = make_header_map(&[("ratelimit-remaining", "5")]);
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
assert!(inner_result(&outcome).signals[0]
.evidence
.contains("ratelimit-remaining"));
}
#[test]
fn header_diff_no_signal_evidence_is_canonical_string() {
let t = test_technique();
let outcome = header_diff_result(&HeaderMap::new(), &HeaderMap::new(), &t, no_gating());
assert_eq!(
inner_result(&outcome).signals[0].evidence,
"no rate-limit header differential"
);
}
proptest! {
#[test]
fn burst_positive_when_baseline_nonzero_probe_zero(
baseline in 1usize..=100,
) {
let t = test_technique();
let outcome = burst_result(baseline, 0, &t, no_gating());
prop_assert!(matches!(outcome, StrategyOutcome::Positive(_)));
}
#[test]
fn burst_contradictory_when_probe_nonzero_baseline_zero(
probe in 1usize..=100,
) {
let t = test_technique();
let outcome = burst_result(0, probe, &t, no_gating());
let StrategyOutcome::Contradictory(_, w) = outcome else {
return Err(proptest::test_runner::TestCaseError::fail(
"expected Contradictory"
));
};
let expected = t.inverted_signal_weight.unwrap_or(0.0);
prop_assert!((w - expected).abs() < f32::EPSILON);
}
#[test]
fn burst_no_signal_when_both_zero_or_both_nonzero(
baseline in 0usize..=50,
probe in 0usize..=50,
) {
prop_assume!(!(baseline > 0 && probe == 0));
prop_assume!(!(baseline == 0 && probe > 0));
let t = test_technique();
let outcome = burst_result(baseline, probe, &t, no_gating());
prop_assert!(matches!(outcome, StrategyOutcome::NoSignal(_)));
}
}
proptest! {
#[test]
fn header_diff_never_inapplicable(b_has_header: bool, p_has_header: bool) {
let t = test_technique();
let baseline = if b_has_header {
make_header_map(&[("ratelimit-remaining", "10")])
} else {
HeaderMap::new()
};
let probe = if p_has_header {
make_header_map(&[("ratelimit-remaining", "10")])
} else {
HeaderMap::new()
};
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
prop_assert!(!matches!(outcome, StrategyOutcome::Inapplicable(_)));
}
#[test]
fn header_diff_contradictory_weight_matches_technique(
inverted_signal_weight in 0.1f32..=1.0f32,
) {
let t = Technique {
id: "rate-limit-burst",
name: "Rate Limit Burst",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::May,
normalization_weight: Some(inverted_signal_weight),
inverted_signal_weight: Some(inverted_signal_weight),
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
};
let baseline = HeaderMap::new();
let probe = make_header_map(&[("ratelimit-remaining", "5")]);
let outcome = header_diff_result(&baseline, &probe, &t, no_gating());
let StrategyOutcome::Contradictory(_, w) = outcome else {
return Err(proptest::test_runner::TestCaseError::fail(
"expected Contradictory when probe has rate-limit headers and baseline does not",
));
};
let expected = t.inverted_signal_weight.unwrap_or(0.0);
prop_assert!(
(w - expected).abs() < f32::EPSILON,
"weight {w} must equal technique inverted_signal_weight {expected}"
);
}
}
}