parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Unit and property tests for `control_integrity`.
//!
//! No mocks — real `DifferentialSet` values are constructed via the public `parlov-core`
//! constructors. Mirrors `surface_tests.rs` patterns.

use bytes::Bytes;
use http::{HeaderMap, StatusCode};
use parlov_core::{
    always_applicable, DifferentialSet, NormativeStrength, OracleClass, ProbeDefinition,
    ProbeExchange, ResponseSurface, SignalSurface, Technique, Vector,
};
use proptest::prelude::*;

use super::{control_integrity, ControlDecision};

// --- fixtures --------------------------------------------------------------

fn test_technique() -> Technique {
    Technique {
        id: "test-control",
        name: "Test control technique",
        oracle_class: OracleClass::Existence,
        vector: Vector::StatusCodeDiff,
        strength: NormativeStrength::May,
        normalization_weight: Some(0.05),
        inverted_signal_weight: None,
        method_relevant: false,
        parser_relevant: false,
        applicability: always_applicable,
        contradiction_surface: SignalSurface::Status,
    }
}

fn make_exchange(status: u16) -> ProbeExchange {
    ProbeExchange {
        request: ProbeDefinition {
            url: "https://example.com/r/1".into(),
            method: http::Method::GET,
            headers: HeaderMap::new(),
            body: None,
        },
        response: ResponseSurface {
            status: StatusCode::from_u16(status).expect("valid status"),
            headers: HeaderMap::new(),
            body: Bytes::new(),
            timing_ns: 0,
        },
    }
}

fn diff_set(
    baseline_status: u16,
    probe_status: u16,
    canonical_status: Option<u16>,
) -> DifferentialSet {
    DifferentialSet {
        baseline: vec![make_exchange(baseline_status)],
        probe: vec![make_exchange(probe_status)],
        canonical: canonical_status.map(make_exchange),
        technique: test_technique(),
    }
}

// --- gate inert: no canonical ---------------------------------------------

