bddkit 0.1.1

Gherkin acceptance testing for backend services: one binary drives the HTTP API and the resources behind it
use crate::http::ReplayError;
use crate::json::{matcher, path};
use crate::polling::{AttemptError, AttemptResult};
use crate::world::World;

pub(super) async fn replay_response(w: &mut World, attempt: u64) -> AttemptResult {
    if attempt == 0 {
        return Ok(());
    }
    w.http.replay_last().await.map_err(|error| match error {
        ReplayError::NotYet(message) => AttemptError::NotYet(message),
        ReplayError::Fatal(message) => AttemptError::Fatal(message),
    })
}

pub(crate) fn last(w: &World) -> Result<&crate::http::Exchange, String> {
    w.http
        .last()
        .ok_or_else(|| "no request has been sent yet".to_string())
}

pub fn response_code(w: &World, expected: &str) -> AttemptResult {
    let want: u16 = expected
        .parse()
        .map_err(|_| AttemptError::Fatal(format!("invalid code {expected:?}")))?;
    let got = last(w).map_err(AttemptError::Fatal)?.status;
    if got == want {
        Ok(())
    } else {
        Err(AttemptError::NotYet(format!(
            "    expected: {want}\n    actual:   {got}"
        )))
    }
}

pub fn response_header(w: &World, name: &str, value: &str) -> AttemptResult {
    let ex = last(w).map_err(AttemptError::Fatal)?;
    let got = ex
        .resp_headers
        .iter()
        .find(|(k, _)| k.eq_ignore_ascii_case(name))
        .map(|(_, v)| v.as_str());
    match got {
        Some(v) if v == value => Ok(()),
        Some(v) => Err(AttemptError::NotYet(format!(
            "    expected: {value}\n    actual:   {v}"
        ))),
        None => Err(AttemptError::NotYet(format!(
            "response header {name:?} is missing"
        ))),
    }
}

pub fn body_contains_json(w: &World, docstring: Option<&String>) -> AttemptResult {
    let expected_src =
        docstring.ok_or_else(|| AttemptError::Fatal("step requires a doc string with JSON".to_string()))?;
    let expected: serde_json::Value = serde_json::from_str(expected_src)
        .map_err(|e| AttemptError::Fatal(format!("expected JSON is invalid: {e}")))?;
    let actual = last(w)
        .map_err(AttemptError::Fatal)?
        .json()
        .map_err(AttemptError::NotYet)?;
    matcher::contains(&actual, &expected).map_err(|m| AttemptError::NotYet(m.to_string()))
}

pub fn body_equals_json(w: &World, docstring: Option<&String>) -> AttemptResult {
    let expected_src =
        docstring.ok_or_else(|| AttemptError::Fatal("step requires a doc string with JSON".to_string()))?;
    let expected: serde_json::Value = serde_json::from_str(expected_src)
        .map_err(|e| AttemptError::Fatal(format!("expected JSON is invalid: {e}")))?;
    let actual = last(w)
        .map_err(AttemptError::Fatal)?
        .json()
        .map_err(AttemptError::NotYet)?;
    matcher::equals(&actual, &expected).map_err(|m| AttemptError::NotYet(m.to_string()))
}

pub fn array_length(w: &World, expected: &str) -> AttemptResult {
    let want: usize = expected
        .parse()
        .map_err(|_| AttemptError::Fatal(format!("invalid length {expected:?}")))?;
    let v = last(w)
        .map_err(AttemptError::Fatal)?
        .json()
        .map_err(AttemptError::NotYet)?;
    let arr = v
        .as_array()
        .ok_or_else(|| AttemptError::NotYet("response body is not an array".to_string()))?;
    if arr.len() == want {
        Ok(())
    } else {
        Err(AttemptError::NotYet(format!(
            "    expected: array of length {want}\n    actual:   length {}",
            arr.len()
        )))
    }
}

pub fn json_node_exists(w: &World, p: &str) -> AttemptResult {
    path::validate(p).map_err(AttemptError::Fatal)?;
    let v = last(w)
        .map_err(AttemptError::Fatal)?
        .json()
        .map_err(AttemptError::NotYet)?;
    path::read(&v, p).map(|_| ()).map_err(AttemptError::NotYet)
}