use super::models::{CollectionRequest, RequestResult};
use colored::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResults {
pub name: String,
pub total_tests: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub all_passed: bool,
pub test_results: Vec<TestResult>,
pub total_duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub name: String,
pub operation: String,
pub passed: bool,
pub status: TestStatus,
pub duration_ms: u64,
pub assertions: Vec<AssertionResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TestStatus {
Passed,
Failed,
Skipped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssertionResult {
pub assertion_type: AssertionType,
pub passed: bool,
pub expected: Value,
pub actual: Value,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssertionType {
Status,
Body,
Header,
}
pub fn evaluate_expectations(
request: &CollectionRequest,
result: &RequestResult,
) -> Vec<AssertionResult> {
let mut assertions = Vec::new();
if let Some(expect) = &request.expect {
if let Some(expected_status) = expect.status {
assertions.push(AssertionResult {
assertion_type: AssertionType::Status,
passed: result.status == expected_status,
expected: Value::Number(expected_status.into()),
actual: Value::Number(result.status.into()),
message: if result.status == expected_status {
format!("Status code is {}", expected_status)
} else {
format!("Expected status {}, got {}", expected_status, result.status)
},
path: None,
});
}
if let Some(expected_body) = &expect.body {
if let Some(actual_body) = &result.body {
let matches = check_body_match(expected_body, actual_body);
assertions.push(AssertionResult {
assertion_type: AssertionType::Body,
passed: matches,
expected: expected_body.clone(),
actual: actual_body.clone(),
message: if matches {
"Response body matches expected".to_string()
} else {
"Response body does not match expected".to_string()
},
path: Some("body".to_string()),
});
} else {
assertions.push(AssertionResult {
assertion_type: AssertionType::Body,
passed: false,
expected: expected_body.clone(),
actual: Value::Null,
message: "Response has no body".to_string(),
path: Some("body".to_string()),
});
}
}
if let Some(expected_headers) = &expect.headers {
for (header_name, expected_value) in expected_headers {
let actual_value = result
.headers
.get(header_name)
.map(|v| Value::String(v.clone()))
.unwrap_or(Value::Null);
let matches = result
.headers
.get(header_name)
.map(|v| v == expected_value)
.unwrap_or(false);
assertions.push(AssertionResult {
assertion_type: AssertionType::Header,
passed: matches,
expected: Value::String(expected_value.clone()),
actual: actual_value,
message: if matches {
format!("Header '{}' matches", header_name)
} else {
format!("Header '{}' mismatch", header_name)
},
path: Some(format!("headers.{}", header_name)),
});
}
}
}
assertions
}
fn check_body_match(expected: &Value, actual: &Value) -> bool {
match (expected, actual) {
(Value::Object(exp_map), Value::Object(act_map)) => {
exp_map.iter().all(|(key, exp_val)| {
act_map
.get(key)
.map(|act_val| check_body_match(exp_val, act_val))
.unwrap_or(false)
})
}
(Value::Array(exp_arr), Value::Array(act_arr)) => {
exp_arr.len() == act_arr.len()
&& exp_arr
.iter()
.zip(act_arr.iter())
.all(|(exp, act)| check_body_match(exp, act))
}
_ => expected == actual,
}
}
pub fn to_junit_xml(results: &TestResults) -> String {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str(&format!(
"<testsuites name=\"{}\" tests=\"{}\" failures=\"{}\" time=\"{}\">\n",
results.name,
results.total_tests,
results.failed,
results.total_duration_ms as f64 / 1000.0
));
xml.push_str(&format!(
" <testsuite name=\"{}\" tests=\"{}\" failures=\"{}\" skipped=\"{}\" time=\"{}\">\n",
results.name,
results.total_tests,
results.failed,
results.skipped,
results.total_duration_ms as f64 / 1000.0
));
for test in &results.test_results {
let time_sec = test.duration_ms as f64 / 1000.0;
match test.status {
TestStatus::Passed => {
xml.push_str(&format!(
" <testcase name=\"{}\" classname=\"{}\" time=\"{}\" />\n",
test.name, test.operation, time_sec
));
}
TestStatus::Failed => {
xml.push_str(&format!(
" <testcase name=\"{}\" classname=\"{}\" time=\"{}\">\n",
test.name, test.operation, time_sec
));
let failure_msg = test
.assertions
.iter()
.filter(|a| !a.passed)
.map(|a| a.message.as_str())
.collect::<Vec<_>>()
.join("; ");
xml.push_str(&format!(
" <failure message=\"{}\" type=\"assertion\">{}</failure>\n",
escape_xml(&failure_msg),
escape_xml(&failure_msg)
));
xml.push_str(" </testcase>\n");
}
TestStatus::Skipped => {
xml.push_str(&format!(
" <testcase name=\"{}\" classname=\"{}\" time=\"{}\">\n",
test.name, test.operation, time_sec
));
xml.push_str(" <skipped />\n");
xml.push_str(" </testcase>\n");
}
}
}
xml.push_str(" </testsuite>\n");
xml.push_str("</testsuites>\n");
xml
}
pub fn print_test_results(results: &TestResults) {
println!("\n{} Test Results: {}", "๐งช".cyan(), results.name.bold());
println!("{}", "โ".repeat(50));
for test in &results.test_results {
let icon = match test.status {
TestStatus::Passed => "โ".green(),
TestStatus::Failed => "โ".red(),
TestStatus::Skipped => "โ".yellow(),
};
let status_str = match test.status {
TestStatus::Passed => "PASSED".green(),
TestStatus::Failed => "FAILED".red(),
TestStatus::Skipped => "SKIPPED".yellow(),
};
println!(
"{} {} {} ({}ms)",
icon,
test.name.bold(),
status_str,
test.duration_ms
);
if test.status == TestStatus::Failed {
for assertion in &test.assertions {
if !assertion.passed {
println!(
" {} {} assertion: {}",
"โ".dimmed(),
format!("{:?}", assertion.assertion_type).red(),
assertion.message.dimmed()
);
if assertion.expected != assertion.actual {
println!(
" {} Expected: {}",
"โ".dimmed(),
serde_json::to_string(&assertion.expected)
.unwrap_or_default()
.green()
);
println!(
" {} Actual: {}",
"โ".dimmed(),
serde_json::to_string(&assertion.actual)
.unwrap_or_default()
.red()
);
}
}
}
}
if let Some(error) = &test.error {
println!(" {} Error: {}", "โ".dimmed(), error.red());
}
}
println!("\n{}", "โ".repeat(50));
println!(
"Tests: {}, Passed: {}, Failed: {}, Skipped: {}",
results.total_tests.to_string().bold(),
results.passed.to_string().green(),
results.failed.to_string().red(),
results.skipped.to_string().yellow()
);
let duration_sec = results.total_duration_ms as f64 / 1000.0;
println!("Duration: {:.2}s", duration_sec);
if results.all_passed {
println!("\n{} All tests passed!", "โจ".green().bold());
} else {
println!("\n{} Some tests failed", "โ".red().bold());
}
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}