diff --git a/src/app/business_logic/request/http/send.rs b/src/app/business_logic/request/http/send.rs index 5e5f092..a11239e 100644 --- a/src/app/business_logic/request/http/send.rs +++ b/src/app/business_logic/request/http/send.rs @@ -157,7 +157,7 @@ pub async fn send_http_request(prepared_request: reqwest_middleware::RequestBuil /* POST-REQUEST SCRIPT */ - let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env)?; + let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env); drop(request); @@ -169,5 +169,66 @@ pub async fn send_http_request(prepared_request: reqwest_middleware::RequestBuil request.cancellation_token = CancellationToken::new(); } - return Ok(modified_response); -} \ No newline at end of file + return modified_response; +} + +#[cfg(test)] +mod tests { + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::sync::Arc; + use parking_lot::RwLock; + use crate::app::business_logic::request::http::send::send_http_request; + use crate::app::business_logic::request::send::RequestResponseError; + use crate::models::request::Request; + + /// Answers a single request with a small 200 response, then closes the connection + fn one_response_server() -> String { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buffer = [0; 4096]; + let mut received = Vec::new(); + + while !received.windows(4).any(|window| window == b"\r\n\r\n") { + let read = stream.read(&mut buffer).unwrap(); + + if read == 0 { + break; + } + + received.extend_from_slice(&buffer[..read]); + } + + stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok").unwrap(); + }); + + format!("http://{address}/") + } + + #[tokio::test] + async fn failing_post_request_script_does_not_leave_the_request_pending() { + let url = one_response_server(); + + let mut request = Request { + url: url.clone(), + ..Default::default() + }; + request.scripts.post_request_script = Some(String::from(r#"throw new Error("post-request script failure");"#)); + + let local_request = Arc::new(RwLock::new(request)); + let prepared_request = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build().get(&url); + + let response = send_http_request(prepared_request, local_request.clone(), &None).await; + + assert!(matches!(response, Err(RequestResponseError::PostRequestScript))); + + let request = local_request.read(); + assert!(!request.is_pending); + + let console_output = request.console_output.post_request_output.as_deref().unwrap_or_default(); + assert!(console_output.contains("post-request script failure"), "{console_output:?}"); + } +} diff --git a/src/app/business_logic/request/mqtt/send.rs b/src/app/business_logic/request/mqtt/send.rs index 08fe8f4..229ae86 100644 --- a/src/app/business_logic/request/mqtt/send.rs +++ b/src/app/business_logic/request/mqtt/send.rs @@ -180,7 +180,7 @@ pub async fn send_mqtt_request(prepared_request: PreparedMqttRequest, local_requ /* POST-REQUEST SCRIPT */ let request = local_request.read(); - let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env)?; + let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env); drop(request); let mut request = local_request.write(); @@ -189,6 +189,8 @@ pub async fn send_mqtt_request(prepared_request: PreparedMqttRequest, local_requ request.is_pending = false; request.cancellation_token = CancellationToken::new(); + let modified_response = modified_response?; + let mqtt_request = request.get_mqtt_request_mut().unwrap(); mqtt_request.messages = vec![]; @@ -385,11 +387,11 @@ fn error_response(error: String) -> RequestResponse { mod tests { use std::sync::Arc; use indexmap::IndexMap; - use parking_lot::RwLock; + use parking_lot::{Mutex, RwLock}; use crate::app::app::App; use crate::app::business_logic::request::mqtt::client::PreparedMqttRequest; - use crate::app::business_logic::request::mqtt::send::mqtt_publish; - use crate::app::business_logic::request::send::PrepareRequestError; + use crate::app::business_logic::request::mqtt::send::{mqtt_publish, send_mqtt_request}; + use crate::app::business_logic::request::send::{PrepareRequestError, RequestResponseError}; use crate::models::auth::auth::Auth; use crate::models::auth::basic::BasicAuth; use crate::models::auth::bearer_token::BearerToken; @@ -613,6 +615,28 @@ mod tests { assert!(mqtt_request.messages.is_empty()); } + #[tokio::test] + async fn failing_post_request_script_does_not_leave_the_request_pending() { + // Nothing listens on this port, so the connection is refused + let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + + let mut failing = request(&format!("mqtt://127.0.0.1:{port}"), |_| {}); + failing.scripts.post_request_script = Some(String::from(r#"throw new Error("post-request script failure");"#)); + + let prepared = prepare(&mut failing).unwrap(); + let local_request = Arc::new(RwLock::new(failing)); + + let response = send_mqtt_request(prepared, local_request.clone(), &None, Arc::new(Mutex::new(false))).await; + + assert!(matches!(response, Err(RequestResponseError::PostRequestScript))); + + let request = local_request.read(); + assert!(!request.is_pending); + + let console_output = request.console_output.post_request_output.as_deref().unwrap_or_default(); + assert!(console_output.contains("post-request script failure"), "{console_output:?}"); + } + #[test] fn publish_after_the_connection_ended_is_not_logged() { let (mut mqtt_request, receiver) = connected(MqttVersion::V3_1_1, 100); diff --git a/src/app/business_logic/request/send.rs b/src/app/business_logic/request/send.rs index 597397b..3623346 100644 --- a/src/app/business_logic/request/send.rs +++ b/src/app/business_logic/request/send.rs @@ -357,10 +357,11 @@ impl App<'_> { } } - pub fn handle_post_request_script(request: &Request, response: RequestResponse, env: &Option>>) -> anyhow::Result<(RequestResponse, Option), RequestResponseError> { + /// The console output is returned even when the script fails, so the error can be read in the console + pub fn handle_post_request_script(request: &Request, response: RequestResponse, env: &Option>>) -> (Result, Option) { match &request.scripts.post_request_script { None => { - Ok((response, None)) + (Ok(response), None) }, Some(post_request_script) => { let env_values = match &env { @@ -386,8 +387,8 @@ impl App<'_> { } match result_response { - None => Err(PostRequestScript), - Some(result_response) => Ok((result_response, Some(result_console_output))) + None => (Err(PostRequestScript), Some(result_console_output)), + Some(result_response) => (Ok(result_response), Some(result_console_output)) } } } diff --git a/src/app/business_logic/request/ws/send.rs b/src/app/business_logic/request/ws/send.rs index bd50758..ca57420 100644 --- a/src/app/business_logic/request/ws/send.rs +++ b/src/app/business_logic/request/ws/send.rs @@ -132,17 +132,19 @@ pub async fn send_ws_request(prepared_request: reqwest_middleware::RequestBuilde /* POST-REQUEST SCRIPT */ - let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env)?; + let (modified_response, post_request_output) = App::handle_post_request_script(&request, response, env); drop(request); - { + let modified_response = { let mut request = local_request.write(); request.console_output.post_request_output = post_request_output; request.is_pending = false; request.cancellation_token = CancellationToken::new(); + let modified_response = modified_response?; + let ws_request = request.get_ws_request_mut().unwrap(); ws_request.messages = vec![]; @@ -152,7 +154,9 @@ pub async fn send_ws_request(prepared_request: reqwest_middleware::RequestBuilde else { ws_request.is_connected = true; } - } + + modified_response + }; let local_request = local_request.clone(); let mut request = local_request.write(); @@ -217,4 +221,43 @@ pub async fn send_ws_request(prepared_request: reqwest_middleware::RequestBuilde }); return Ok(modified_response); -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use parking_lot::{Mutex, RwLock}; + use crate::app::business_logic::request::send::RequestResponseError; + use crate::app::business_logic::request::ws::send::send_ws_request; + use crate::models::protocol::protocol::Protocol; + use crate::models::protocol::ws::ws::WsRequest; + use crate::models::request::Request; + + #[tokio::test] + async fn failing_post_request_script_does_not_leave_the_request_pending() { + // Nothing listens on this port, so the connection is refused + let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + let url = format!("http://127.0.0.1:{port}/"); + + let mut request = Request { + url: url.clone(), + protocol: Protocol::WsRequest(WsRequest::default()), + ..Default::default() + }; + request.scripts.post_request_script = Some(String::from(r#"throw new Error("post-request script failure");"#)); + + let local_request = Arc::new(RwLock::new(request)); + let prepared_request = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()).build().get(&url); + + let response = send_ws_request(prepared_request, local_request.clone(), &None, Arc::new(Mutex::new(false))).await; + + assert!(matches!(response, Err(RequestResponseError::PostRequestScript))); + + let request = local_request.read(); + assert!(!request.is_pending); + assert!(!request.get_ws_request().unwrap().is_connected); + + let console_output = request.console_output.post_request_output.as_deref().unwrap_or_default(); + assert!(console_output.contains("post-request script failure"), "{console_output:?}"); + } +} diff --git a/src/cli/cli_logic/request/send.rs b/src/cli/cli_logic/request/send.rs index b0e8f4f..799be51 100644 --- a/src/cli/cli_logic/request/send.rs +++ b/src/cli/cli_logic/request/send.rs @@ -93,12 +93,23 @@ impl App<'_> { let local_env = self.get_selected_env_as_local(); let response = match (&protocol, prepared_request) { - (Protocol::HttpRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_http_request(prepared_request, local_request.clone(), &local_env).await?, - (Protocol::WsRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_ws_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await?, - (Protocol::MqttRequest(_), PreparedRequest::Mqtt(prepared_request)) => send_mqtt_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await?, + (Protocol::HttpRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_http_request(prepared_request, local_request.clone(), &local_env).await, + (Protocol::WsRequest(_), PreparedRequest::Reqwest(prepared_request)) => send_ws_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await, + (Protocol::MqttRequest(_), PreparedRequest::Mqtt(prepared_request)) => send_mqtt_request(prepared_request, local_request.clone(), &local_env, self.received_response.clone()).await, _ => unreachable!() }; + let response = match response { + Ok(response) => response, + Err(error) => { + if send_command.console && let Some(post_request_output) = &local_request.read().console_output.post_request_output { + println!("{}", post_request_output); + } + + return Err(anyhow!(error)); + } + }; + let request = local_request.read(); if send_command.status_code {