#[test]
fn no_canonical_returns_reached_one() {
    let ds = diff_set(404, 404, None);
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

#[test]
fn no_canonical_with_status_diff_returns_reached_one() {
    // Even when baseline/probe differ, the gate is inert without a canonical.
    let ds = diff_set(200, 404, None);
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

// --- empty baseline list (gate inert) -------------------------------------

#[test]
fn empty_baseline_with_canonical_returns_reached_one() {
    let ds = DifferentialSet {
        baseline: vec![],
        probe: vec![],
        canonical: Some(make_exchange(200)),
        technique: test_technique(),
    };
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

// --- both succeed: control preserved --------------------------------------

#[test]
fn canonical_200_mutated_200_reaches_one() {
    let ds = diff_set(200, 404, Some(200));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

#[test]
fn canonical_201_mutated_200_reaches_one() {
    // Both 2xx — control preserved (different success codes are still success).
    let ds = diff_set(200, 404, Some(201));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

// --- both fail: mutation didn't break things further ----------------------

#[test]
fn canonical_404_mutated_404_reaches_one() {
    // Mutation didn't break routing further than the existing 404. The Contradictory is
    // genuinely about normalization at the 404 level, not control destruction.
    let ds = diff_set(404, 404, Some(404));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

#[test]
fn canonical_401_mutated_401_reaches_one() {
    // Auth gate fires before the technique on both — control preserved (gate-wise).
    let ds = diff_set(401, 401, Some(401));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

#[test]
fn canonical_500_mutated_500_reaches_one() {
    let ds = diff_set(500, 500, Some(500));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

// --- canonical succeeded, mutated failed: classic case-sensitive ----------

#[test]
fn canonical_200_mutated_404_blocks() {
    // Classic case-sensitive routing: canonical /api/users/1 → 200; mutated /api/USERS/1 → 404.
    let ds = diff_set(404, 404, Some(200));
    assert_eq!(control_integrity(&ds), ControlDecision::Blocked);
}

#[test]
fn canonical_204_mutated_404_blocks() {
    // 2xx canonical, non-2xx mutated → block (204 No Content is a success).
    let ds = diff_set(404, 404, Some(204));
    assert_eq!(control_integrity(&ds), ControlDecision::Blocked);
}

#[test]
fn canonical_200_mutated_500_blocks() {
    // 5xx mutated is non-success — also block.
    let ds = diff_set(500, 500, Some(200));
    assert_eq!(control_integrity(&ds), ControlDecision::Blocked);
}

// --- canonical 301/308: server canonicalized ------------------------------

#[test]
fn canonical_301_blocks() {
    // Server canonicalized — mutated path is not the same resource.
    let ds = diff_set(200, 200, Some(301));
    assert_eq!(control_integrity(&ds), ControlDecision::Blocked);
}

#[test]
fn canonical_308_blocks() {
    let ds = diff_set(200, 200, Some(308));
    assert_eq!(control_integrity(&ds), ControlDecision::Blocked);
}

// --- canonical 302/303/307: NOT canonicalization redirects ----------------

#[test]
fn canonical_302_does_not_block() {
    // 302 is a temporary redirect, not a canonicalization redirect like 301/308.
    // The runner follows it (default reqwest behavior) but the canonical-status reflects 302
    // when redirects are disabled. Treat as not-canonicalized.
    let ds = diff_set(200, 200, Some(302));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

#[test]
fn canonical_303_does_not_block() {
    let ds = diff_set(200, 200, Some(303));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

#[test]
fn canonical_307_does_not_block() {
    let ds = diff_set(200, 200, Some(307));
    assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
}

// --- ControlDecision::confidence accessor ---------------------------------

#[test]
fn confidence_of_reached_returns_inner() {
    assert!((ControlDecision::Reached(0.7).confidence() - 0.7).abs() < f64::EPSILON);
}

#[test]
fn confidence_of_reached_one() {
    assert!((ControlDecision::Reached(1.0).confidence() - 1.0).abs() < f64::EPSILON);
}

#[test]
fn confidence_of_blocked_is_zero() {
    assert!(ControlDecision::Blocked.confidence().abs() < f64::EPSILON);
}

// --- property tests --------------------------------------------------------

proptest! {
    /// `control_integrity` is referentially transparent — same input produces same output.
    #[test]
    fn control_integrity_referentially_transparent(
        baseline_status in 200u16..=599,
        probe_status in 200u16..=599,
        canonical_present in any::<bool>(),
        canonical_status in 200u16..=599,
    ) {
        let canonical = if canonical_present { Some(canonical_status) } else { None };
        let ds = diff_set(baseline_status, probe_status, canonical);
        let d1 = control_integrity(&ds);
        let d2 = control_integrity(&ds);
        prop_assert_eq!(d1, d2);
    }

    /// When `canonical` is None, the gate is always inert (Reached(1.0)).
    #[test]
    fn no_canonical_always_reaches_one(
        baseline_status in 200u16..=599,
        probe_status in 200u16..=599,
    ) {
        let ds = diff_set(baseline_status, probe_status, None);
        prop_assert_eq!(control_integrity(&ds), ControlDecision::Reached(1.0));
    }

    /// `confidence()` is bounded in `[0.0, 1.0]` for any decision.
    #[test]
    fn confidence_is_bounded(
        baseline_status in 200u16..=599,
        probe_status in 200u16..=599,
        canonical_status in 200u16..=599,
    ) {
        let ds = diff_set(baseline_status, probe_status, Some(canonical_status));
        let c = control_integrity(&ds).confidence();
        prop_assert!(c >= 0.0);
        prop_assert!(c <= 1.0);
    }

    /// 301/308 canonical always blocks regardless of baseline/probe.
    #[test]
    fn canonical_301_or_308_always_blocks(
        baseline_status in 200u16..=599,
        probe_status in 200u16..=599,
        select_308 in any::<bool>(),
    ) {
        let canonical_status = if select_308 { 308 } else { 301 };
        let ds = diff_set(baseline_status, probe_status, Some(canonical_status));
        prop_assert_eq!(control_integrity(&ds), ControlDecision::Blocked);
    }
}