parlov 0.6.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
//! Sanity tests for the cross-method server: application-generated vs
//! server-generated 404.
//!
//! The oracle is that app-routed 404s are structured JSON with
//! `X-Request-Id`, while unrouted paths return plain-text server 404s
//! with no such header.

#![deny(clippy::all)]

mod fixtures {
    pub mod cross_method_server;
}

use fixtures::cross_method_server::spawn;

// ========================================================================
// /api/users/{id} — app-generated 404 (structured JSON + X-Request-Id)
// ========================================================================

#[tokio::test]
async fn app_404_returns_404_status() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/users/42"))
        .await
        .expect("request failed");
    assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn app_404_has_json_content_type() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/users/42"))
        .await
        .expect("request failed");
    assert_eq!(
        resp.headers()["content-type"].to_str().unwrap(),
        "application/json"
    );
}

#[tokio::test]
async fn app_404_has_x_request_id_header() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/users/42"))
        .await
        .expect("request failed");
    assert!(
        resp.headers().contains_key("x-request-id"),
        "expected X-Request-Id header on app-generated 404"
    );
    assert_eq!(
        resp.headers()["x-request-id"].to_str().unwrap(),
        "req_test123"
    );
}

#[tokio::test]
async fn app_404_body_is_structured_json() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/users/99"))
        .await
        .expect("request failed");
    let body: serde_json::Value = resp.json().await.expect("invalid json");
    assert_eq!(body["error"], "user not found");
}

// ========================================================================
// /api/zzz/{id} — server-generated 404 (no JSON, no X-Request-Id)
// ========================================================================

#[tokio::test]
async fn server_404_returns_404_status() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/zzz/42"))
        .await
        .expect("request failed");
    assert_eq!(resp.status(), reqwest::StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn server_404_does_not_have_json_content_type() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/zzz/42"))
        .await
        .expect("request failed");
    let ct = resp
        .headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");
    assert!(
        !ct.contains("application/json"),
        "server-generated 404 must not have application/json content-type, got: {ct}"
    );
}

#[tokio::test]
async fn server_404_does_not_have_x_request_id() {
    let addr = spawn().await;
    let resp = reqwest::get(format!("http://{addr}/api/zzz/42"))
        .await
        .expect("request failed");
    assert!(
        !resp.headers().contains_key("x-request-id"),
        "server-generated 404 must not have X-Request-Id header"
    );
}