use crate::step::Step;
use crate::store::StoreKey;
use crate::test::TestState;
use futures::future::BoxFuture;
use reqwest::Method;
use serde::Serialize;
use serde_json::Value;
use std::fmt::Debug;
use thiserror::Error;
#[derive(Debug, Clone, Copy, Default)]
pub enum ExpectJsonTarget {
#[default]
Exact,
Contains,
Unordered,
}
#[derive(Debug, Clone, Default)]
pub struct HttpExpectations {
pub status: Option<reqwest::StatusCode>,
pub cookies: Vec<String>,
pub json_body: Option<Value>,
pub json_target: ExpectJsonTarget,
pub json_pointers: Vec<JsonPointerExpectation>,
}
#[derive(Debug, Clone)]
pub struct JsonPointerExpectation {
pub pointer: String,
pub expected: Value,
pub target: ExpectJsonTarget,
}
#[derive(Debug, Clone)]
pub enum Extractor {
JsonPointer { pointer: String, key: StoreKey },
EntireJson { key: StoreKey },
}
#[derive(Debug, Clone)]
pub struct HttpStep {
client_name: String,
method: Method,
path: String,
json_body: Option<Value>,
expectations: HttpExpectations,
extractors: Vec<Extractor>,
bearer_key: Option<StoreKey>,
}
impl HttpStep {
async fn do_execute(&self, state: &TestState) -> anyhow::Result<()> {
let Some(client_config) = state.clients.get(&self.client_name) else {
return Err(anyhow::anyhow!(
"Client '{}' not registered",
self.client_name
));
};
let url = format!("{}{}", client_config.base_url, self.path);
let mut request = client_config.client.request(self.method.clone(), url);
if let Some(bearer_key) = &self.bearer_key {
let token: Option<String> = state.store.get(bearer_key.clone());
let Some(token) = token else {
return Err(anyhow::anyhow!(
"Bearer token missing for key {:?}",
bearer_key
));
};
request = request.bearer_auth(token);
}
if let Some(body) = &self.json_body {
request = request.json(body);
}
let response = request.send().await?;
let status = response.status();
let headers = response.headers().clone();
let body_bytes = response.bytes().await?;
let expected_status = self
.expectations
.status
.ok_or_else(|| anyhow::anyhow!("Expected status is required"))?;
if status != expected_status {
return Err(anyhow::anyhow!(
"Expected status {} but got {}",
expected_status,
status
));
}
for cookie_name in &self.expectations.cookies {
let found = headers
.get_all(reqwest::header::SET_COOKIE)
.iter()
.any(|value| value.to_str().unwrap_or("").contains(cookie_name));
if !found {
return Err(anyhow::anyhow!("Missing cookie '{}'", cookie_name));
}
}
let mut json_value: Option<Value> = None;
if self.expectations.json_body.is_some()
|| !self.extractors.is_empty()
|| !self.expectations.json_pointers.is_empty()
{
let parsed: Value = serde_json::from_slice(&body_bytes)
.map_err(|error| anyhow::anyhow!("Failed to parse JSON response: {error}"))?;
json_value = Some(parsed);
}
if let Some(expected_json) = &self.expectations.json_body {
let Some(actual_json) = json_value.as_ref() else {
return Err(anyhow::anyhow!("No JSON body to compare"));
};
match self.expectations.json_target {
ExpectJsonTarget::Exact => {
if actual_json != expected_json {
return Err(anyhow::anyhow!(
"JSON mismatch. Expected: {expected_json:?}, Actual: {actual_json:?}"
));
}
}
ExpectJsonTarget::Contains => {
if !json_contains(actual_json, expected_json) {
return Err(anyhow::anyhow!(
"JSON did not contain expected subset. Expected: {expected_json:?}, Actual: {actual_json:?}"
));
}
}
ExpectJsonTarget::Unordered => {
if !json_unordered_eq(actual_json, expected_json) {
return Err(anyhow::anyhow!(
"JSON unordered mismatch. Expected: {expected_json:?}, Actual: {actual_json:?}"
));
}
}
}
}
if !self.expectations.json_pointers.is_empty() {
let Some(actual_json) = json_value.as_ref() else {
return Err(anyhow::anyhow!("No JSON body to compare"));
};
for expectation in &self.expectations.json_pointers {
let Some(actual_value) = actual_json.pointer(&expectation.pointer) else {
return Err(anyhow::anyhow!(
"JSON pointer '{}' not found",
expectation.pointer
));
};
match expectation.target {
ExpectJsonTarget::Exact => {
if actual_value != &expectation.expected {
return Err(anyhow::anyhow!(
"JSON pointer '{}' mismatch. Expected: {:?}, Actual: {:?}",
expectation.pointer,
expectation.expected,
actual_value
));
}
}
ExpectJsonTarget::Contains => {
if !json_contains(actual_value, &expectation.expected) {
return Err(anyhow::anyhow!(
"JSON pointer '{}' did not contain expected subset. Expected: {:?}, Actual: {:?}",
expectation.pointer,
expectation.expected,
actual_value
));
}
}
ExpectJsonTarget::Unordered => {
if !json_unordered_eq(actual_value, &expectation.expected) {
return Err(anyhow::anyhow!(
"JSON pointer '{}' unordered mismatch. Expected: {:?}, Actual: {:?}",
expectation.pointer,
expectation.expected,
actual_value
));
}
}
}
}
}
if let Some(value) = json_value.as_ref() {
for extractor in &self.extractors {
match extractor {
Extractor::JsonPointer { pointer, key } => {
let Some(value) = value.pointer(pointer) else {
return Err(anyhow::anyhow!("JSON pointer '{}' not found", pointer));
};
state.store.insert(key.clone(), value.clone())?;
}
Extractor::EntireJson { key } => {
state.store.insert(key.clone(), value.clone())?;
}
}
}
}
Ok(())
}
}
impl Step for HttpStep {
fn execute<'a>(&'a self, state: &'a TestState) -> BoxFuture<'a, anyhow::Result<()>> {
Box::pin(async move { self.do_execute(state).await })
}
}
#[derive(Debug, Default)]
pub struct HttpStepBuilder {
client_name: Option<String>,
method: Option<Method>,
path: Option<String>,
json_body: Option<Value>,
expectations: HttpExpectations,
extractors: Vec<Extractor>,
bearer_key: Option<StoreKey>,
build_error: Option<HttpStepBuildError>,
}
#[derive(Debug, Error, Clone)]
pub enum HttpStepBuildError {
#[error("HTTP client name is required")]
MissingClient,
#[error("HTTP method is required")]
MissingMethod,
#[error("HTTP path is required")]
MissingPath,
#[error("Expected status must be set")]
MissingExpectedStatus,
#[error("Failed to serialize JSON payload: {0}")]
JsonSerialization(String),
}
impl HttpStepBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn client(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
self
}
pub fn get(mut self, path: impl Into<String>) -> Self {
self.method = Some(Method::GET);
self.path = Some(path.into());
self
}
pub fn post(mut self, path: impl Into<String>) -> Self {
self.method = Some(Method::POST);
self.path = Some(path.into());
self
}
pub fn patch(mut self, path: impl Into<String>) -> Self {
self.method = Some(Method::PATCH);
self.path = Some(path.into());
self
}
pub fn delete(mut self, path: impl Into<String>) -> Self {
self.method = Some(Method::DELETE);
self.path = Some(path.into());
self
}
pub fn json(mut self, body: impl Serialize) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(body) {
Ok(value) => self.json_body = Some(value),
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn expect_status(mut self, status: reqwest::StatusCode) -> Self {
self.expectations.status = Some(status);
self
}
pub fn expect_cookie(mut self, cookie_name: impl Into<String>) -> Self {
self.expectations.cookies.push(cookie_name.into());
self
}
pub fn expect_json(mut self, expected: impl Serialize) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(expected) {
Ok(value) => {
self.expectations.json_body = Some(value);
self.expectations.json_target = ExpectJsonTarget::Exact;
}
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn expect_json_contains(mut self, expected: impl Serialize) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(expected) {
Ok(value) => {
self.expectations.json_body = Some(value);
self.expectations.json_target = ExpectJsonTarget::Contains;
}
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn expect_json_unordered(mut self, expected: impl Serialize) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(expected) {
Ok(value) => {
self.expectations.json_body = Some(value);
self.expectations.json_target = ExpectJsonTarget::Unordered;
}
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn expect_json_at(mut self, pointer: impl Into<String>, expected: impl Serialize) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(expected) {
Ok(value) => self
.expectations
.json_pointers
.push(JsonPointerExpectation {
pointer: pointer.into(),
expected: value,
target: ExpectJsonTarget::Exact,
}),
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn expect_json_contains_at(
mut self,
pointer: impl Into<String>,
expected: impl Serialize,
) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(expected) {
Ok(value) => self
.expectations
.json_pointers
.push(JsonPointerExpectation {
pointer: pointer.into(),
expected: value,
target: ExpectJsonTarget::Contains,
}),
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn expect_json_unordered_at(
mut self,
pointer: impl Into<String>,
expected: impl Serialize,
) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(expected) {
Ok(value) => self
.expectations
.json_pointers
.push(JsonPointerExpectation {
pointer: pointer.into(),
expected: value,
target: ExpectJsonTarget::Unordered,
}),
Err(error) => {
self.build_error =
Some(HttpStepBuildError::JsonSerialization(error.to_string()))
}
}
}
self
}
pub fn capture_json(mut self, pointer: impl Into<String>, key: StoreKey) -> Self {
self.extractors.push(Extractor::JsonPointer {
pointer: pointer.into(),
key,
});
self
}
pub fn capture_json_body(mut self, key: StoreKey) -> Self {
self.extractors.push(Extractor::EntireJson { key });
self
}
pub fn bearer_from(mut self, key: StoreKey) -> Self {
self.bearer_key = Some(key);
self
}
pub fn build(self) -> Result<HttpStep, HttpStepBuildError> {
if let Some(error) = self.build_error {
return Err(error);
}
if self.expectations.status.is_none() {
return Err(HttpStepBuildError::MissingExpectedStatus);
}
Ok(HttpStep {
client_name: self.client_name.ok_or(HttpStepBuildError::MissingClient)?,
method: self.method.ok_or(HttpStepBuildError::MissingMethod)?,
path: self.path.ok_or(HttpStepBuildError::MissingPath)?,
json_body: self.json_body,
expectations: self.expectations,
extractors: self.extractors,
bearer_key: self.bearer_key,
})
}
}
fn json_contains(actual: &Value, expected: &Value) -> bool {
match (actual, expected) {
(Value::Object(actual_map), Value::Object(expected_map)) => {
expected_map
.iter()
.all(|(key, expected_value)| match actual_map.get(key) {
Some(actual_value) => json_contains(actual_value, expected_value),
None => false,
})
}
(Value::Array(actual_list), Value::Array(expected_list)) => {
expected_list.iter().all(|expected_value| {
actual_list
.iter()
.any(|actual_value| json_contains(actual_value, expected_value))
})
}
_ => actual == expected,
}
}
fn json_unordered_eq(actual: &Value, expected: &Value) -> bool {
match (actual, expected) {
(Value::Array(actual_list), Value::Array(expected_list)) => {
let actual_sorted_result: Result<Vec<String>, _> =
actual_list.iter().map(serde_json::to_string).collect();
let expected_sorted_result: Result<Vec<String>, _> =
expected_list.iter().map(serde_json::to_string).collect();
let (Ok(mut actual_sorted), Ok(mut expected_sorted)) =
(actual_sorted_result, expected_sorted_result)
else {
return false;
};
actual_sorted.sort();
expected_sorted.sort();
actual_sorted == expected_sorted
}
_ => actual == expected,
}
}