use std::{collections::HashMap, str::FromStr, sync::Arc, time::Instant};
use reqwest::{
Client,
header::{HeaderMap, HeaderName, HeaderValue},
};
use serde_json::Value;
use crate::models::{
config::Assert,
context::{Global, RunState},
error::AlixtError,
plan::{ExecuteRequest, RunPlan, TestPlan},
test_data::{AssertionOutcome, FailureType, RequestOutcome, RunData, TestData},
};
pub async fn execute_test(
client: &Client,
plan: TestPlan,
global: Arc<Global>,
) -> Result<TestData, AlixtError> {
let mut test_outcome = TestData::new();
for run in plan.runs {
match execute_run(client, run, RunState::new(global.clone())).await {
Ok(outcome) => test_outcome.run_data.push(outcome),
Err(e) => return Err(e),
};
}
Ok(test_outcome)
}
async fn execute_run(
client: &Client,
run: RunPlan,
mut state: RunState,
) -> Result<RunData, AlixtError> {
let mut run_outcome = RunData::new(run.name.clone());
for request in run.requests {
let outcome = execute_request(client, request, &mut state).await?;
if !outcome.passing.is_passing() && outcome.breaking {
run_outcome.outcomes.push(outcome);
return Ok(run_outcome);
}
run_outcome.outcomes.push(outcome);
}
Ok(run_outcome)
}
async fn execute_request(
client: &Client,
request: ExecuteRequest,
state: &mut RunState,
) -> Result<RequestOutcome, AlixtError> {
let url = state.substitute_values_in_text(&request.url);
let mut final_headers = HeaderMap::new();
if let Some(headers) = &request.headers {
for (key, value) in headers {
let value = state.substitute_values_in_text(value);
let header_name = HeaderName::from_str(key).map_err(|e| {
AlixtError::Config(format!("Invalid Header Name '{}', {:#?}", key, e))
})?;
let header_value = HeaderValue::from_str(value.as_str()).map_err(|e| {
AlixtError::Config(format!(
"Invalid header value for '{}: {}', {:#?}",
key, value, e
))
})?;
final_headers.insert(header_name, header_value);
}
}
let mut builder = client
.request(request.method.clone(), url.clone())
.headers(final_headers);
if let Some(text) = request.body {
let body = state.substitute_values_in_text(text.as_str());
builder = builder.body(body);
}
let start = Instant::now();
let response = builder.send().await?;
let duration = start.elapsed();
let status = response.status();
let body_text = response.text().await?;
let json: Option<Value> = if !body_text.is_empty() {
serde_json::from_str(&body_text).ok()
} else {
None
};
if let Some(capture) = request.capture
&& let Some(json) = &json
{
for (variable, pattern) in capture {
let value = if pattern.starts_with('/') {
json.pointer(&pattern)
} else {
json.get(pattern)
};
if let Some(value) = value {
let value_string = match value {
Value::String(s) => s.clone(),
v => v.to_string(),
};
state.run_variables.insert(variable, value_string);
}
}
}
let mut outcome = RequestOutcome {
name: request.name,
method: request.method.to_string(),
url,
passing: AssertionOutcome::Passed,
breaking: false,
status: Some(status.as_u16()),
response_body: if !body_text.is_empty() {
Some(body_text)
} else {
None
},
duration,
};
if let Some(assert) = request.assert {
outcome.breaking = assert.breaking;
outcome.passing = assert_response(json.as_ref(), &assert, outcome.status, state);
}
Ok(outcome)
}
fn assert_response(
body_json: Option<&Value>,
assertions: &Assert,
status: Option<u16>,
state: &mut RunState,
) -> AssertionOutcome {
let mut outcome = AssertionOutcome::Passed;
if let Some(expected) = assertions.status {
if status.map_or(true, |num| num != expected) {
outcome.push(FailureType::StatusMismatch {
expected,
found: status,
})
}
}
if assertions.body_matches.is_none()
&& assertions.subset_matches.is_none()
&& assertions.subset_includes.is_none()
&& assertions.subset_regex.is_none()
{
return outcome;
}
let Some(json) = body_json else {
outcome.push(FailureType::InvalidJson());
return outcome;
};
let check_subset = |expected: &HashMap<String, Value>,
outcome: &mut AssertionOutcome,
regex: bool,
state: &mut RunState| {
for (path, expected_value) in expected {
match json.pointer(path) {
Some(Value::String(value)) if regex => {
let Some(pattern) = expected_value.as_str() else {
outcome.push(FailureType::JsonRegexMismatch {
path: path.clone(),
pattern: expected_value.to_string(),
found: "Invalid Regex Syntax".to_string(),
});
continue;
};
let pattern = state.substitute_values_in_text(pattern);
let Ok(re) = regex::Regex::new(pattern.as_str()) else {
outcome.push(FailureType::JsonRegexMismatch {
path: path.clone(),
pattern: pattern.to_string(),
found: "Invalid Regex Syntax".to_string(),
});
continue;
};
if !re.is_match(value) {
outcome.push(FailureType::JsonRegexMismatch {
path: path.clone(),
pattern: pattern.to_string(),
found: value.clone(),
});
}
}
Some(_) if regex => outcome.push(FailureType::JsonNotString { path: path.clone() }),
Some(value) => {
if expected_value.as_str() == Some("*") {
continue;
} else {
let expected_value = if let Value::String(expected_str) = expected_value {
Value::String(state.substitute_values_in_text(expected_str.as_str()).to_string())
} else {
expected_value.clone()
};
if &expected_value != value {
outcome.push(FailureType::JsonValueMismatch {
path: path.clone(),
expected: format!("{expected_value}"),
found: format!("{value}"),
})
}
}
}
None => outcome.push(FailureType::JsonMissingField { path: path.clone() }),
}
}
};
if let Some(expected) = assertions.body_matches.as_ref() {
let found_paths = flatten_json(json);
for path in &found_paths {
if !expected.contains_key(path) {
outcome.push(FailureType::JsonExtraField { path: path.clone() });
}
}
check_subset(expected, &mut outcome, false, state);
}
if let Some(expected) = assertions.subset_matches.as_ref() {
check_subset(expected, &mut outcome, false, state);
}
if let Some(expected) = assertions.subset_includes.as_ref() {
for path in expected {
match json.pointer(path) {
Some(_) => {}
None => outcome.push(FailureType::JsonMissingField { path: path.clone() }),
}
}
}
if let Some(expected) = assertions.subset_regex.as_ref() {
check_subset(expected, &mut outcome, true, state);
}
outcome
}
fn flatten_json(root: &Value) -> Vec<String> {
let mut paths: Vec<String> = Vec::new();
let mut stack: Vec<(&Value, String)> = vec![(root, "".to_string())];
while let Some((node, prefix)) = stack.pop() {
match node {
Value::Object(obj) => {
for (key, value) in obj {
let new_prefix = format!("{}/{}", prefix, key);
stack.push((value, new_prefix));
}
}
Value::Array(arr) => {
for (index, value) in arr.iter().enumerate() {
let new_prefix = format!("{}/{}", prefix, index);
stack.push((value, new_prefix));
}
}
_ => {
let key = if prefix.is_empty() {
"/".to_string()
} else {
prefix
};
paths.push(key);
}
}
}
paths
}
#[allow(unused)]
pub async fn execute_capture_run() {}
pub async fn execute_capture_request(
client: &Client,
request: &ExecuteRequest,
global: &mut Global,
) -> Result<(), AlixtError> {
let url = global.substitute_values_in_text(&request.url);
let mut final_headers = HeaderMap::new();
if let Some(headers) = &request.headers {
for (key, value) in headers {
let value = global.substitute_values_in_text(value);
let header_name = HeaderName::from_str(key).map_err(|e| {
AlixtError::Config(format!("Invalid Header Name '{}', {:#?}", key, e))
})?;
let header_value = HeaderValue::from_str(value.as_str()).map_err(|e| {
AlixtError::Config(format!(
"Invalid header value for '{}: {}', {:#?}",
key, value, e
))
})?;
final_headers.insert(header_name, header_value);
}
}
let mut builder = client
.request(request.method.clone(), url.clone())
.headers(final_headers);
if let Some(text) = &request.body {
let body = global.substitute_values_in_text(text.as_str());
builder = builder.body(body);
}
let response = builder.send().await?;
let Some(capture) = request.capture.as_ref() else {
return Ok(());
};
let response = response.json::<Value>().await?;
for (variable, pattern) in capture {
let value = if pattern.starts_with('/') {
response.pointer(pattern)
} else {
response.get(pattern)
};
let Some(value) = value else {
return Err(AlixtError::Config(format!(
"pattern '{}' not found in response body for request '{}'",
pattern, request.name
)));
};
let value_string = match value {
Value::String(s) => s.clone(),
v => v.to_string(),
};
global.variables.insert(variable.to_string(), value_string);
}
Ok(())
}