parlov-output 0.8.0

Output formatters for parlov: SARIF, terminal table, and raw JSON.
Documentation
use super::build_curl;
use bytes::Bytes;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use parlov_core::ProbeDefinition;

fn probe(
    method: Method,
    url: &str,
    headers: &[(&str, &str)],
    body: Option<&str>,
) -> ProbeDefinition {
    let mut hm = HeaderMap::new();
    for &(n, v) in headers {
        hm.insert(
            HeaderName::from_bytes(n.as_bytes()).expect("valid header name"),
            HeaderValue::from_str(v).expect("valid header value"),
        );
    }
    ProbeDefinition {
        url: url.to_owned(),
        method,
        headers: hm,
        body: body.map(|b| Bytes::copy_from_slice(b.as_bytes())),
    }
}

#[test]
fn build_curl_get_no_body_no_headers() {
    let p = probe(Method::GET, "http://x/y", &[], None);
    assert_eq!(build_curl(&p), "curl -X GET 'http://x/y'");
}

#[test]
fn build_curl_post_with_json_body() {
    let p = probe(
        Method::POST,
        "http://x/y",
        &[("content-type", "application/json")],
        Some(r#"{"k":"v"}"#),
    );
    let out = build_curl(&p);
    assert!(out.starts_with("curl -X POST 'http://x/y'"));
    assert!(out.contains("--data '{\"k\":\"v\"}'"));
}

#[test]
fn build_curl_with_simple_header() {
    let p = probe(
        Method::GET,
        "http://x/y",
        &[("accept", "application/json")],
        None,
    );
    let out = build_curl(&p);
    assert!(out.contains("-H 'accept: application/json'"));
}

#[test]
fn build_curl_with_authorization_header_not_redacted() {
    let p = probe(
        Method::GET,
        "http://x/y",
        &[("authorization", "Bearer s3cret-t0ken")],
        None,
    );
    let out = build_curl(&p);
    assert!(
        out.contains("Bearer s3cret-t0ken"),
        "Authorization must appear verbatim — got: {out}"
    );
}

#[test]
fn build_curl_escapes_single_quote_in_header_value() {
    let p = probe(Method::GET, "http://x/y", &[("x-test", "it's")], None);
    let out = build_curl(&p);
    // shell-escape closes the single-quoted block at the literal quote, inserts \',
    // then reopens — the entire `name: value` is wrapped together.
    assert!(
        out.contains(r"'x-test: it'\''s'"),
        "expected shell-escaped single quote in header line; got: {out}"
    );
}

#[test]
fn build_curl_escapes_single_quote_in_body() {
    let p = probe(Method::POST, "http://x/y", &[], Some(r#"{"k":"v's"}"#));
    let out = build_curl(&p);
    assert!(out.contains(r"'\''"));
}

#[test]
fn build_curl_escapes_url_with_query_string_special_chars() {
    let p = probe(Method::GET, "http://x/y?q=a&b=c'd", &[], None);
    let out = build_curl(&p);
    assert!(out.contains(r"'\''"));
    assert!(out.contains("?q=a&b=c"));
}

#[test]
fn build_curl_with_empty_body_some() {
    let p = ProbeDefinition {
        url: "http://x/y".to_owned(),
        method: Method::POST,
        headers: HeaderMap::new(),
        body: Some(Bytes::new()),
    };
    let out = build_curl(&p);
    assert!(out.contains("--data ''"));
}

#[test]
fn build_curl_uses_uppercase_method() {
    // http::Method is canonical-uppercase; this verifies our cast preserves that.
    let p = probe(Method::PATCH, "http://x/y", &[], Some("{}"));
    assert!(build_curl(&p).starts_with("curl -X PATCH "));
}

#[test]
fn build_curl_with_multiple_headers_each_on_own_line() {
    let p = probe(
        Method::GET,
        "http://x/y",
        &[("accept", "application/json"), ("x-foo", "bar")],
        None,
    );
    let out = build_curl(&p);
    let header_lines: Vec<&str> = out.lines().filter(|l| l.contains("-H ")).collect();
    assert_eq!(
        header_lines.len(),
        2,
        "expected one line per header — got: {out}"
    );
}