use std::{
collections::VecDeque, env, error::Error as StdError, future::Future, ops::Deref,
string::ToString,
};
use colored::{ColoredString, Colorize};
use http::{Method, StatusCode};
use log::{debug, info, trace};
use prettytable::{cell, row, Table};
use serde_json::Value as JsonValue;
use url::Url;
use crate::{
conformance::{
ConformanceTestSpec, OperationSpec, ParamPosition, RequestSource, RequestSpec,
ResolvedConformanceTestSpec, ResponseSpec, TestAuthentication, TestRequest, TestResponse,
},
validation::Error as ValidationError,
Error, Spec,
};
type TestResult = (ConformanceTestSpec, Result<TestResponse, Error>);
#[derive(Debug)]
pub struct TestRunner {
pub base_url: String,
pub spec: Spec,
pub queue: VecDeque<ConformanceTestSpec>,
pub results: Vec<TestResult>,
pub auth: Option<TestAuthentication>,
}
impl TestRunner {
pub fn new<T: Into<String>>(base_url: T, spec: Spec) -> Self {
Self {
base_url: base_url.into(),
spec,
queue: VecDeque::new(),
results: vec![],
auth: None,
}
}
pub fn add_tests(&mut self, tests: &[ConformanceTestSpec]) {
self.queue.append(&mut tests.to_owned().into())
}
pub fn add_test(&mut self, test: ConformanceTestSpec) {
self.queue.push_back(test);
}
pub fn immediate_test(&mut self, test: ConformanceTestSpec) -> impl Future<Output = ()> + '_ {
self.add_test(test);
self.run_queued_tests()
}
pub async fn send_request(&self, req: &TestRequest) -> Result<TestResponse, Error> {
let client = reqwest::Client::new();
let method: reqwest::Method = req.operation.method.as_str().parse().unwrap();
let url: String = [self.base_url.deref(), &req.operation.path].concat();
let url = req
.params
.iter()
.filter(|¶m| param.position == ParamPosition::Path)
.fold(url, |url, part| {
url.replace(&["{", &part.name, "}"].concat(), &part.value)
});
let mut url: Url = url.parse().unwrap();
{
let mut qs = url.query_pairs_mut();
qs.clear();
req.params
.iter()
.filter(|¶m| param.position == ParamPosition::Query)
.for_each(|param| {
qs.append_pair(¶m.name, ¶m.value);
});
}
let res = client
.request(method, &url.to_string())
.headers(req.headers.clone())
.body(req.body.to_vec())
.send()
.await?;
let status = res.status();
let headers = res.headers().clone();
let body_is_empty = res.content_length().map(|x| x == 0).unwrap_or(false);
let body = if body_is_empty {
None
} else {
Some(res.json().await.map_err(|_| ValidationError::NotJson)?)
};
Ok(TestResponse {
status,
headers,
body,
})
}
async fn run_test(&self, test: ResolvedConformanceTestSpec) -> Result<TestResponse, Error> {
debug!("request: {:?}", &test.request);
debug!("response spec: {:?}", &test.response);
let res = self.send_request(&test.request).await?;
test.response.validate_status(&res.status)?;
if test.response.body_validator.is_some() {
if res.body().is_none() {
return Err(ValidationError::NotJson.into());
}
test.response.validate_body(&res.body().unwrap())?;
}
Ok(res)
}
pub async fn run_queued_tests(&mut self) {
trace!("run queued tests");
while let Some(test) = self.queue.pop_front() {
println!("{} tests remaining", self.queue.len() + 1);
trace!("run test: {:?}", &test);
match test.resolve(&self.spec) {
Ok(resolved_test) => {
self.results
.push((test.clone(), self.run_test(resolved_test).await));
}
Err(err) => self.results.push((test.clone(), Err(err))),
}
}
}
pub fn results(&self) -> &[TestResult] {
&self.results
}
pub fn last_response_body(&self) -> Option<JsonValue> {
let (_, res) = self.results.last().unwrap();
let res = res.as_ref().unwrap();
res.body()
}
pub fn print_results(&self) {
let mut table = Table::new();
table.add_row(row!["TEST", "RESULT", "MESSAGE"]);
for (test, error) in &self.results {
let op = &test.operation;
let test_desc = if let Some(name) = &test.name {
let test_name = name.yellow();
let op_spec = op.to_string().italic();
format!("{}\n{}", test_name, op_spec)
} else {
let op_spec = op.to_string().italic();
op_spec.to_string()
};
let status = if error.is_err() {
" ERR ".red().reversed().blink()
} else {
" OK ".green().reversed()
};
let msg = error
.as_ref()
.err()
.map(|err| format_error(err))
.unwrap_or_else(|| "".normal());
table.add_row(row![test_desc, status, msg]);
}
table.printstd();
}
pub fn clear_results(&mut self) {
self.results.clear();
}
}
pub fn format_error(err: &dyn StdError) -> ColoredString {
let mut err_str = err.to_string();
err_str.push('\n');
let mut cause = err.source();
while let Some(err) = cause {
err_str.push_str(&err.to_string());
err_str.push('\n');
cause = err.source();
}
err_str.red()
}