1use serde::{Deserialize, Serialize};
4
5use crate::OracleResult;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum StrategyOutcome {
13 Positive(OracleResult),
18 NoSignal(OracleResult),
23 Contradictory(OracleResult, f32),
29 Inapplicable(std::borrow::Cow<'static, str>),
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::{
40 NormativeStrength, OracleClass, OracleVerdict, Severity, Signal, SignalKind, Vector,
41 };
42
43 fn positive_result() -> OracleResult {
44 OracleResult {
45 class: OracleClass::Existence,
46 verdict: OracleVerdict::Confirmed,
47 severity: Some(Severity::High),
48 confidence: 85,
49 impact_class: None,
50 reasons: vec![],
51 signals: vec![Signal {
52 kind: SignalKind::StatusCodeDiff,
53 evidence: "403 (baseline) vs 404 (probe)".into(),
54 rfc_basis: Some("RFC 9110 §15.5.4".into()),
55 }],
56 technique_id: Some("get-200-404".into()),
57 vector: Some(Vector::StatusCodeDiff),
58 normative_strength: Some(NormativeStrength::Must),
59 label: Some("Authorization-based differential".into()),
60 leaks: Some("Resource existence confirmed to low-privilege callers".into()),
61 rfc_basis: Some("RFC 9110 §15.5.4".into()),
62 }
63 }
64
65 fn no_signal_result() -> OracleResult {
66 OracleResult {
67 class: OracleClass::Existence,
68 verdict: OracleVerdict::NotPresent,
69 severity: None,
70 confidence: 0,
71 impact_class: None,
72 reasons: vec![],
73 signals: vec![Signal {
74 kind: SignalKind::StatusCodeDiff,
75 evidence: "404 (baseline) vs 404 (probe)".into(),
76 rfc_basis: None,
77 }],
78 technique_id: Some("get-200-404".into()),
79 vector: Some(Vector::StatusCodeDiff),
80 normative_strength: Some(NormativeStrength::Must),
81 label: None,
82 leaks: None,
83 rfc_basis: None,
84 }
85 }
86
87 fn contradictory_result() -> OracleResult {
88 OracleResult {
89 class: OracleClass::Existence,
90 verdict: OracleVerdict::NotPresent,
91 severity: None,
92 confidence: 0,
93 impact_class: None,
94 reasons: vec![],
95 signals: vec![Signal {
96 kind: SignalKind::StatusCodeDiff,
97 evidence: "200 (baseline) vs 200 (probe) — server normalises".into(),
98 rfc_basis: None,
99 }],
100 technique_id: Some("if-none-match".into()),
101 vector: Some(Vector::CacheProbing),
102 normative_strength: Some(NormativeStrength::Should),
103 label: None,
104 leaks: None,
105 rfc_basis: None,
106 }
107 }
108
109 #[test]
110 fn roundtrip_positive_inner_oracle_result_survives() {
111 let outcome = StrategyOutcome::Positive(positive_result());
112 let json = serde_json::to_string(&outcome).expect("serialization failed");
113 let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
114 let StrategyOutcome::Positive(result) = back else {
115 panic!("expected Positive variant after roundtrip");
116 };
117 assert_eq!(result.verdict, OracleVerdict::Confirmed);
118 assert_eq!(result.confidence, 85);
119 assert_eq!(result.technique_id.as_deref(), Some("get-200-404"));
120 assert_eq!(result.signals.len(), 1);
121 assert_eq!(result.signals[0].kind, SignalKind::StatusCodeDiff);
122 assert_eq!(result.signals[0].evidence, "403 (baseline) vs 404 (probe)");
123 assert_eq!(
124 result.label.as_deref(),
125 Some("Authorization-based differential")
126 );
127 }
128
129 #[test]
130 fn roundtrip_no_signal_inner_oracle_result_survives() {
131 let outcome = StrategyOutcome::NoSignal(no_signal_result());
132 let json = serde_json::to_string(&outcome).expect("serialization failed");
133 let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
134 let StrategyOutcome::NoSignal(result) = back else {
135 panic!("expected NoSignal variant after roundtrip");
136 };
137 assert_eq!(result.verdict, OracleVerdict::NotPresent);
138 assert_eq!(result.confidence, 0);
139 assert!(result.label.is_none());
140 assert!(result.leaks.is_none());
141 }
142
143 #[test]
144 fn roundtrip_contradictory_weight_survives() {
145 let weight: f32 = 0.75;
146 let outcome = StrategyOutcome::Contradictory(contradictory_result(), weight);
147 let json = serde_json::to_string(&outcome).expect("serialization failed");
148 let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
149 let StrategyOutcome::Contradictory(result, back_weight) = back else {
150 panic!("expected Contradictory variant after roundtrip");
151 };
152 assert_eq!(result.verdict, OracleVerdict::NotPresent);
153 assert!((back_weight - weight).abs() < f32::EPSILON);
154 assert_eq!(result.technique_id.as_deref(), Some("if-none-match"));
155 }
156
157 #[test]
158 fn roundtrip_inapplicable_reason_string_survives() {
159 let reason = "target does not support ETags".to_string();
160 let outcome = StrategyOutcome::Inapplicable(std::borrow::Cow::Owned(reason.clone()));
161 let json = serde_json::to_string(&outcome).expect("serialization failed");
162 let back: StrategyOutcome = serde_json::from_str(&json).expect("deserialization failed");
163 let StrategyOutcome::Inapplicable(back_reason) = back else {
164 panic!("expected Inapplicable variant after roundtrip");
165 };
166 assert_eq!(back_reason, reason);
167 }
168
169 #[test]
170 fn clone_positive() {
171 let outcome = StrategyOutcome::Positive(positive_result());
172 let cloned = outcome.clone();
173 let StrategyOutcome::Positive(result) = cloned else {
174 panic!("expected Positive after clone");
175 };
176 assert_eq!(result.confidence, 85);
177 }
178
179 #[test]
180 fn clone_no_signal() {
181 let outcome = StrategyOutcome::NoSignal(no_signal_result());
182 let cloned = outcome.clone();
183 let StrategyOutcome::NoSignal(result) = cloned else {
184 panic!("expected NoSignal after clone");
185 };
186 assert_eq!(result.verdict, OracleVerdict::NotPresent);
187 }
188
189 #[test]
190 fn clone_contradictory() {
191 let outcome = StrategyOutcome::Contradictory(contradictory_result(), 0.5);
192 let cloned = outcome.clone();
193 let StrategyOutcome::Contradictory(result, w) = cloned else {
194 panic!("expected Contradictory after clone");
195 };
196 assert_eq!(result.verdict, OracleVerdict::NotPresent);
197 assert!((w - 0.5_f32).abs() < f32::EPSILON);
198 }
199
200 #[test]
201 fn clone_inapplicable() {
202 let outcome = StrategyOutcome::Inapplicable(std::borrow::Cow::Borrowed("no ETag support"));
203 let cloned = outcome.clone();
204 let StrategyOutcome::Inapplicable(reason) = cloned else {
205 panic!("expected Inapplicable after clone");
206 };
207 assert_eq!(reason, "no ETag support");
208 }
209}