parlov-elicit 0.1.2

Elicitation engine: strategy selection and probe plan generation for parlov.
Documentation
//! `Strategy` trait: the interface every elicitation strategy must implement.

use http::Method;

use crate::context::ScanContext;
use crate::types::{ProbeSpec, RiskLevel};

/// An elicitation strategy: maps a `ScanContext` to a set of `ProbeSpec` values.
///
/// Implementations are pure — no I/O, no async. The strategy inspects the
/// context, decides whether it applies, and generates the probe plan. The
/// scheduler owns execution.
pub trait Strategy: Send + Sync {
    /// Stable machine-readable identifier for this strategy, e.g.
    /// `"existence-get-200-404"`.
    fn id(&self) -> &'static str;

    /// Human-readable display name, e.g. `"GET 200/404 existence"`.
    fn name(&self) -> &'static str;

    /// Risk classification. The scheduler skips this strategy when
    /// `ctx.max_risk < self.risk()`.
    fn risk(&self) -> RiskLevel;

    /// HTTP methods this strategy exercises, used for capability filtering.
    fn methods(&self) -> &[Method];

    /// Returns `true` when this strategy can produce useful probes for `ctx`.
    ///
    /// Called before `generate`; the scheduler never calls `generate` when
    /// `is_applicable` returns `false`.
    fn is_applicable(&self, ctx: &ScanContext) -> bool;

    /// Produce probe specifications for `ctx`.
    ///
    /// Only called when `is_applicable` returns `true`. May return an empty
    /// `Vec` if context constraints prevent generation at runtime.
    fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::ScanContext;
    use crate::types::{ProbePair, ProbeSpec, RiskLevel, StrategyMetadata};
    use http::{HeaderMap, Method};
    use parlov_core::ProbeDefinition;

    struct TestStrategy;

    impl Strategy for TestStrategy {
        fn id(&self) -> &'static str {
            "test"
        }

        fn name(&self) -> &'static str {
            "Test Strategy"
        }

        fn risk(&self) -> RiskLevel {
            RiskLevel::Safe
        }

        fn methods(&self) -> &[Method] {
            &[Method::GET]
        }

        fn is_applicable(&self, _ctx: &ScanContext) -> bool {
            true
        }

        fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec> {
            let def = ProbeDefinition {
                url: ctx.target.clone(),
                method: Method::GET,
                headers: HeaderMap::new(),
                body: None,
            };
            let meta = StrategyMetadata {
                strategy_id: self.id(),
                strategy_name: self.name(),
                risk: self.risk(),
            };
            vec![ProbeSpec::Pair(ProbePair {
                baseline: def.clone(),
                probe: def,
                metadata: meta,
            })]
        }
    }

    // --- Object safety ---

    #[test]
    fn strategy_is_object_safe() {
        let _: Box<dyn Strategy> = Box::new(TestStrategy);
    }

    // --- Minimal implementation round-trip ---

    #[test]
    fn test_strategy_is_applicable_returns_true() {
        let ctx = ScanContext {
            target: "https://example.com/{id}".to_owned(),
            baseline_id: "1".to_owned(),
            probe_id: "999".to_owned(),
            headers: HeaderMap::new(),
            max_risk: RiskLevel::Safe,
            known_duplicate: None,
            state_field: None,
            alt_credential: None,
            body_template: None,
        };
        assert!(TestStrategy.is_applicable(&ctx));
    }

    #[test]
    fn test_strategy_generate_returns_pair() {
        let ctx = ScanContext {
            target: "https://example.com/{id}".to_owned(),
            baseline_id: "1".to_owned(),
            probe_id: "999".to_owned(),
            headers: HeaderMap::new(),
            max_risk: RiskLevel::Safe,
            known_duplicate: None,
            state_field: None,
            alt_credential: None,
            body_template: None,
        };
        let specs = TestStrategy.generate(&ctx);
        assert_eq!(specs.len(), 1);
        assert!(matches!(specs[0], ProbeSpec::Pair(_)));
    }

}