Skip to main content

parlov_elicit/
context.rs

1//! Scan context: the operator-supplied parameters that drive strategy selection.
2
3use http::HeaderMap;
4
5use crate::types::RiskLevel;
6
7/// A duplicate field value that already exists in the target system.
8///
9/// Supplied when the operator knows a specific field value is taken, enabling
10/// uniqueness-conflict elicitation strategies.
11#[derive(Debug, Clone)]
12pub struct KnownDuplicate {
13    /// JSON field name, e.g. `"email"`.
14    pub field: String,
15    /// The value already in use, e.g. `"alice@example.com"`.
16    pub value: String,
17}
18
19/// A field value that puts the target resource into an invalid or rejected state.
20///
21/// Supplied to enable state-transition elicitation strategies.
22#[derive(Debug, Clone)]
23pub struct StateField {
24    /// JSON field name, e.g. `"status"`.
25    pub field: String,
26    /// The invalid value, e.g. `"invalid_state"`.
27    pub value: String,
28}
29
30/// All operator-supplied parameters that govern a single elicitation scan.
31///
32/// Strategies inspect `ScanContext` to decide applicability and to construct
33/// `ProbeSpec` values. No I/O occurs here — this is pure configuration.
34#[derive(Debug, Clone)]
35pub struct ScanContext {
36    /// Target URL template; may contain `{id}` placeholder.
37    pub target: String,
38    /// Known-existing resource identifier.
39    pub baseline_id: String,
40    /// Known-nonexistent resource identifier.
41    pub probe_id: String,
42    /// Common to all probes — typically includes `Authorization`.
43    pub headers: HeaderMap,
44    /// Strategies above this risk level are skipped.
45    pub max_risk: RiskLevel,
46    /// Known duplicate field value for uniqueness-conflict strategies. `None` if
47    /// not applicable.
48    pub known_duplicate: Option<KnownDuplicate>,
49    /// Field value that triggers invalid-state for state-transition strategies.
50    /// `None` if not applicable.
51    pub state_field: Option<StateField>,
52    /// Alternative (under-scoped) credential set for scope-manipulation strategies.
53    /// `None` if not applicable.
54    pub alt_credential: Option<HeaderMap>,
55    /// Optional body template with `{id}` placeholder.
56    /// Used when the resource identifier appears in the request body rather than the URL.
57    pub body_template: Option<String>,
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use http::HeaderMap;
64
65    // --- Type construction ---
66
67    #[test]
68    fn known_duplicate_fields_accessible() {
69        let kd = KnownDuplicate {
70            field: "email".to_owned(),
71            value: "alice@example.com".to_owned(),
72        };
73        assert_eq!(kd.field, "email");
74        assert_eq!(kd.value, "alice@example.com");
75    }
76
77    #[test]
78    fn state_field_fields_accessible() {
79        let sf = StateField {
80            field: "status".to_owned(),
81            value: "invalid_state".to_owned(),
82        };
83        assert_eq!(sf.field, "status");
84        assert_eq!(sf.value, "invalid_state");
85    }
86
87    #[test]
88    fn scan_context_all_some_fields_accessible() {
89        let ctx = ScanContext {
90            target: "https://example.com/users/{id}".to_owned(),
91            baseline_id: "00000000-0000-0000-0000-000000000001".to_owned(),
92            probe_id: "ffffffff-ffff-ffff-ffff-ffffffffffff".to_owned(),
93            headers: HeaderMap::new(),
94            max_risk: RiskLevel::MethodDestructive,
95            known_duplicate: Some(KnownDuplicate {
96                field: "email".to_owned(),
97                value: "alice@example.com".to_owned(),
98            }),
99            state_field: Some(StateField {
100                field: "status".to_owned(),
101                value: "invalid_state".to_owned(),
102            }),
103            alt_credential: Some(HeaderMap::new()),
104            body_template: None,
105        };
106
107        assert_eq!(ctx.target, "https://example.com/users/{id}");
108        assert_eq!(ctx.max_risk, RiskLevel::MethodDestructive);
109        assert!(ctx.known_duplicate.is_some());
110        assert!(ctx.state_field.is_some());
111        assert!(ctx.alt_credential.is_some());
112    }
113
114    #[test]
115    fn scan_context_all_none_fields_accessible() {
116        let ctx = ScanContext {
117            target: "https://example.com/items/{id}".to_owned(),
118            baseline_id: "1".to_owned(),
119            probe_id: "999999".to_owned(),
120            headers: HeaderMap::new(),
121            max_risk: RiskLevel::Safe,
122            known_duplicate: None,
123            state_field: None,
124            alt_credential: None,
125            body_template: None,
126        };
127
128        assert!(ctx.known_duplicate.is_none());
129        assert!(ctx.state_field.is_none());
130        assert!(ctx.alt_credential.is_none());
131    }
132}