parlov 0.7.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
//! Sanity tests for PATCH error-message-granularity (EMG) routes.
//!
//! Validates: 400/404 status diff where body confirms ID resolved before
//! schema validation, and the normalized negative case.

#![deny(clippy::all)]

mod fixtures {
    pub mod patch_server;
}

use fixtures::patch_server::spawn;
use serde_json::json;

// ========================================================================
// /emg/users/{id} — 400 vs 404, body confirms ID resolved before validation
// ========================================================================

#[tokio::test]
async fn emg_patch_users_known_id_returns_400() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/users/42"))
        .json(&json!({"email": ["invalid_type"]}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn emg_patch_users_unknown_id_returns_404() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/users/999"))
        .json(&json!({"email": ["invalid_type"]}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn emg_patch_users_known_id_body_is_type_error() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/users/42"))
        .json(&json!({"email": ["invalid_type"]}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(
        resp.headers()["content-type"].to_str().unwrap(),
        "application/json"
    );
    let body: serde_json::Value = resp.json().await.expect("invalid json");
    assert_eq!(
        body["error"],
        "invalid type for field 'email', expected string"
    );
}

#[tokio::test]
async fn emg_patch_users_unknown_id_body_is_not_found() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/users/999"))
        .json(&json!({"email": ["invalid_type"]}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(
        resp.headers()["content-type"].to_str().unwrap(),
        "application/json"
    );
    let body: serde_json::Value = resp.json().await.expect("invalid json");
    assert_eq!(body["error"], "user not found");
}

// ========================================================================
// /emg/normalized/{id} — negative case: identical 404 for both IDs
// ========================================================================

#[tokio::test]
async fn emg_patch_normalized_known_id_returns_404() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/normalized/42"))
        .json(&json!({"email": ["invalid_type"]}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn emg_patch_normalized_unknown_id_returns_404() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/normalized/999"))
        .json(&json!({"email": ["invalid_type"]}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn emg_patch_normalized_both_ids_have_identical_body() {
    let addr = spawn().await;
    let known_body = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/normalized/42"))
        .json(&json!({}))
        .send()
        .await
        .expect("request failed")
        .bytes()
        .await
        .expect("body read failed");
    let unknown_body = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/normalized/999"))
        .json(&json!({}))
        .send()
        .await
        .expect("request failed")
        .bytes()
        .await
        .expect("body read failed");
    assert_eq!(
        known_body, unknown_body,
        "normalized route must return identical bodies for all IDs"
    );
}

#[tokio::test]
async fn emg_patch_normalized_body_content() {
    let addr = spawn().await;
    let resp = reqwest::Client::new()
        .patch(format!("http://{addr}/emg/normalized/42"))
        .json(&json!({}))
        .send()
        .await
        .expect("request failed");
    assert_eq!(
        resp.headers()["content-type"].to_str().unwrap(),
        "application/json"
    );
    let body: serde_json::Value = resp.json().await.expect("invalid json");
    assert_eq!(body["error"], "not found");
}