use serde::{Deserialize, Serialize};
use crate::OracleResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StrategyOutcome {
Positive(OracleResult),
NoSignal(OracleResult),
Contradictory(OracleResult, f32),
Inapplicable(std::borrow::Cow<'static, str>),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
NormativeStrength, OracleClass, OracleVerdict, Severity, Signal, SignalKind, Vector,
};
fn positive_result() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::Confirmed,
severity: Some(Severity::High),
confidence: 85,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: SignalKind::StatusCodeDiff,
evidence: "403 (baseline) vs 404 (probe)".into(),
rfc_basis: Some("RFC 9110 §15.5.4".into()),
}],
technique_id: Some("get-200-404".into()),
vector: Some(Vector::StatusCodeDiff),
normative_strength: Some(NormativeStrength::Must),
label: Some("Authorization-based differential".into()),
leaks: Some("Resource existence confirmed to low-privilege callers".into()),
rfc_basis: Some("RFC 9110 §15.5.4".into()),
}
}
fn no_signal_result() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: None,
confidence: 0,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: SignalKind::StatusCodeDiff,
evidence: "404 (baseline) vs 404 (probe)".into(),
rfc_basis: None,
}],
technique_id: Some("get-200-404".into()),
vector: Some(Vector::StatusCodeDiff),
normative_strength: Some(NormativeStrength::Must),
label: None,
leaks: None,
rfc_basis: None,
}
}
fn contradictory_result() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: None,
confidence: 0,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: SignalKind::StatusCodeDiff,
evidence: "200 (baseline) vs 200 (probe) — server normalises".into(),
rfc_basis: None,
}],
technique_id: Some("if-none-match".into()),
vector: Some(Vector::CacheProbing),
normative_strength: Some(NormativeStrength::Should),
label: None,
leaks: None,
rfc_basis: None,
}
}
#[test]
fn roundtrip_positive_inner_oracle_result_survives() {
let outcome = StrategyOutcome::Positive(positive_result());
let json = serde_json::to_string(&outcome).expect("serialization failed");
let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
let StrategyOutcome::Positive(result) = back else {
panic!("expected Positive variant after roundtrip");
};
assert_eq!(result.verdict, OracleVerdict::Confirmed);
assert_eq!(result.confidence, 85);
assert_eq!(result.technique_id.as_deref(), Some("get-200-404"));
assert_eq!(result.signals.len(), 1);
assert_eq!(result.signals[0].kind, SignalKind::StatusCodeDiff);
assert_eq!(result.signals[0].evidence, "403 (baseline) vs 404 (probe)");
assert_eq!(
result.label.as_deref(),
Some("Authorization-based differential")
);
}
#[test]
fn roundtrip_no_signal_inner_oracle_result_survives() {
let outcome = StrategyOutcome::NoSignal(no_signal_result());
let json = serde_json::to_string(&outcome).expect("serialization failed");
let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
let StrategyOutcome::NoSignal(result) = back else {
panic!("expected NoSignal variant after roundtrip");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert_eq!(result.confidence, 0);
assert!(result.label.is_none());
assert!(result.leaks.is_none());
}
#[test]
fn roundtrip_contradictory_weight_survives() {
let weight: f32 = 0.75;
let outcome = StrategyOutcome::Contradictory(contradictory_result(), weight);
let json = serde_json::to_string(&outcome).expect("serialization failed");
let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
let StrategyOutcome::Contradictory(result, back_weight) = back else {
panic!("expected Contradictory variant after roundtrip");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert!((back_weight - weight).abs() < f32::EPSILON);
assert_eq!(result.technique_id.as_deref(), Some("if-none-match"));
}
#[test]
fn roundtrip_inapplicable_reason_string_survives() {
let reason = "target does not support ETags".to_string();
let outcome = StrategyOutcome::Inapplicable(std::borrow::Cow::Owned(reason.clone()));
let json = serde_json::to_string(&outcome).expect("serialization failed");
let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
let StrategyOutcome::Inapplicable(back_reason) = back else {
panic!("expected Inapplicable variant after roundtrip");
};
assert_eq!(back_reason, reason);
}
#[test]
fn clone_positive() {
let outcome = StrategyOutcome::Positive(positive_result());
let cloned = outcome.clone();
let StrategyOutcome::Positive(result) = cloned else {
panic!("expected Positive after clone");
};
assert_eq!(result.confidence, 85);
}
#[test]
fn clone_no_signal() {
let outcome = StrategyOutcome::NoSignal(no_signal_result());
let cloned = outcome.clone();
let StrategyOutcome::NoSignal(result) = cloned else {
panic!("expected NoSignal after clone");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
}
#[test]
fn clone_contradictory() {
let outcome = StrategyOutcome::Contradictory(contradictory_result(), 0.5);
let cloned = outcome.clone();
let StrategyOutcome::Contradictory(result, w) = cloned else {
panic!("expected Contradictory after clone");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert!((w - 0.5_f32).abs() < f32::EPSILON);
}
#[test]
fn clone_inapplicable() {
let outcome = StrategyOutcome::Inapplicable(std::borrow::Cow::Borrowed("no ETag support"));
let cloned = outcome.clone();
let StrategyOutcome::Inapplicable(reason) = cloned else {
panic!("expected Inapplicable after clone");
};
assert_eq!(reason, "no ETag support");
}
}