Skip to main content

respdiff/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::time::Duration;
4
5#[derive(Clone, Debug, PartialEq)]
6pub struct ResponseDiff {
7    pub status_changed: bool,
8    pub old_status: u16,
9    pub new_status: u16,
10    pub new_headers: Vec<(String, String)>,
11    pub missing_headers: Vec<String>,
12    pub changed_headers: Vec<(String, String, String)>,
13    pub body_size_delta: i64,
14    pub timing_delta_ms: i64,
15    pub body_similarity: f64,
16}
17
18impl ResponseDiff {
19    pub fn has_differences(&self) -> bool {
20        crate::diff::is_differential_match(self)
21    }
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub struct DiffPolicy {
26    pub timing_threshold_ms: i64,
27    pub similarity_threshold: f64,
28}
29
30impl Default for DiffPolicy {
31    fn default() -> Self {
32        Self {
33            timing_threshold_ms: 100,
34            similarity_threshold: 0.95,
35        }
36    }
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
40pub enum ObservationOutcome {
41    Match,
42    Error,
43    Silent,
44    Timeout,
45    Crash,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct ProbeObservation {
50    pub outcome: ObservationOutcome,
51    pub error: Option<String>,
52    pub categories: Vec<String>,
53    pub elapsed: Duration,
54    pub return_value: Option<String>,
55    pub confirmed: bool,
56}
57
58impl ProbeObservation {
59    pub fn silent(elapsed: Duration) -> Self {
60        Self {
61            outcome: ObservationOutcome::Silent,
62            error: None,
63            categories: Vec::new(),
64            elapsed,
65            return_value: None,
66            confirmed: false,
67        }
68    }
69
70    pub fn matched(
71        elapsed: Duration,
72        categories: impl IntoIterator<Item = impl Into<String>>,
73    ) -> Self {
74        let mut categories: Vec<String> = categories.into_iter().map(|item| item.into()).collect();
75        categories.sort();
76        categories.dedup();
77        Self {
78            outcome: ObservationOutcome::Match,
79            error: None,
80            categories,
81            elapsed,
82            return_value: None,
83            confirmed: true,
84        }
85    }
86
87    pub fn error(elapsed: Duration, error: impl Into<String>) -> Self {
88        Self {
89            outcome: ObservationOutcome::Error,
90            error: Some(error.into()),
91            categories: Vec::new(),
92            elapsed,
93            return_value: None,
94            confirmed: false,
95        }
96    }
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct ProbeVariant {
101    pub properties: BTreeMap<String, String>,
102    pub reason: String,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum PropertyRole {
107    Gate(Vec<String>),
108    Injectable,
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn baseline_diff() -> ResponseDiff {
116        ResponseDiff {
117            status_changed: false,
118            old_status: 200,
119            new_status: 200,
120            new_headers: Vec::new(),
121            missing_headers: Vec::new(),
122            changed_headers: Vec::new(),
123            body_size_delta: 0,
124            timing_delta_ms: 0,
125            body_similarity: 1.0,
126        }
127    }
128
129    #[test]
130    fn response_diff_has_no_differences_when_empty() {
131        assert!(!baseline_diff().has_differences());
132    }
133
134    #[test]
135    fn response_diff_detects_status_difference() {
136        let mut diff = baseline_diff();
137        diff.status_changed = true;
138        assert!(diff.has_differences());
139    }
140
141    #[test]
142    fn diff_policy_default_values_are_stable() {
143        let policy = DiffPolicy::default();
144        assert_eq!(policy.timing_threshold_ms, 100);
145        assert_eq!(policy.similarity_threshold, 0.95);
146    }
147
148    #[test]
149    fn silent_observation_sets_expected_fields() {
150        let observation = ProbeObservation::silent(Duration::from_millis(5));
151        assert_eq!(observation.outcome, ObservationOutcome::Silent);
152        assert_eq!(observation.error, None);
153        assert!(observation.categories.is_empty());
154        assert_eq!(observation.return_value, None);
155        assert!(!observation.confirmed);
156    }
157
158    #[test]
159    fn matched_observation_sorts_and_deduplicates_categories() {
160        let observation =
161            ProbeObservation::matched(Duration::from_millis(10), ["sql", "xss", "sql"]);
162        assert_eq!(observation.outcome, ObservationOutcome::Match);
163        assert_eq!(
164            observation.categories,
165            vec!["sql".to_string(), "xss".to_string()]
166        );
167        assert!(observation.confirmed);
168    }
169
170    #[test]
171    fn error_observation_sets_error_text() {
172        let observation = ProbeObservation::error(Duration::from_millis(2), "boom");
173        assert_eq!(observation.outcome, ObservationOutcome::Error);
174        assert_eq!(observation.error.as_deref(), Some("boom"));
175        assert!(!observation.confirmed);
176    }
177
178    #[test]
179    fn probe_variant_supports_value_comparison() {
180        let mut properties = BTreeMap::new();
181        properties.insert("role".to_string(), "admin".to_string());
182        let left = ProbeVariant {
183            properties: properties.clone(),
184            reason: "same".to_string(),
185        };
186        let right = ProbeVariant {
187            properties,
188            reason: "same".to_string(),
189        };
190        assert_eq!(left, right);
191    }
192
193    #[test]
194    fn property_role_gate_preserves_values() {
195        let role = PropertyRole::Gate(vec!["admin".to_string(), "user".to_string()]);
196        assert_eq!(
197            role,
198            PropertyRole::Gate(vec!["admin".to_string(), "user".to_string()])
199        );
200    }
201}