use regex::Regex;
use reqwest::redirect::Policy;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, LazyLock, Mutex, Once};
use std::time::{Duration, Instant};
use xmltojson::to_json;
use async_recursion::async_recursion;
use encoding_rs::{Encoding, UTF_8};
use mime::Mime;
use reqwest::{Body, Client, Response};
use serde_json::{Map, Value};
use tokio::select;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use super::{
ApicizeBody, ApicizeExecution, ApicizeExecutionTestContext, ApicizeGroupResult,
ApicizeGroupResultContent, ApicizeGroupResultRow, ApicizeGroupResultRun, ApicizeHttpRequest,
ApicizeHttpResponse, ApicizeRequestResult, ApicizeRequestResultRun, ApicizeResult,
ApicizeTestBehavior, ApicizeTestResponse, ApicizeTestResult, DataContext, DataContextGenerator,
GetDataContext, Tally,
};
use crate::authorization::AuthorizationPlain;
use crate::oauth2_client_tokens::TokenResult;
use crate::parameters::ParameterCipher;
use crate::types::workspace::RequestExecutionParameters;
use crate::workspace::RequestExecutionState;
use crate::{
ApicizeError, ApicizeGroupResultRowContent, ApicizeRequestResultContent,
ApicizeRequestResultRow, ApicizeRequestResultRowContent, Authorization, Certificate,
ExecutionConcurrency, Identifiable, OAuth2ClientCredentialParameters, Proxy, Request,
RequestBody, RequestEntry, RequestGroup, RequestMethod, VariableCache, Workspace,
get_oauth2_client_credentials, retrieve_oauth2_token_from_cache,
};
static V8_INIT: Once = Once::new();
fn clone_and_sub(text: &str, subs: &HashMap<String, String>) -> String {
if subs.is_empty() || !text.contains("{{") {
return text.to_string();
}
let mut result = String::with_capacity(text.len());
let mut remaining = text;
while let Some(start) = remaining.find("{{") {
result.push_str(&remaining[..start]);
if let Some(end) = remaining[start + 2..].find("}}") {
let key = &remaining[start..start + 2 + end + 2];
if let Some(value) = subs.get(key) {
result.push_str(value);
} else {
result.push_str(key);
}
remaining = &remaining[start + 2 + end + 2..];
} else {
result.push_str(&remaining[start..]);
return result;
}
}
result.push_str(remaining);
result
}
fn clone_and_sub_json(text: &str, subs: &HashMap<String, String>) -> String {
if subs.is_empty() || !text.contains("{{") {
return text.to_string();
}
let mut result = String::with_capacity(text.len());
let mut remaining = text;
while let Some(quote_start) = remaining.find('"') {
result.push_str(&remaining[..quote_start + 1]);
remaining = &remaining[quote_start + 1..];
loop {
match (remaining.find("{{"), remaining.find('"')) {
(Some(hb_start), Some(q_end)) if hb_start < q_end => {
if let Some(hb_end) = remaining[hb_start + 2..].find("}}") {
let key = &remaining[hb_start..hb_start + 2 + hb_end + 2];
result.push_str(&remaining[..hb_start]);
if let Some(value) = subs.get(key) {
result.push_str(&value.replace('"', "\\\""));
} else {
result.push_str(key);
}
remaining = &remaining[hb_start + 2 + hb_end + 2..];
} else {
result.push_str(remaining);
return result;
}
}
_ => {
if let Some(q_end) = remaining.find('"') {
result.push_str(&remaining[..q_end + 1]);
remaining = &remaining[q_end + 1..];
} else {
result.push_str(remaining);
return result;
}
break;
}
}
}
}
result.push_str(remaining);
let text2 = result;
let mut result2 = String::with_capacity(text2.len());
let mut remaining2 = text2.as_str();
while let Some(start) = remaining2.find("{{") {
result2.push_str(&remaining2[..start]);
if let Some(end) = remaining2[start + 2..].find("}}") {
let key = &remaining2[start..start + 2 + end + 2];
if let Some(value) = subs.get(key) {
result2.push_str(value);
} else {
result2.push_str(key);
}
remaining2 = &remaining2[start + 2 + end + 2..];
} else {
result2.push_str(&remaining2[start..]);
return result2;
}
}
result2.push_str(remaining2);
result2
}
static PORT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r".+:(\d{1,5})(?:\?.*)?$").unwrap());
static FRAMEWORK_SNAPSHOT: LazyLock<Vec<u8>> = LazyLock::new(|| {
V8_INIT.call_once(|| {
let platform = v8::new_unprotected_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
});
let mut isolate = v8::Isolate::snapshot_creator(None, None);
{
v8::scope!(let scope, &mut isolate);
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
let framework_code = include_str!(concat!(env!("OUT_DIR"), "/framework.min.js"));
let v8_code = v8::String::new(scope, framework_code).unwrap();
let script = v8::Script::compile(scope, v8_code, None).unwrap();
script.run(scope).unwrap();
scope.set_default_context(context);
}
let startup_data = isolate
.create_blob(v8::FunctionCodeHandling::Keep)
.expect("Failed to create V8 snapshot for test framework");
startup_data.to_vec()
});
pub trait ApicizeRunner {
fn run(
&self,
request_ids: Vec<String>,
) -> impl std::future::Future<Output = Vec<Result<ApicizeResult, ApicizeError>>> + Send;
}
pub struct TestRunnerContext {
workspace: Workspace,
cancellation: CancellationToken,
executing_request_or_group_id: String,
value_cache: Mutex<VariableCache>,
tests_started: Instant,
single_run_no_timeout: bool,
enable_trace: bool,
generate_curl: bool,
execution_counter_callback: Option<Box<ExecutionCounterCallback>>,
}
pub struct ExecutionProgress {
pub id: String,
pub exec_ctr: i8,
pub row_number: Option<usize>,
pub run_number: Option<usize>,
}
type ExecutionCounterCallback = dyn Fn(&ExecutionProgress) + Send + Sync;
pub struct TestRunnerContextInit<'a> {
pub workspace: Workspace,
pub cancellation: Option<CancellationToken>,
pub executing_request_or_group_id: &'a str,
pub single_run_no_timeout: bool,
pub allowed_data_path: &'a Option<PathBuf>,
pub enable_trace: bool,
pub generate_curl: bool,
pub execution_counter_callback: Option<Box<ExecutionCounterCallback>>,
}
impl TestRunnerContext {
pub fn new(init: TestRunnerContextInit) -> Self {
V8_INIT.call_once(|| {
let platform = v8::new_unprotected_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
});
TestRunnerContext {
workspace: init.workspace,
cancellation: init.cancellation.unwrap_or_default(),
executing_request_or_group_id: init.executing_request_or_group_id.to_string(),
value_cache: Mutex::new(VariableCache::new(init.allowed_data_path)),
tests_started: Instant::now(),
single_run_no_timeout: init.single_run_no_timeout,
enable_trace: init.enable_trace,
generate_curl: init.generate_curl,
execution_counter_callback: init.execution_counter_callback,
}
}
pub fn ellapsed_in_ms(&self) -> u128 {
self.tests_started.elapsed().as_millis()
}
pub fn get_executing_request_or_group_id(&self) -> &str {
self.executing_request_or_group_id.as_str()
}
pub fn get_request_entry(
&self,
request_or_group_id: &str,
) -> Result<&RequestEntry, ApicizeError> {
match self.workspace.requests.entities.get(request_or_group_id) {
Some(entry) => Ok(entry),
None => Err(ApicizeError::InvalidId {
description: format!("Invalid Request or Group ID {request_or_group_id}"),
}),
}
}
pub fn get_request(&self, request_id: &str) -> Result<&Request, ApicizeError> {
match self.workspace.requests.entities.get(request_id) {
Some(RequestEntry::Request(request)) => Ok(request),
_ => Err(ApicizeError::InvalidId {
description: format!("Invalid Request ID {request_id}"),
}),
}
}
pub fn get_request_key(
&self,
request_or_group_id: &str,
) -> Result<Option<String>, ApicizeError> {
match self.workspace.requests.entities.get(request_or_group_id) {
Some(RequestEntry::Request(request)) => {
if let Some(key) = &request.key {
Ok(Some(key.clone()))
} else if let Some(parent_id) =
self.workspace.requests.parent_ids.get(request_or_group_id)
{
self.get_request_key(parent_id)
} else {
Ok(None)
}
}
Some(RequestEntry::Group(group)) => {
if let Some(key) = &group.key {
Ok(Some(key.clone()))
} else if let Some(parent_id) =
self.workspace.requests.parent_ids.get(request_or_group_id)
{
self.get_request_key(parent_id)
} else {
Ok(None)
}
}
_ => Err(ApicizeError::InvalidId {
description: format!("Invalid Request ID {request_or_group_id}"),
}),
}
}
pub fn get_group(&self, group_id: &str) -> Result<&RequestGroup, ApicizeError> {
match self.workspace.requests.entities.get(group_id) {
Some(RequestEntry::Group(group)) => Ok(group),
_ => Err(ApicizeError::InvalidId {
description: format!("Invalid Group ID {group_id}"),
}),
}
}
pub fn get_group_children(&self, group_id: &str) -> &[String] {
match self.workspace.requests.child_ids.get(group_id) {
Some(child_ids) => child_ids.as_slice(),
None => &[],
}
}
}
impl ApicizeRunner for Arc<TestRunnerContext> {
async fn run(
&self,
request_entry_ids: Vec<String>,
) -> Vec<Result<ApicizeResult, ApicizeError>> {
let mut results =
Vec::<Result<ApicizeResult, ApicizeError>>::with_capacity(request_entry_ids.len());
for request_entry_id in request_entry_ids {
let result = Box::pin(run_request_entry(
self.clone(),
request_entry_id,
Arc::new(RequestExecutionParameters::default()),
Arc::new(RequestExecutionState::default()),
true,
))
.await;
match result {
Ok(Some(r)) => {
results.push(Ok(r));
}
Ok(None) => {}
Err(err) => results.push(Err(err)),
}
}
results
}
}
async fn run_request_entry(
context: Arc<TestRunnerContext>,
request_or_group_id: String,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
force_run: bool,
) -> Result<Option<ApicizeResult>, ApicizeError> {
let entry = context.get_request_entry(&request_or_group_id)?;
let new_params =
context
.workspace
.retrieve_request_parameters(entry, &context.value_cache, ¶ms)?;
let row_number = state.row_number;
if let Some(execution_counter) = &context.execution_counter_callback {
execution_counter(&ExecutionProgress {
id: request_or_group_id.clone(),
exec_ctr: 1,
row_number,
run_number: None,
});
}
let result = match entry {
RequestEntry::Request(request) => {
if request.disabled && !force_run {
Ok(None)
} else {
match run_request(
context.clone(),
&request_or_group_id,
Arc::new(new_params),
state,
)
.await?
{
Some(result) => Ok(Some(ApicizeResult::Request(result))),
None => Ok(None),
}
}
}
RequestEntry::Group(group) => {
if group.disabled && !force_run {
Ok(None)
} else {
match run_group(
context.clone(),
&request_or_group_id,
Arc::new(new_params),
state,
)
.await?
{
Some(result) => Ok(Some(ApicizeResult::Group(result))),
None => Ok(None),
}
}
}
};
if let Some(execution_counter) = &context.execution_counter_callback {
execution_counter(&ExecutionProgress {
id: request_or_group_id.clone(),
exec_ctr: -1,
row_number,
run_number: None,
});
}
result
}
async fn run_request(
context: Arc<TestRunnerContext>,
request_id: &str,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
) -> Result<Option<Box<ApicizeRequestResult>>, ApicizeError> {
let key = context.get_request_key(request_id)?;
let executed_at = context.ellapsed_in_ms();
let request = context.get_request(request_id)?;
let multi_run = request.runs > 1 && (!context.single_run_no_timeout);
if request.runs < 1 {
return Ok(None);
}
let (content, data_context, tallies, logs) = if params.data_set.is_some() && state.row.is_none()
{
let mut rows =
run_request_rows(context.clone(), request_id, params.clone(), state.clone()).await?;
let data_context = rows.generate_data_context();
let tallies = rows.get_tallies();
if rows.len() == 1 {
let row = rows.remove(0);
match row.results {
ApicizeRequestResultRowContent::Runs(runs) => {
let data_context = runs.generate_data_context();
let tallies = runs.get_tallies();
(
ApicizeRequestResultContent::Runs { runs },
data_context,
tallies,
None,
)
}
ApicizeRequestResultRowContent::Execution(execution) => {
let data_context = execution.generate_data_context();
let tallies = execution.get_tallies();
let logs = execution.logs.clone();
(
ApicizeRequestResultContent::Execution { execution },
data_context,
tallies,
logs,
)
}
}
} else {
(
ApicizeRequestResultContent::Rows { rows },
data_context,
tallies,
None,
)
}
} else if multi_run {
let runs = run_request_runs(
context.clone(),
request_id,
params.clone(),
state.clone(),
None,
)
.await?;
let data_context = runs.generate_data_context();
let tallies = runs.get_tallies();
(
ApicizeRequestResultContent::Runs { runs },
data_context,
tallies,
None,
)
} else {
let execution =
dispatch_request_and_test(context.clone(), request_id.to_string(), params, state)
.await?;
let data_context = execution.generate_data_context();
let tallies = execution.get_tallies();
let logs = execution.logs.clone();
(
ApicizeRequestResultContent::Execution {
execution: Box::new(execution),
},
data_context,
tallies,
logs,
)
};
Ok(Some(Box::new(ApicizeRequestResult {
id: request.id.clone(),
name: request.get_title(),
key,
tag: None,
url: None,
executed_at,
duration: context.ellapsed_in_ms() - executed_at,
data_context,
content,
logs,
success: tallies.success,
request_success_count: tallies.request_success_count,
request_failure_count: tallies.request_failure_count,
request_error_count: tallies.request_error_count,
test_pass_count: tallies.test_pass_count,
test_fail_count: tallies.test_fail_count,
})))
}
async fn run_request_rows(
context: Arc<TestRunnerContext>,
request_id: &str,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
) -> Result<Vec<ApicizeRequestResultRow>, ApicizeError> {
let request = context.get_request(request_id)?;
let mut row_number = 1;
match params.data_set.as_ref() {
None => Err(ApicizeError::Error {
description: "run_request_rows called with no rows defined".to_string(),
}),
Some(data_set) => {
let mut rows = Vec::<ApicizeRequestResultRow>::with_capacity(data_set.data.len());
for row in &data_set.data {
let row_executed_at = context.ellapsed_in_ms();
let row_state = Arc::new(RequestExecutionState {
row: Some(Arc::new(row.clone())),
row_number: Some(row_number),
output_variables: state.output_variables.clone(),
});
if request.runs == 1 {
if let Some(execution_counter) = &context.execution_counter_callback {
execution_counter(&ExecutionProgress {
id: request_id.to_string(),
exec_ctr: 1,
row_number: Some(row_number),
run_number: None,
});
}
let execution = dispatch_request_and_test(
context.clone(),
request_id.to_string(),
params.clone(),
row_state.clone(),
)
.await?;
if let Some(execution_counter) = &context.execution_counter_callback {
execution_counter(&ExecutionProgress {
id: request_id.to_string(),
exec_ctr: -1,
row_number: Some(row_number),
run_number: None,
});
}
let data_context = DataContext {
scenario: params.variables.clone(),
data: row_state.row.clone(),
output: row_state.output_variables.clone(),
output_result: execution.output_variables.clone(),
};
let taliles = execution.get_tallies();
rows.push(ApicizeRequestResultRow {
row_number,
executed_at: row_executed_at,
duration: context.ellapsed_in_ms() - row_executed_at,
data_context,
results: ApicizeRequestResultRowContent::Execution(Box::new(execution)),
success: taliles.success,
request_success_count: taliles.request_success_count,
request_failure_count: taliles.request_failure_count,
request_error_count: taliles.request_error_count,
test_pass_count: taliles.test_pass_count,
test_fail_count: taliles.test_fail_count,
});
} else {
let runs = run_request_runs(
context.clone(),
request_id,
params.clone(),
state.clone(),
Some(row_number),
)
.await?;
let row_tallies = runs.get_tallies();
rows.push(ApicizeRequestResultRow {
row_number,
executed_at: row_executed_at,
duration: context.ellapsed_in_ms() - row_executed_at,
data_context: runs.generate_data_context(),
results: ApicizeRequestResultRowContent::Runs(runs),
success: row_tallies.success,
request_success_count: row_tallies.request_success_count,
request_failure_count: row_tallies.request_failure_count,
request_error_count: row_tallies.request_error_count,
test_pass_count: row_tallies.test_pass_count,
test_fail_count: row_tallies.test_fail_count,
});
}
row_number += 1;
}
Ok(rows)
}
}
}
async fn run_request_runs(
context: Arc<TestRunnerContext>,
request_id: &str,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
row_number: Option<usize>,
) -> Result<Vec<ApicizeRequestResultRun>, ApicizeError> {
let mut runs = Vec::<ApicizeRequestResultRun>::new();
let request = context.get_request(request_id)?;
let number_of_runs = if context.single_run_no_timeout {
1
} else {
request.runs
};
match request.multi_run_execution {
ExecutionConcurrency::Sequential => {
for run_number in 1..number_of_runs + 1 {
if let Some(execution_counter) = &context.execution_counter_callback {
execution_counter(&ExecutionProgress {
id: request_id.to_string(),
exec_ctr: 1,
row_number,
run_number: Some(run_number),
});
}
let run_executed_at = context.ellapsed_in_ms();
let execution = dispatch_request_and_test(
context.clone(),
request_id.to_string(),
params.clone(),
state.clone(),
)
.await?;
if let Some(execution_counter) = &context.execution_counter_callback {
execution_counter(&ExecutionProgress {
id: request_id.to_string(),
exec_ctr: -1,
row_number,
run_number: Some(run_number),
});
}
let success = execution.success;
let request_failure_count = if execution.test_fail_count > 0 { 1 } else { 0 };
let request_error_count = if execution.error.is_some() { 1 } else { 0 };
let request_success_count = if request_failure_count > 0 || request_error_count > 0
{
0
} else {
1
};
let test_pass_count = execution.test_pass_count;
let test_fail_count = execution.test_fail_count;
let run = ApicizeRequestResultRun {
run_number,
executed_at: run_executed_at,
duration: context.ellapsed_in_ms() - run_executed_at,
execution,
success,
request_success_count,
request_failure_count,
request_error_count,
test_pass_count,
test_fail_count,
};
runs.push(run);
}
}
ExecutionConcurrency::Concurrent => {
let runs_executed_at = context.ellapsed_in_ms();
let mut executing_runs: JoinSet<Result<ApicizeRequestResultRun, ApicizeError>> =
JoinSet::new();
for run_number in 1..number_of_runs + 1 {
let context = context.clone();
let request_id = request_id.to_string();
let params = params.clone();
let state = state.clone();
executing_runs.spawn(async move {
select! {
_ = context.cancellation.cancelled() => Err(ApicizeError::Cancelled),
result = dispatch_request_and_test(
context.clone(),
request_id,
params,
state,
) => {
match result {
Ok(execution) => {
let success = execution.success;
let request_failure_count = if execution.test_fail_count > 0 { 1 } else { 0 };
let request_error_count = if execution.error.is_some() { 1 } else { 0 };
let request_success_count = if request_failure_count > 0 || request_error_count > 0 { 0 } else { 1 };
let test_pass_count = execution.test_pass_count;
let test_fail_count = execution.test_fail_count;
Ok(ApicizeRequestResultRun {
run_number,
executed_at: runs_executed_at,
duration: context.ellapsed_in_ms() - runs_executed_at,
execution,
success,
request_success_count,
request_failure_count,
request_error_count,
test_pass_count,
test_fail_count,
})
},
Err(err) => {
Err(err)
}
}
}
}
});
}
runs = executing_runs.join_all().await.into_iter().try_fold(
vec![],
|mut unrolled, result| -> Result<Vec<ApicizeRequestResultRun>, ApicizeError> {
unrolled.push(result?);
Ok(unrolled)
},
)?;
runs.sort_by_key(|run| run.run_number);
}
}
Ok(runs)
}
async fn run_group(
context: Arc<TestRunnerContext>,
group_id: &str,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
) -> Result<Option<Box<ApicizeGroupResult>>, ApicizeError> {
let executed_at = context.ellapsed_in_ms();
let group = context.get_group(group_id)?;
let key = context.get_request_key(group_id)?;
let multi_run = group.runs > 1 && (!context.single_run_no_timeout);
if group.runs < 1 {
return Ok(None);
}
let child_ids = context.get_group_children(group_id);
let (use_state, logs) = if let Some(setup) = &group.setup
&& let Some(setup_response) = execute_request_test(
setup,
&None,
&None,
¶ms.variables,
&state.row,
&state.output_variables,
&context.tests_started,
)? {
(
Arc::new(RequestExecutionState {
row: state.row.clone(),
row_number: state.row_number,
output_variables: Some(Arc::new(setup_response.output.clone())),
}),
setup_response.logs,
)
} else {
(state, None)
};
let (content, data_context, tallies) = if params.data_set.is_some() && !child_ids.is_empty() {
let mut rows =
run_group_rows(context.clone(), group_id, params.clone(), use_state.clone()).await?;
if rows.len() == 1 {
let row = rows.remove(0);
match row.content {
ApicizeGroupResultRowContent::Runs { runs } => {
let data_context = runs.generate_data_context();
let tallies = runs.get_tallies();
(
ApicizeGroupResultContent::Runs { runs },
data_context,
tallies,
)
}
ApicizeGroupResultRowContent::Results { results } => {
let data_context = results.generate_data_context();
let tallies = results.get_tallies();
(
ApicizeGroupResultContent::Results { results },
data_context,
tallies,
)
}
}
} else {
let data_context = rows.generate_data_context();
let tallies = rows.get_tallies();
(
ApicizeGroupResultContent::Rows { rows },
data_context,
tallies,
)
}
} else if multi_run {
let runs =
run_group_runs(context.clone(), group_id, params.clone(), use_state.clone()).await?;
let data_context = runs.generate_data_context();
let tallies = runs.get_tallies();
(
ApicizeGroupResultContent::Runs { runs },
data_context,
tallies,
)
} else {
let results = run_group_children(
context.clone(),
child_ids,
params.clone(),
use_state.clone(),
&group.execution,
)
.await?;
let data_context = results.generate_data_context();
let tallies = results.get_tallies();
(
ApicizeGroupResultContent::Results { results },
data_context,
tallies,
)
};
Ok(Some(Box::new(ApicizeGroupResult {
id: group.id.clone(),
name: group.get_title(),
key,
tag: None,
executed_at,
duration: context.ellapsed_in_ms() - executed_at,
data_context,
content,
logs,
success: tallies.success,
request_success_count: tallies.request_success_count,
request_failure_count: tallies.request_failure_count,
request_error_count: tallies.request_error_count,
test_pass_count: tallies.test_pass_count,
test_fail_count: tallies.test_fail_count,
})))
}
#[async_recursion]
async fn run_group_children(
context: Arc<TestRunnerContext>,
child_ids: &[String],
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
concurrency: &ExecutionConcurrency,
) -> Result<Vec<ApicizeResult>, ApicizeError> {
if child_ids.is_empty() {
Ok(vec![])
} else {
match concurrency {
ExecutionConcurrency::Sequential => {
let mut results = Vec::<ApicizeResult>::with_capacity(child_ids.len());
let mut group_state = state.clone();
for child_id in child_ids {
let result = run_request_entry(
context.clone(),
child_id.clone(),
params.clone(),
group_state.clone(),
false,
)
.await?;
if let Some(r) = result {
let result_output_variables = &r.get_data_context().output_result;
if result_output_variables.is_some() {
group_state = Arc::new(RequestExecutionState {
row: state.row.clone(),
row_number: state.row_number,
output_variables: result_output_variables.clone(),
});
}
results.push(r);
}
}
Ok(results)
}
ExecutionConcurrency::Concurrent => {
let mut executing_children: JoinSet<Result<Option<ApicizeResult>, ApicizeError>> =
JoinSet::new();
for child_id in child_ids {
let context = context.clone();
let child_id = child_id.clone();
let params = params.clone();
let state = state.clone();
executing_children.spawn(async move {
select! {
_ = context.cancellation.cancelled() => Err(ApicizeError::Cancelled),
result = run_request_entry(
context.clone(),
child_id,
params,
state,
false,
) => {
result
}
}
});
}
let mut results = executing_children.join_all().await.into_iter().try_fold(
vec![],
|mut unrolled, result| {
match result {
Ok(Some(r)) => {
unrolled.push(r);
}
Ok(None) => {}
Err(err) => {
return Err(err);
}
}
Ok(unrolled)
},
)?;
results.sort_by(|a, b| {
let id1 = a.get_id();
let id2 = b.get_id();
let pos1 = &child_ids
.iter()
.position(|id| id == id1)
.unwrap_or(usize::MAX);
let pos2 = &child_ids
.iter()
.position(|id| id == id2)
.unwrap_or(usize::MAX);
pos1.cmp(pos2)
});
Ok(results)
}
}
}
}
async fn run_group_rows(
context: Arc<TestRunnerContext>,
group_id: &str,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
) -> Result<Vec<ApicizeGroupResultRow>, ApicizeError> {
let group = context.get_group(group_id)?;
let child_ids = context.get_group_children(group_id);
let empty_data = vec![];
let active_data = match ¶ms.data_set.as_ref() {
Some(d) => &d.data,
None => &empty_data,
};
if child_ids.is_empty() || active_data.is_empty() {
Ok(vec![])
} else {
let mut rows = Vec::<ApicizeGroupResultRow>::with_capacity(active_data.len());
let mut row_state = RequestExecutionState {
row: None,
row_number: None,
output_variables: state.output_variables.clone(),
};
for (row_number, row) in (1..).zip(active_data.iter()) {
let row_executed_at = context.ellapsed_in_ms();
row_state.row = Some(Arc::new(row.clone()));
row_state.row_number = Some(row_number);
let (content, tallies, data_context) = if group.runs == 1 {
let entries = run_group_children(
context.clone(),
child_ids,
params.clone(),
Arc::new(row_state.clone()),
&group.execution,
)
.await?;
let tallies = entries.get_tallies();
let data_context = entries.generate_data_context();
(
ApicizeGroupResultRowContent::Results { results: entries },
tallies,
data_context,
)
} else {
let runs = run_group_runs(
context.clone(),
group_id,
params.clone(),
Arc::new(row_state.clone()),
)
.await?;
let tallies = runs.get_tallies();
let data_context = runs.generate_data_context();
(
ApicizeGroupResultRowContent::Runs { runs },
tallies,
data_context,
)
};
if data_context.output_result.is_some() {
row_state.output_variables = data_context.output_result.clone();
}
rows.push(ApicizeGroupResultRow {
row_number,
executed_at: row_executed_at,
duration: context.ellapsed_in_ms() - row_executed_at,
data_context,
content,
success: tallies.success,
request_success_count: tallies.request_success_count,
request_failure_count: tallies.request_failure_count,
request_error_count: tallies.request_error_count,
test_pass_count: tallies.test_pass_count,
test_fail_count: tallies.test_fail_count,
});
}
Ok(rows)
}
}
async fn run_group_runs(
context: Arc<TestRunnerContext>,
group_id: &str,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
) -> Result<Vec<ApicizeGroupResultRun>, ApicizeError> {
let group = context.get_group(group_id)?;
let number_of_runs = if context.single_run_no_timeout {
1
} else {
group.runs
};
let child_ids = context.get_group_children(group_id);
let mut runs: Vec<ApicizeGroupResultRun> =
Vec::<ApicizeGroupResultRun>::with_capacity(group.runs);
match group.multi_run_execution {
ExecutionConcurrency::Sequential => {
for run_number in 1..number_of_runs + 1 {
let run_executed_at = context.ellapsed_in_ms();
let results = run_group_children(
context.clone(),
child_ids,
params.clone(),
state.clone(),
&group.execution,
)
.await?;
let tallies = results.get_tallies();
let data_context = results.generate_data_context();
runs.push(ApicizeGroupResultRun {
run_number,
executed_at: run_executed_at,
duration: context.ellapsed_in_ms() - run_executed_at,
data_context,
results,
success: tallies.success,
request_success_count: tallies.request_success_count,
request_failure_count: tallies.request_failure_count,
request_error_count: tallies.request_error_count,
test_pass_count: tallies.test_pass_count,
test_fail_count: tallies.test_fail_count,
});
}
}
ExecutionConcurrency::Concurrent => {
let mut executing_runs: JoinSet<Result<ApicizeGroupResultRun, ApicizeError>> =
JoinSet::new();
for run_number in 1..number_of_runs + 1 {
let context = context.clone();
let child_ids = child_ids.to_vec();
let params = params.clone();
let state = state.clone();
let execution = group.execution.clone();
let run_executed_at = context.ellapsed_in_ms();
executing_runs.spawn(async move {
select! {
_ = context.cancellation.cancelled() => Err(ApicizeError::Cancelled),
executed_results = run_group_children(
context.clone(),
&child_ids,
params,
state,
&execution,
) => {
match executed_results {
Ok(results) => {
let tallies = results.get_tallies();
Ok(ApicizeGroupResultRun {
run_number,
executed_at: run_executed_at,
duration: context.ellapsed_in_ms() - run_executed_at,
data_context: results.generate_data_context(),
results,
success: tallies.success,
request_success_count: tallies.request_success_count,
request_failure_count: tallies.request_failure_count,
request_error_count: tallies.request_error_count,
test_pass_count: tallies.test_pass_count,
test_fail_count: tallies.test_fail_count,
})
},
Err(err) => Err(err),
}
}
}
});
}
runs = executing_runs.join_all().await.into_iter().try_fold(
vec![],
|mut unrolled, result| -> Result<Vec<ApicizeGroupResultRun>, ApicizeError> {
unrolled.push(result?);
Ok(unrolled)
},
)?;
runs.sort_by_key(|run| run.run_number);
}
}
Ok(runs)
}
#[async_recursion]
async fn dispatch_request_and_test(
context: Arc<TestRunnerContext>,
request_id: String,
params: Arc<RequestExecutionParameters>,
state: Arc<RequestExecutionState>,
) -> Result<ApicizeExecution, ApicizeError> {
let mut execution_request: Option<ApicizeHttpRequest> = None;
let mut execution_response: Option<ApicizeHttpResponse> = None;
let mut output_variables: Option<Arc<Map<String, Value>>> = None;
let mut tests: Option<Vec<ApicizeTestBehavior>> = None;
let mut logs: Option<Vec<String>> = None;
let mut error: Option<ApicizeError> = None;
let name: String;
let key = context.get_request_key(&request_id)?;
let request = context.get_request(&request_id)?;
let mut merged_vars = match ¶ms.variables {
Some(vars) => (**vars).clone(),
None => Map::new(),
};
if let Some(r) = state.output_variables.as_ref() {
merged_vars.extend((**r).clone());
}
if let Some(r) = state.row.as_ref() {
merged_vars.extend((**r).clone());
}
let merged = match merged_vars.is_empty() {
true => None,
false => Some(Arc::new(merged_vars)),
};
let subs = match &merged {
Some(m) => {
let mut subs = HashMap::with_capacity(m.len());
for (name, value) in m.iter() {
let v = if let Some(s) = value.as_str() {
s.to_owned()
} else {
value.to_string()
};
subs.insert(format!("{{{{{name}}}}}"), v);
}
subs
}
None => HashMap::new(),
};
let mut test_count: usize = 0;
let mut test_fail_count: usize = 0;
let mut method: Option<String> = None;
let url: Option<String>;
let mut curl: Option<String> = None;
match dispatch_request(context.clone(), &request_id, ¶ms, &subs).await {
Ok((name_with_subs, url_called, http_request, http_response, _, curl_command)) => {
curl = curl_command;
name = name_with_subs;
url = Some(url_called);
method = Some(http_request.method.clone());
execution_request = Some(http_request);
execution_response = Some(http_response);
match &request.test {
Some(t) => {
match execute_request_test(
clone_and_sub(t, &subs).as_str(),
&execution_request,
&execution_response,
¶ms.variables,
&state.row,
&state.output_variables,
&context.tests_started,
) {
Ok(test_response) => {
(tests, output_variables, logs) = match test_response {
Some(response) => {
let output = if response.output.is_empty() {
None
} else {
Some(Arc::new(response.output))
};
let mut behaviors = Vec::<ApicizeTestBehavior>::new();
flatten_test_results(&response.results, &mut behaviors, &[]);
let test_results = if behaviors.is_empty() {
None
} else {
for b in &behaviors {
test_count += 1;
if !b.success {
test_fail_count += 1;
}
}
Some(behaviors)
};
(test_results, output, response.logs)
}
None => (None, None, None),
};
}
Err(err) => {
error = Some(err);
}
}
}
None => {
tests = None;
output_variables = None;
}
}
}
Err(err) => {
name = request.get_name().to_string();
url = None;
error = Some(err);
}
}
if let Some(ApicizeError::Cancelled) = error {
return Err(ApicizeError::Cancelled);
}
let success = error.is_none() && (test_count == 0 || test_fail_count == 0);
Ok(ApicizeExecution {
name,
key,
method,
url,
curl,
test_context: ApicizeExecutionTestContext {
merged,
scenario: params.variables.clone(),
output: state.output_variables.clone(),
data: state.row.clone(),
request: execution_request,
response: execution_response,
},
output_variables,
tests,
logs,
error,
success,
test_pass_count: test_count - test_fail_count,
test_fail_count,
})
}
async fn dispatch_request(
context: Arc<TestRunnerContext>,
request_id: &str,
params: &RequestExecutionParameters,
subs: &HashMap<String, String>,
) -> Result<
(
String,
String,
ApicizeHttpRequest,
ApicizeHttpResponse,
Option<Map<String, Value>>,
Option<String>,
),
ApicizeError,
> {
let request = context.get_request(request_id)?;
let method = match &request.method {
Some(RequestMethod::Get) => reqwest::Method::GET,
Some(RequestMethod::Post) => reqwest::Method::POST,
Some(RequestMethod::Patch) => reqwest::Method::PATCH,
Some(RequestMethod::Put) => reqwest::Method::PUT,
Some(RequestMethod::Delete) => reqwest::Method::DELETE,
Some(RequestMethod::Head) => reqwest::Method::HEAD,
Some(RequestMethod::Options) => reqwest::Method::OPTIONS,
None => reqwest::Method::GET,
};
let timeout = if context.single_run_no_timeout {
None
} else if let Some(t) = request.timeout {
if t == 0 {
None
} else {
Some(Duration::from_millis(t as u64))
}
} else {
Some(Duration::from_secs(30))
};
let mut reqwest_builder = Client::builder()
.http2_keep_alive_while_idle(request.keep_alive)
.danger_accept_invalid_certs(request.accept_invalid_certs)
.redirect(if request.number_of_redirects == 0 {
Policy::none()
} else {
Policy::limited(request.number_of_redirects)
})
.connection_verbose(context.enable_trace);
if let Some(t) = timeout {
reqwest_builder = reqwest_builder.timeout(t);
} else {
let max = Duration::from_mins(5);
reqwest_builder = reqwest_builder
.connect_timeout(max)
.read_timeout(max)
.pool_idle_timeout(max);
#[cfg(target_os = "linux")]
{
reqwest_builder = reqwest_builder.tcp_user_timeout(max);
}
reqwest_builder = reqwest_builder.http2_keep_alive_timeout(max);
}
if let Some(certificate) = context
.workspace
.certificates
.get_optional(¶ms.certificate_id)
{
reqwest_builder = certificate.append_to_builder(reqwest_builder)?;
}
if let Some(proxy) = context.workspace.proxies.get_optional(¶ms.proxy_id) {
reqwest_builder = proxy.append_to_builder(reqwest_builder)?;
}
let builder_result = reqwest_builder.build();
let mut oauth2_token: Option<TokenResult> = None;
let name = clone_and_sub(&request.name, subs);
let client = builder_result.map_err(|err| ApicizeError::from_reqwest(err, None))?;
let mut url = clone_and_sub(request.url.as_str(), subs).trim().to_string();
if url.is_empty() {
return Err(ApicizeError::Http {
context: None,
description: "Missing URL".to_string(),
url: None,
});
}
if !(url.starts_with("https://") || url.starts_with("http://")) {
let mut https = false;
if let Some(result) = PORT_REGEX.captures(&url)
&& let Some(m) = result.get(1)
&& let Ok(port) = m.as_str().parse::<u32>()
{
https = (port % 1000) == 443;
}
if !https
&& let Ok(Ok(_)) = tokio::time::timeout(
Duration::from_secs(2),
tokio::net::TcpStream::connect(format!("{url}:443")),
)
.await
{
https = true;
}
url = format!("{}://{}", if https { "https" } else { "http" }, url);
}
let mut request_builder = client.request(method, &url);
let mut headers = match &request.headers {
Some(h) => {
let capacity = h.iter().filter(|nvp| nvp.disabled != Some(true)).count();
reqwest::header::HeaderMap::with_capacity(capacity)
}
None => reqwest::header::HeaderMap::new(),
};
if let Some(h) = &request.headers {
for nvp in h {
if nvp.disabled != Some(true) {
let name_str = clone_and_sub(&nvp.name, subs);
let value_str = clone_and_sub(&nvp.value, subs);
headers.insert(
reqwest::header::HeaderName::try_from(name_str).unwrap(),
reqwest::header::HeaderValue::try_from(value_str).unwrap(),
);
}
}
}
match context
.workspace
.authorizations
.get_optional(¶ms.authorization_id)
{
Some(Authorization::Cipher(ParameterCipher { id, .. })) => {
return Err(ApicizeError::Encryption {
description: format!("Unable to add encrypted Authorization {id}"),
});
}
Some(Authorization::Plain(plain)) => match plain.as_ref() {
AuthorizationPlain::Basic {
username, password, ..
} => {
request_builder = request_builder.basic_auth(
clone_and_sub(username, subs),
Some(clone_and_sub(password, subs)),
);
}
AuthorizationPlain::ApiKey { header, value, .. } => {
headers.append(
reqwest::header::HeaderName::try_from(clone_and_sub(header, subs)).unwrap(),
reqwest::header::HeaderValue::try_from(clone_and_sub(value, subs)).unwrap(),
);
}
AuthorizationPlain::OAuth2Client {
id,
access_token_url,
client_id,
client_secret,
audience,
scope,
send_credentials_in_body,
selected_certificate,
selected_proxy,
..
} => {
let sub_token_url = clone_and_sub(access_token_url.as_str(), subs);
let sub_client_id = clone_and_sub(client_id.as_str(), subs);
let sub_client_secret = clone_and_sub(client_secret.as_str(), subs);
let sub_scope = clone_and_sub(scope.as_str(), subs);
let sub_audience = clone_and_sub(audience.as_str(), subs);
match get_oauth2_client_credentials(
id.as_str(),
OAuth2ClientCredentialParameters {
token_url: sub_token_url.as_str(),
client_id: sub_client_id.as_str(),
client_secret: sub_client_secret.as_str(),
send_credentials_in_body: send_credentials_in_body.unwrap_or(false),
scope: sub_scope.as_str(),
audience: sub_audience.as_str(),
certificate: context
.workspace
.certificates
.get_optional(selected_certificate.get_id()),
proxy: context.workspace.proxies.get_optional(&selected_proxy.id),
enable_trace: context.enable_trace,
},
)
.await
{
Ok(token_result) => {
request_builder = request_builder.bearer_auth(token_result.token.clone());
oauth2_token = Some(token_result);
}
Err(err) => return Err(err),
}
}
AuthorizationPlain::OAuth2Pkce { id, .. } => {
match retrieve_oauth2_token_from_cache(id).await {
Some(t) => {
request_builder = request_builder.bearer_auth(t.access_token.clone());
}
None => {
return Err(ApicizeError::Error {
description: String::from("PKCE access token is not available"),
});
}
}
}
},
None => {}
}
if !headers.is_empty() {
request_builder = request_builder.headers(headers);
}
if let Some(q) = &request.query_string_params {
let mut query: Vec<(String, String)> = vec![];
for nvp in q {
if nvp.disabled != Some(true) {
query.push((
clone_and_sub(&nvp.name, subs),
clone_and_sub(&nvp.value, subs),
));
}
}
request_builder = request_builder.query(&query);
}
let mut request_body: Option<ApicizeBody>;
match &request.body {
Some(RequestBody::Text { data }) => {
let s = clone_and_sub(data, subs);
request_body = Some(ApicizeBody::Text {
text: "".to_string(),
});
request_builder = request_builder.body(Body::from(s.clone()));
}
Some(RequestBody::JSON { data, .. }) => {
let s = clone_and_sub_json(data, subs);
request_body = match serde_json::from_str::<Value>(&s) {
Ok(data) => Some(ApicizeBody::JSON {
text: "".to_string(),
data,
}),
Err(_) => Some(ApicizeBody::Text { text: s.clone() }),
};
request_builder = request_builder.body(Body::from(s));
}
Some(RequestBody::XML { data }) => {
let s = clone_and_sub(data, subs);
request_body = match to_json(data) {
Ok(data) => Some(ApicizeBody::XML {
text: "".to_string(),
data,
}),
Err(_) => Some(ApicizeBody::Text { text: s.clone() }),
};
request_builder = request_builder.body(Body::from(s));
}
Some(RequestBody::Form { data }) => {
let form_data = data
.iter()
.map(|pair| {
(
clone_and_sub(&pair.name, subs),
clone_and_sub(&pair.value, subs),
)
})
.collect::<HashMap<String, String>>();
request_body = Some(ApicizeBody::Form {
text: "".to_string(),
data: form_data.clone(),
});
request_builder = request_builder.form(&form_data);
}
Some(RequestBody::Raw { data }) => {
request_body = Some(ApicizeBody::Binary { data: data.clone() });
request_builder = request_builder.body(Body::from(data.clone()));
}
None => {
request_body = None;
}
}
let mut web_request = request_builder
.build()
.map_err(|err| ApicizeError::from_reqwest(err, None))?;
let request_url = web_request.url().to_string();
let request_headers = web_request
.headers()
.iter()
.map(|(h, v)| {
(
String::from(h.as_str()),
String::from(v.to_str().unwrap_or("(Header Contains Non-ASCII Data)")),
)
})
.collect::<HashMap<String, String>>();
let ref_body = web_request.body_mut();
if let Some(data) = ref_body {
let bytes = data.as_bytes().unwrap();
if !bytes.is_empty() {
let request_encoding = UTF_8;
let data = bytes.to_vec();
match request_body.as_mut() {
None => {}
Some(ApicizeBody::Binary { .. }) => {}
Some(ApicizeBody::Form { text, .. })
| Some(ApicizeBody::JSON { text, .. })
| Some(ApicizeBody::XML { text, .. })
| Some(ApicizeBody::Text { text, .. }) => {
let (decoded, _, malformed) = request_encoding.decode(&data);
*text = if malformed {
"Malformed UTF8".to_string()
} else {
decoded.to_string()
}
}
}
}
}
let curl_command = if context.generate_curl {
Some(generate_curl_command(
request.method.as_ref().unwrap_or(&RequestMethod::Get),
&request_url,
&request_headers,
&request_body,
context
.workspace
.certificates
.get_optional(¶ms.certificate_id),
context.workspace.proxies.get_optional(¶ms.proxy_id),
))
} else {
None
};
let client_response: Result<Response, ApicizeError> = select! {
_ = context.cancellation.cancelled() => Err(ApicizeError::Cancelled),
result = client.execute(web_request) => {
match result {
Ok(response) => Ok(response),
Err(error) => Err(ApicizeError::from_reqwest(error, None)),
}
}
};
match client_response {
Err(error) => Err(error),
Ok(response) => {
let response_headers = response.headers();
let mut may_have_json = false;
let headers = if response_headers.is_empty() {
None
} else {
Some(HashMap::from_iter(
response_headers
.iter()
.map(|(h, v)| {
let name = h.as_str();
let value = v.to_str().unwrap_or("(Header Contains Non-ASCII Data)");
if name.eq_ignore_ascii_case("content-type")
&& value.to_ascii_lowercase().contains("json")
{
may_have_json = true;
}
(name.to_string(), value.to_string())
})
.collect::<HashMap<String, String>>(),
))
};
let response_content_type = response_headers
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok());
let response_encoding_name = response_content_type
.as_ref()
.and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
.unwrap_or("utf-8");
let response_encoding =
Encoding::for_label(response_encoding_name.as_bytes()).unwrap_or(UTF_8);
let status = response.status();
let status_text = String::from(status.canonical_reason().unwrap_or("Unknown"));
match response.bytes().await {
Ok(bytes) => {
let mut output_variables: Option<Map<String, Value>> = None;
let response_body = if bytes.is_empty() {
None
} else {
let data = Vec::from(bytes.as_ref());
let (decoded, _, malformed) = response_encoding.decode(&data);
if malformed {
Some(ApicizeBody::Binary { data })
} else {
let text = decoded.into_owned();
if may_have_json {
if let Ok(parsed) = serde_json::from_str::<Value>(&text) {
if let Some(obj) = parsed.as_object() {
output_variables = Some(obj.clone());
}
Some(ApicizeBody::JSON { text, data: parsed })
} else {
Some(ApicizeBody::Text { text })
}
} else {
Some(ApicizeBody::Text { text })
}
}
};
let response = (
name,
url,
ApicizeHttpRequest {
url: request_url,
method: request
.method
.as_ref()
.unwrap_or(&RequestMethod::Get)
.as_str()
.to_string(),
headers: request_headers,
body: request_body,
},
ApicizeHttpResponse {
status: status.as_u16(),
status_text,
headers,
body: response_body,
oauth2_token,
},
output_variables,
curl_command,
);
Ok(response)
}
Err(err) => Err(ApicizeError::from_reqwest(err, None)),
}
}
}
}
fn shell_escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn generate_curl_command(
method: &RequestMethod,
url: &str,
headers: &HashMap<String, String>,
body: &Option<ApicizeBody>,
certificate: Option<&Certificate>,
proxy: Option<&Proxy>,
) -> String {
let mut parts = vec!["curl".to_string()];
let method_str = method.as_str();
if method_str != "GET" {
parts.push("-X".to_string());
parts.push(method_str.to_string());
}
let mut sorted_headers: Vec<_> = headers.iter().collect();
sorted_headers.sort_by_key(|(k, _)| k.to_lowercase());
for (name, value) in sorted_headers {
parts.push("-H".to_string());
parts.push(format!(
"\"{}: {}\"",
shell_escape(name),
shell_escape(value)
));
}
if let Some(body) = body {
match body {
ApicizeBody::Text { text, .. }
| ApicizeBody::JSON { text, .. }
| ApicizeBody::XML { text, .. } => {
if !text.is_empty() {
parts.push("-d".to_string());
parts.push(format!("\"{}\"", shell_escape(text)));
}
}
ApicizeBody::Form { data, .. } => {
for (key, value) in data {
parts.push("--data-urlencode".to_string());
parts.push(format!("\"{}={}\"", shell_escape(key), shell_escape(value)));
}
}
ApicizeBody::Binary { .. } => {
parts.push("--data-binary".to_string());
parts.push("\"<BINARY_DATA>\"".to_string());
}
}
}
if let Some(cert) = certificate {
match cert {
Certificate::Plain(plain) => match plain.as_ref() {
crate::types::certificate::CertificatePlain::PKCS12 { name, .. } => {
parts.push("--cert".to_string());
parts.push(format!("\"{}.p12\"", shell_escape(name)));
parts.push("--cert-type".to_string());
parts.push("P12".to_string());
}
crate::types::certificate::CertificatePlain::PKCS8PEM { name, .. } => {
parts.push("--cert".to_string());
parts.push(format!("\"{}.pem\"", shell_escape(name)));
parts.push("--key".to_string());
parts.push(format!("\"{}.key\"", shell_escape(name)));
}
crate::types::certificate::CertificatePlain::PEM { name, .. } => {
parts.push("--cert".to_string());
parts.push(format!("\"{}.pem\"", shell_escape(name)));
}
},
Certificate::Cipher(_) => {
parts.push("--cert".to_string());
parts.push("\"<ENCRYPTED_CERTIFICATE>\"".to_string());
}
}
}
if let Some(proxy) = proxy {
match proxy {
Proxy::Plain(plain) => {
parts.push("--proxy".to_string());
parts.push(format!("\"{}\"", shell_escape(&plain.url)));
}
Proxy::Cipher(_) => {
parts.push("--proxy".to_string());
parts.push("\"<ENCRYPTED_PROXY>\"".to_string());
}
}
}
parts.push(format!("\"{}\"", shell_escape(url)));
parts.join(" ")
}
fn set_v8_global(scope: &mut v8::ContextScope<v8::HandleScope>, name: &str, json: &str) {
let global = scope.get_current_context().global(scope);
let key = v8::String::new(scope, name).unwrap();
let val = v8::String::new(scope, json).unwrap();
global.set(scope, key.into(), val.into());
}
fn execute_request_test(
test: &str,
request: &Option<ApicizeHttpRequest>,
response: &Option<ApicizeHttpResponse>,
variables: &Option<Arc<Map<String, Value>>>,
data: &Option<Arc<Map<String, Value>>>,
output: &Option<Arc<Map<String, Value>>>,
tests_started: &Instant,
) -> Result<Option<ApicizeTestResponse>, ApicizeError> {
let snapshot_ref: &'static [u8] = &FRAMEWORK_SNAPSHOT;
let startup_data: v8::StartupData = snapshot_ref.into();
let params = v8::CreateParams::default().snapshot_blob(startup_data);
let isolate = &mut v8::Isolate::new(params);
v8::scope!(let scope, isolate);
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
set_v8_global(scope, "__request", &serde_json::to_string(request).unwrap());
set_v8_global(
scope,
"__response",
&serde_json::to_string(response).unwrap(),
);
set_v8_global(
scope,
"__variables",
&serde_json::to_string(&variables).unwrap(),
);
set_v8_global(scope, "__data", &serde_json::to_string(&data).unwrap());
set_v8_global(scope, "__output", &serde_json::to_string(&output).unwrap());
let test_offset = std::time::UNIX_EPOCH.elapsed().unwrap().as_millis()
- tests_started.elapsed().as_millis()
+ 1;
let init_code = format!(
"runTestSuite(JSON.parse(__request), JSON.parse(__response), JSON.parse(__variables), JSON.parse(__data), JSON.parse(__output), {}, () => {{{}}})",
test_offset, test,
);
let scope = std::pin::pin!(v8::TryCatch::new(scope));
let scope = scope.init();
let v8_code = v8::String::new(&scope, &init_code).unwrap();
let Some(script) = v8::Script::compile(&scope, v8_code, None) else {
let message = scope.message().unwrap();
let message = message.get(&scope).to_rust_string_lossy(&scope);
return Err(ApicizeError::from_failed_test(message));
};
let Some(value) = script.run(&scope) else {
let message = scope.message().unwrap();
let message = message.get(&scope).to_rust_string_lossy(&scope);
return Err(ApicizeError::from_failed_test(message));
};
let result = value.to_string(&scope);
let s = result.unwrap().to_rust_string_lossy(&scope);
let test_response: ApicizeTestResponse = serde_json::from_str(&s).unwrap();
Ok(Some(test_response))
}
fn flatten_test_results(
results: &Option<Vec<ApicizeTestResult>>,
behaviors: &mut Vec<ApicizeTestBehavior>,
parents: &[String],
) {
if let Some(r) = results {
for result in r {
match &result {
ApicizeTestResult::Scenario(scenario) => {
if scenario.children.is_some() {
let mut new_parents = Vec::with_capacity(parents.len() + 1);
new_parents.extend_from_slice(parents);
new_parents.push(scenario.name.clone());
flatten_test_results(&scenario.children, behaviors, &new_parents);
}
}
ApicizeTestResult::Behavior(behavior) => {
let mut name = Vec::with_capacity(parents.len() + 1);
name.extend_from_slice(parents);
name.push(behavior.name.clone());
behaviors.push(ApicizeTestBehavior {
name: name.join(" "),
tag: behavior.tag.clone(),
success: behavior.success,
error: behavior.error.clone(),
logs: behavior.logs.clone(),
});
}
}
}
}
}
pub fn cleanup_v8() {
unsafe {
v8::V8::dispose();
}
v8::V8::dispose_platform();
}