parlov 0.3.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
//! Integration tests for the GET existence oracle pipeline.
//!
//! Each test spawns an isolated RFC-compliant server, drives the adaptive sampling
//! loop via `evaluate`, and asserts the final verdict and severity.

#![deny(clippy::all)]

mod fixtures {
    pub mod get_server;
}

use bytes::Bytes;
use fixtures::get_server::spawn;
use http::{HeaderMap, Method};
use parlov_analysis::existence::ExistenceAnalyzer;
use parlov_analysis::{Analyzer, SampleDecision};
use parlov_core::{OracleResult, OracleVerdict, ProbeDefinition, ProbeSet, ResponseSurface, Severity};
use parlov_probe::http::HttpProbe;
use parlov_probe::Probe;
use reqwest::redirect;

// --- helpers ---

fn def(url: String) -> ProbeDefinition {
    ProbeDefinition {
        url,
        method: Method::GET,
        headers: HeaderMap::new(),
        body: None,
    }
}

/// Adaptive sampling loop matching the binary's `collect_until_verdict` pattern.
async fn collect_until_verdict(baseline_url: String, probe_url: String) -> OracleResult {
    let client = HttpProbe::new();
    let analyzer = ExistenceAnalyzer;
    let mut probe_set = ProbeSet {
        baseline: Vec::new(),
        probe: Vec::new(),
    };

    loop {
        let b = client.execute(&def(baseline_url.clone())).await.expect("baseline request failed");
        let p = client.execute(&def(probe_url.clone())).await.expect("probe request failed");
        probe_set.baseline.push(b);
        probe_set.probe.push(p);

        if let SampleDecision::Complete(result) = analyzer.evaluate(&probe_set) {
            return result;
        }
    }
}

/// Like `collect_until_verdict` but with a client that does not follow redirects.
async fn collect_no_redirect(baseline_url: String, probe_url: String) -> OracleResult {
    let client = HttpProbe::with_client(
        reqwest::Client::builder()
            .redirect(redirect::Policy::none())
            .build()
            .expect("client build failed"),
    );
    let analyzer = ExistenceAnalyzer;
    let mut probe_set = ProbeSet {
        baseline: Vec::new(),
        probe: Vec::new(),
    };
    loop {
        let b = client.execute(&def(baseline_url.clone())).await.expect("baseline failed");
        let p = client.execute(&def(probe_url.clone())).await.expect("probe failed");
        probe_set.baseline.push(b);
        probe_set.probe.push(p);
        if let SampleDecision::Complete(result) = analyzer.evaluate(&probe_set) {
            return result;
        }
    }
}

fn fake_surface(status: u16) -> ResponseSurface {
    ResponseSurface {
        status: http::StatusCode::from_u16(status).expect("valid status"),
        headers: HeaderMap::new(),
        body: Bytes::new(),
        timing_ns: 0,
    }
}

// --- 403 vs 404 → Confirmed / High ---

#[tokio::test]
async fn oracle_403_vs_404_confirmed_high() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/oracle/42"),
        format!("http://{addr}/oracle/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::High));
}

// --- 200 vs 404 → Confirmed / High ---

#[tokio::test]
async fn public_200_vs_404_confirmed_high() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/public/42"),
        format!("http://{addr}/public/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::High));
}

// --- 401 vs 404 → Confirmed / High ---

#[tokio::test]
async fn protected_401_vs_404_confirmed_high() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/protected/42"),
        format!("http://{addr}/protected/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::High));
}

// --- 410 vs 404 → Confirmed / Medium ---

#[tokio::test]
async fn archive_410_vs_404_confirmed_medium() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/archive/42"),
        format!("http://{addr}/archive/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::Medium));
}

// --- 429 vs 404 → Likely / Medium ---

#[tokio::test]
async fn metered_429_vs_404_likely_medium() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/metered/42"),
        format!("http://{addr}/metered/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Likely);
    assert_eq!(result.severity, Some(Severity::Medium));
}

// --- 500 vs 404 → Confirmed / Medium ---

#[tokio::test]
async fn broken_500_vs_404_confirmed_medium() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/broken/42"),
        format!("http://{addr}/broken/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::Medium));
}

// --- 402 vs 404 → Likely / Medium ---

#[tokio::test]
async fn premium_402_vs_404_likely_medium() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/premium/42"),
        format!("http://{addr}/premium/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Likely);
    assert_eq!(result.severity, Some(Severity::Medium));
}

// --- same vs same → NotPresent ---

#[tokio::test]
async fn normalized_same_same_not_present() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/normalized/42"),
        format!("http://{addr}/normalized/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::NotPresent);
    assert_eq!(result.severity, None);
}

// --- unrecognised diff → Likely / Low ---

#[tokio::test]
async fn unrecognised_diff_likely_low() {
    // Synthetic ProbeSet with a status pair not matching any known pattern (418 vs 404).
    // Classifier catch-all: Likely / Low for any unrecognised differential.
    let probe_set = ProbeSet {
        baseline: vec![fake_surface(418), fake_surface(418), fake_surface(418)],
        probe: vec![fake_surface(404), fake_surface(404), fake_surface(404)],
    };
    let result = ExistenceAnalyzer.analyze(&probe_set);
    assert_eq!(result.verdict, OracleVerdict::Likely);
    assert_eq!(result.severity, Some(Severity::Low));
}

// --- 206 vs 404 → Confirmed / High ---

#[tokio::test]
async fn ranged_206_vs_404_confirmed_high() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/ranged/42"),
        format!("http://{addr}/ranged/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::High));
    assert!(result.label.is_some());
    assert!(result.rfc_basis.is_some());
}

// --- 301 vs 404 → Confirmed / Medium ---

#[tokio::test]
async fn redirected_301_vs_404_confirmed_medium() {
    let addr = spawn().await;
    let result = collect_no_redirect(
        format!("http://{addr}/redirected/42"),
        format!("http://{addr}/redirected/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::Medium));
    assert!(result.label.is_some());
    assert!(result.rfc_basis.is_some());
}

// --- 304 vs 404 → Confirmed / High ---

#[tokio::test]
async fn conditional_304_vs_404_confirmed_high() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/conditional/42"),
        format!("http://{addr}/conditional/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::High));
    assert!(result.label.is_some());
    assert!(result.rfc_basis.is_some());
}

// --- 406 vs 404 → Confirmed / High ---

#[tokio::test]
async fn negotiated_406_vs_404_confirmed_high() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/negotiated/42"),
        format!("http://{addr}/negotiated/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::High));
    assert!(result.label.is_some());
    assert!(result.rfc_basis.is_some());
}

// --- 400 vs 404 → Likely / Low (wildcard, not in pattern table) ---

#[tokio::test]
async fn validated_400_vs_404_likely_low() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/validated/42"),
        format!("http://{addr}/validated/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Likely);
    assert_eq!(result.severity, Some(Severity::Low));
}

// --- 416 vs 404 → Confirmed / Medium ---

#[tokio::test]
async fn range_invalid_416_vs_404_confirmed_medium() {
    let addr = spawn().await;
    let result = collect_until_verdict(
        format!("http://{addr}/range-invalid/42"),
        format!("http://{addr}/range-invalid/999"),
    )
    .await;
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert_eq!(result.severity, Some(Severity::Medium));
    assert!(result.label.is_some());
    assert!(result.rfc_basis.is_some());
}