Merge pull request #202 from Orange-OpenSource/feature/add-real-request-sent

Return "real" HTTP request sent from HTTP client
This commit is contained in:
Fabrice Reix 2021-06-15 22:06:01 +02:00 committed by GitHub
commit 45a3ab480e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 3656 additions and 1780 deletions

File diff suppressed because one or more lines are too long

View File

@ -56,5 +56,5 @@ span.line:before {
color: dimgray;
}
</style></head><body>
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/predicate/error/type</span></span></div><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span><span class="line section-header">[Asserts]</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.status"</span> <span class="predicate-type">equals</span> <span class="string">"true"</span></span><span class="line"><span class="comment">#jsonpath "$.count" equals "0"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.count"</span> <span class="predicate-type">equals</span> <span class="number">0</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">equals</span> <span class="number">0</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.empty"</span> <span class="predicate-type">equals</span> <span class="number">0</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.number"</span> <span class="predicate-type">equals</span> <span class="number">1.1</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">startsWith</span> <span class="string">"hi"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">contains</span> <span class="string">"hi"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">matches</span> <span class="string">"hi"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">equals</span> <span class="number">1</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.toto"</span> <span class="predicate-type">exists</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> not <span class="predicate-type">exists</span></span></div></div></div>
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/predicate/error/type</span></span></div><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span><span class="line section-header">[Asserts]</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.status"</span> <span class="predicate-type">equals</span> <span class="string">"true"</span></span><span class="line"><span class="comment">#jsonpath "$.count" equals "0"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.count"</span> <span class="predicate-type">equals</span> <span class="number">0</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">equals</span> <span class="number">0</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.empty"</span> <span class="predicate-type">equals</span> <span class="number">0</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.number"</span> <span class="predicate-type">equals</span> <span class="number">1.1</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">startsWith</span> <span class="string">"hi"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">contains</span> <span class="string">"hi"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">matches</span> <span class="string">"hi"</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> <span class="predicate-type">equals</span> <span class="number">1</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.toto"</span> <span class="predicate-type">exists</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.message"</span> not <span class="predicate-type">exists</span></span><span class="line"><span class="query-type">jsonpath</span> <span class="string">"$.list"</span> <span class="predicate-type">equals</span> <span class="number">2</span></span></div></div></div>
</body></html>

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ use std::time::Instant;
use super::core::*;
use super::options::ClientOptions;
use super::request::*;
use super::request_spec::*;
use super::response::*;
use std::str::FromStr;
use url::Url;
@ -51,7 +52,7 @@ pub struct Client {
pub redirect_count: usize,
// unfortunately, follow-location feature from libcurl can not be used
// libcurl returns a single list of headers for the 2 responses
// hurl needs the return the headers only for the second (last) response)
// hurl needs to keep everything
}
impl Client {
@ -85,15 +86,50 @@ impl Client {
///
/// Execute an http request
///
pub fn execute(
pub fn execute_with_redirect(
&mut self,
request: &Request,
redirect_count: usize,
) -> Result<Response, HttpError> {
request: &RequestSpec,
) -> Result<Vec<(Request, Response)>, HttpError> {
let mut calls = vec![];
let mut request_spec = request.clone();
self.redirect_count = 0;
loop {
let (request, response) = self.execute(&request_spec)?;
calls.push((request, response.clone()));
if let Some(url) = self.get_follow_location(response.clone()) {
request_spec = RequestSpec {
method: Method::Get,
url,
headers: vec![],
querystring: vec![],
form: vec![],
multipart: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
content_type: None,
};
self.redirect_count += 1;
if let Some(max_redirect) = self.options.max_redirect {
if self.redirect_count > max_redirect {
return Err(HttpError::TooManyRedirect);
}
}
} else {
break;
}
}
Ok(calls)
}
///
/// Execute an http request
///
pub fn execute(&mut self, request: &RequestSpec) -> Result<(Request, Response), HttpError> {
// set handle attributes
// that have not been set or reset
self.handle.verbose(self.options.verbose).unwrap();
self.handle.verbose(true).unwrap();
self.handle.ssl_verify_host(!self.options.insecure).unwrap();
self.handle.ssl_verify_peer(!self.options.insecure).unwrap();
if let Some(proxy) = self.options.proxy.clone() {
@ -107,7 +143,8 @@ impl Client {
.connect_timeout(self.options.connect_timeout)
.unwrap();
self.set_url(&request.url, &request.querystring);
let url = self.generate_url(&request.url, &request.querystring);
self.handle.url(url.as_str()).unwrap();
self.set_method(&request.method);
self.set_cookies(&request.cookies);
@ -120,23 +157,8 @@ impl Client {
self.set_headers(&request);
self.handle
.debug_function(|info_type, data| match info_type {
// return all request headers (not one by one)
easy::InfoType::HeaderOut => {
let lines = split_lines(data);
for line in lines {
eprintln!("> {}", line);
}
}
easy::InfoType::HeaderIn => {
if let Some(s) = decode_header(data) {
eprint!("< {}", s);
}
}
_ => {}
})
.unwrap();
let verbose = self.options.verbose;
let mut request_headers: Vec<Header> = vec![];
let start = Instant::now();
let mut status_lines = vec![];
@ -149,7 +171,35 @@ impl Client {
.read_function(|buf| Ok(data.read(buf).unwrap_or(0)))
.unwrap();
}
transfer
.debug_function(|info_type, data| match info_type {
// return all request headers (not one by one)
easy::InfoType::HeaderOut => {
let mut lines = split_lines(data);
if verbose {
for line in lines.clone() {
eprintln!("> {}", line);
}
}
lines.pop().unwrap();
lines.remove(0); // method/url
for line in lines {
if let Some(header) = Header::parse(line) {
request_headers.push(header);
}
}
}
easy::InfoType::HeaderIn => {
if let Some(s) = decode_header(data) {
if verbose {
eprint!("< {}", s);
}
}
}
_ => {}
})
.unwrap();
transfer
.header_function(|h| {
if let Some(s) = decode_header(h) {
@ -196,46 +246,28 @@ impl Client {
Some(status_line) => self.parse_response_version(status_line.clone())?,
};
let headers = self.parse_response_headers(&headers);
if let Some(url) = self.get_follow_location(headers.clone()) {
let request = Request {
method: Method::Get,
url,
headers: vec![],
querystring: vec![],
form: vec![],
multipart: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
content_type: None,
};
let redirect_count = redirect_count + 1;
if let Some(max_redirect) = self.options.max_redirect {
if redirect_count > max_redirect {
return Err(HttpError::TooManyRedirect);
}
}
return self.execute(&request, redirect_count);
}
let duration = start.elapsed();
self.redirect_count = redirect_count;
self.handle.reset();
Ok(Response {
let request = Request {
url,
method: (&request.method).to_string(),
headers: request_headers,
};
let response = Response {
version,
status,
headers,
body,
duration,
})
};
Ok((request, response))
}
///
/// set url
/// generate url
///
fn set_url(&mut self, url: &str, params: &[Param]) {
fn generate_url(&mut self, url: &str, params: &[Param]) -> String {
let url = if params.is_empty() {
url.to_string()
} else {
@ -249,7 +281,7 @@ impl Client {
let s = self.encode_params(params);
format!("{}{}", url, s)
};
self.handle.url(url.as_str()).unwrap();
url
}
///
@ -272,7 +304,7 @@ impl Client {
///
/// set request headers
///
fn set_headers(&mut self, request: &Request) {
fn set_headers(&mut self, request: &RequestSpec) {
let mut list = easy::List::new();
for header in request.headers.clone() {
@ -426,17 +458,15 @@ impl Client {
/// 2. a 3xx response code
/// 3. a header Location
///
fn get_follow_location(&mut self, headers: Vec<Header>) -> Option<String> {
fn get_follow_location(&mut self, response: Response) -> Option<String> {
if !self.options.follow_location {
return None;
}
let response_code = self.handle.response_code().unwrap();
let response_code = response.status;
if !(300..400).contains(&response_code) {
return None;
}
let location = match get_header_values(headers, "Location".to_string()).get(0) {
let location = match get_header_values(response.headers, "Location".to_string()).get(0) {
None => return None,
Some(value) => value.clone(),
};
@ -490,7 +520,7 @@ impl Client {
///
/// return curl command-line for the http request run by the client
///
pub fn curl_command_line(&mut self, http_request: &Request) -> String {
pub fn curl_command_line(&mut self, http_request: &RequestSpec) -> String {
let mut arguments = vec!["curl".to_string()];
arguments.append(&mut http_request.curl_args(self.options.context_dir.clone()));
@ -514,7 +544,7 @@ impl Client {
///
/// return cookies from both cookies from the cookie storage and the request
///
pub fn all_cookies(cookie_storage: Vec<Cookie>, request: &Request) -> Vec<RequestCookie> {
pub fn all_cookies(cookie_storage: Vec<Cookie>, request: &RequestSpec) -> Vec<RequestCookie> {
let mut cookies = request.cookies.clone();
cookies.append(
&mut cookie_storage

View File

@ -37,6 +37,18 @@ pub struct Cookie {
pub http_only: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestCookie {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Param {
pub name: String,
pub value: String,
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
@ -60,6 +72,18 @@ impl fmt::Display for Cookie {
}
}
impl fmt::Display for RequestCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}", self.name, self.value)
}
}
impl fmt::Display for Param {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseCookieError {}

View File

@ -17,11 +17,12 @@
*/
pub use self::client::{Client, HttpError};
pub use self::core::{Cookie, Header};
pub use self::core::{Cookie, Header, Param, RequestCookie};
pub use self::options::ClientOptions;
pub use self::request::Request;
#[cfg(test)]
pub use self::request::tests::*;
pub use self::request::{Body, FileParam, Method, MultipartParam, Param, Request, RequestCookie};
pub use self::request_spec::tests::*;
pub use self::request_spec::{Body, FileParam, Method, MultipartParam, RequestSpec};
#[cfg(test)]
pub use self::response::tests::*;
pub use self::response::{Response, Version};
@ -30,4 +31,5 @@ mod client;
mod core;
mod options;
mod request;
mod request_spec;
mod response;

View File

@ -16,564 +16,170 @@
*
*/
use core::fmt;
use super::core::*;
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
pub method: Method,
pub url: String,
pub method: String,
pub headers: Vec<Header>,
pub querystring: Vec<Param>,
pub form: Vec<Param>,
pub multipart: Vec<MultipartParam>,
pub cookies: Vec<RequestCookie>,
pub body: Body,
pub content_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Param {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultipartParam {
Param(Param),
FileParam(FileParam),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileParam {
pub name: String,
pub filename: String,
pub data: Vec<u8>,
pub content_type: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestCookie {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Body {
Text(String),
Binary(Vec<u8>),
File(Vec<u8>, String),
}
impl Body {
pub fn bytes(&self) -> Vec<u8> {
match self {
Body::Text(s) => s.as_bytes().to_vec(),
Body::Binary(bs) => bs.clone(),
Body::File(bs, _) => bs.clone(),
}
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH",
};
write!(f, "{}", value)
}
}
impl fmt::Display for Param {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
impl fmt::Display for MultipartParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MultipartParam::Param(param) => write!(f, "{}", param.to_string()),
MultipartParam::FileParam(param) => write!(f, "{}", param.to_string()),
}
}
}
impl fmt::Display for FileParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}: file,{}; {}",
self.name, self.filename, self.content_type
)
}
}
impl fmt::Display for RequestCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}", self.name, self.value)
}
}
impl Request {
///
/// return request as curl arguments
/// It does not contain the requests cookies (they will be accessed from the client)
///
pub fn curl_args(&self, context_dir: String) -> Vec<String> {
let querystring = if self.querystring.is_empty() {
"".to_string()
} else {
let params = self
.querystring
.iter()
.map(|p| p.curl_arg_escape())
.collect::<Vec<String>>();
params.join("&")
};
let url = if querystring.as_str() == "" {
self.url.to_string()
} else if self.url.to_string().contains('?') {
format!("{}&{}", self.url.to_string(), querystring)
} else {
format!("{}?{}", self.url.to_string(), querystring)
};
let mut arguments = vec![format!("'{}'", url)];
let data =
!self.multipart.is_empty() || !self.form.is_empty() || !self.body.bytes().is_empty();
arguments.append(&mut self.method.curl_args(data));
for header in self.headers.clone() {
arguments.append(&mut header.curl_args());
pub fn query_string_params(self) -> Vec<Param> {
let u = Url::parse(self.url.as_str()).expect("valid url");
let mut params = vec![];
for (name, value) in u.query_pairs() {
let param = Param {
name: name.to_string(),
value: value.to_string(),
};
params.push(param);
}
params
}
let has_explicit_content_type = self
.headers
pub fn cookies(self) -> Vec<RequestCookie> {
self.headers
.iter()
.map(|h| h.name.clone())
.any(|n| n.as_str() == "Content-Type");
if !has_explicit_content_type {
if let Some(content_type) = self.content_type.clone() {
if content_type.as_str() != "application/x-www-form-urlencoded"
&& content_type.as_str() != "multipart/form-data"
{
arguments.push("-H".to_string());
arguments.push(format!("'Content-Type: {}'", content_type));
}
} else if !self.body.bytes().is_empty() {
match self.body.clone() {
Body::Text(_) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type:'".to_string())
}
Body::Binary(_) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type: application/octet-stream'".to_string())
}
Body::File(_, _) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type:'".to_string())
}
}
}
}
for param in self.form.clone() {
arguments.push("--data".to_string());
arguments.push(format!("'{}'", param.curl_arg_escape()));
}
for param in self.multipart.clone() {
arguments.push("-F".to_string());
arguments.push(format!("'{}'", param.curl_arg(context_dir.clone())));
}
if !self.body.bytes().is_empty() {
arguments.push("--data".to_string());
match self.body.clone() {
Body::Text(s) => {
let prefix = if s.contains('\n') { "$" } else { "" };
arguments.push(format!("{}'{}'", prefix, s.replace("\n", "\\n")))
}
Body::Binary(bytes) => arguments.push(format!("$'{}'", encode_bytes(bytes))),
Body::File(_, filename) => {
let prefix = if context_dir.as_str() == "." {
"".to_string()
} else {
format!("{}/", context_dir)
};
arguments.push(format!("'@{}{}'", prefix, filename))
}
}
}
arguments
.filter(|h| h.name.as_str() == "Cookie")
.flat_map(|h| parse_cookies(h.value.as_str().trim()))
.collect()
}
}
fn encode_byte(b: u8) -> String {
format!("\\x{:02x}", b)
fn parse_cookies(s: &str) -> Vec<RequestCookie> {
s.split(';').map(|t| parse_cookie(t.trim())).collect()
}
fn encode_bytes(b: Vec<u8>) -> String {
b.iter().map(|b| encode_byte(*b)).collect()
}
impl Method {
pub fn curl_args(&self, data: bool) -> Vec<String> {
match self {
Method::Get => {
if data {
vec!["-X".to_string(), "GET".to_string()]
} else {
vec![]
}
}
Method::Head => vec!["-X".to_string(), "HEAD".to_string()],
Method::Post => {
if data {
vec![]
} else {
vec!["-X".to_string(), "POST".to_string()]
}
}
Method::Put => vec!["-X".to_string(), "PUT".to_string()],
Method::Delete => vec!["-X".to_string(), "DELETE".to_string()],
Method::Connect => vec!["-X".to_string(), "CONNECT".to_string()],
Method::Options => vec!["-X".to_string(), "OPTIONS".to_string()],
Method::Trace => vec!["-X".to_string(), "TRACE".to_string()],
Method::Patch => vec!["-X".to_string(), "PATCH".to_string()],
}
}
}
impl Header {
pub fn curl_args(&self) -> Vec<String> {
let name = self.name.clone();
let value = self.value.clone();
vec![
"-H".to_string(),
encode_value(format!("{}: {}", name, value)),
]
}
}
impl Param {
pub fn curl_arg_escape(&self) -> String {
let name = self.name.clone();
let value = escape_url(self.value.clone());
format!("{}={}", name, value)
}
pub fn curl_arg(&self) -> String {
let name = self.name.clone();
let value = self.value.clone();
format!("{}={}", name, value)
}
}
impl MultipartParam {
pub fn curl_arg(&self, context_dir: String) -> String {
match self {
MultipartParam::Param(param) => param.curl_arg(),
MultipartParam::FileParam(FileParam {
name,
filename,
content_type,
..
}) => {
let prefix = if context_dir.as_str() == "." {
"".to_string()
} else {
format!("{}/", context_dir)
};
let value = format!("@{}{};type={}", prefix, filename, content_type);
format!("{}={}", name, value)
}
}
}
}
fn escape_single_quote(s: String) -> String {
s.chars()
.map(|c| {
if c == '\'' {
"\\'".to_string()
} else {
c.to_string()
}
})
.collect::<Vec<String>>()
.join("")
}
fn escape_url(s: String) -> String {
percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
}
// special encoding for the shell
// $'...'
fn encode_value(s: String) -> String {
if s.contains('\'') {
let s = escape_single_quote(s);
format!("$'{}'", s)
} else {
format!("'{}'", s)
fn parse_cookie(s: &str) -> RequestCookie {
match s.find('=') {
Some(i) => RequestCookie {
name: s.split_at(i).0.to_string(),
value: s.split_at(i + 1).1.to_string(),
},
None => RequestCookie {
name: s.to_string(),
value: "".to_string(),
},
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::http::RequestCookie;
pub fn hello_http_request() -> Request {
pub fn hello_request() -> Request {
Request {
method: Method::Get,
method: "GET".to_string(),
url: "http://localhost:8000/hello".to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn custom_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost/custom".to_string(),
querystring: vec![],
headers: vec![
Header {
name: String::from("User-Agent"),
value: String::from("iPhone"),
name: "Host".to_string(),
value: "localhost:8000".to_string(),
},
Header {
name: String::from("Foo"),
value: String::from("Bar"),
name: "Accept".to_string(),
value: "*/*".to_string(),
},
Header {
name: "User-Agent".to_string(),
value: "hurl/1.0".to_string(),
},
],
cookies: vec![
RequestCookie {
name: String::from("theme"),
value: String::from("light"),
},
RequestCookie {
name: String::from("sessionToken"),
value: String::from("abc123"),
},
],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn query_http_request() -> Request {
pub fn query_string_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost:8000/querystring-params".to_string(),
querystring: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
method: "GET".to_string(),
url: "http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3".to_string(),
headers: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn form_http_request() -> Request {
pub fn cookies_request() -> Request {
Request {
method: Method::Post,
url: "http://localhost/form-params".to_string(),
querystring: vec![],
method: "GET".to_string(),
url: "http://localhost:8000/cookies".to_string(),
headers: vec![Header {
name: String::from("Content-Type"),
value: String::from("application/x-www-form-urlencoded"),
name: "Cookie".to_string(),
value: "cookie1=value1; cookie2=value2".to_string(),
}],
cookies: vec![],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
content_type: Some("multipart/form-data".to_string()),
}
}
#[test]
fn test_encode_byte() {
assert_eq!(encode_byte(1), "\\x01".to_string());
assert_eq!(encode_byte(32), "\\x20".to_string());
fn test_query_string() {
assert!(hello_request().query_string_params().is_empty());
assert_eq!(
query_string_request().query_string_params(),
vec![
Param {
name: "param1".to_string(),
value: "value1".to_string()
},
Param {
name: "param2".to_string(),
value: "".to_string()
},
Param {
name: "param3".to_string(),
value: "a=b".to_string()
},
Param {
name: "param4".to_string(),
value: "1,2,3".to_string()
}
]
)
}
#[test]
fn method_curl_args() {
assert!(Method::Get.curl_args(false).is_empty());
fn test_cookies() {
assert!(hello_request().cookies().is_empty());
assert_eq!(
Method::Get.curl_args(true),
vec!["-X".to_string(), "GET".to_string()]
);
assert_eq!(
Method::Post.curl_args(false),
vec!["-X".to_string(), "POST".to_string()]
);
assert!(Method::Post.curl_args(true).is_empty());
assert_eq!(
Method::Put.curl_args(false),
vec!["-X".to_string(), "PUT".to_string()]
);
assert_eq!(
Method::Put.curl_args(true),
vec!["-X".to_string(), "PUT".to_string()]
);
cookies_request().cookies(),
vec![
RequestCookie {
name: "cookie1".to_string(),
value: "value1".to_string()
},
RequestCookie {
name: "cookie2".to_string(),
value: "value2".to_string()
}
]
)
}
#[test]
fn header_curl_args() {
fn test_parse_cookies() {
assert_eq!(
Header {
name: "Host".to_string(),
value: "example.com".to_string()
}
.curl_args(),
vec!["-H".to_string(), "'Host: example.com'".to_string()]
);
assert_eq!(
Header {
name: "If-Match".to_string(),
value: "\"e0023aa4e\"".to_string()
}
.curl_args(),
vec!["-H".to_string(), "'If-Match: \"e0023aa4e\"'".to_string()]
);
parse_cookies("cookie1=value1; cookie2=value2"),
vec![
RequestCookie {
name: "cookie1".to_string(),
value: "value1".to_string()
},
RequestCookie {
name: "cookie2".to_string(),
value: "value2".to_string()
}
]
)
}
#[test]
fn param_curl_args() {
fn test_parse_cookie() {
assert_eq!(
Param {
name: "param1".to_string(),
parse_cookie("cookie1=value1"),
RequestCookie {
name: "cookie1".to_string(),
value: "value1".to_string()
}
.curl_arg(),
"param1=value1".to_string()
);
assert_eq!(
Param {
name: "param2".to_string(),
value: "".to_string()
}
.curl_arg(),
"param2=".to_string()
);
assert_eq!(
Param {
name: "param3".to_string(),
value: "a=b".to_string()
}
.curl_arg_escape(),
"param3=a%3Db".to_string()
);
assert_eq!(
Param {
name: "param4".to_string(),
value: "1,2,3".to_string()
}
.curl_arg_escape(),
"param4=1%2C2%2C3".to_string()
);
}
#[test]
fn requests_curl_args() {
assert_eq!(
hello_http_request().curl_args(".".to_string()),
vec!["'http://localhost:8000/hello'".to_string()]
);
assert_eq!(
custom_http_request().curl_args(".".to_string()),
vec![
"'http://localhost/custom'".to_string(),
"-H".to_string(),
"'User-Agent: iPhone'".to_string(),
"-H".to_string(),
"'Foo: Bar'".to_string(),
]
);
assert_eq!(
query_http_request().curl_args(".".to_string()),
vec![
"'http://localhost:8000/querystring-params?param1=value1&param2=a%20b'".to_string()
]
);
assert_eq!(
form_http_request().curl_args(".".to_string()),
vec![
"'http://localhost/form-params'".to_string(),
"-H".to_string(),
"'Content-Type: application/x-www-form-urlencoded'".to_string(),
"--data".to_string(),
"'param1=value1'".to_string(),
"--data".to_string(),
"'param2=a%20b'".to_string(),
]
);
}
#[test]
fn test_encode_value() {
assert_eq!(
encode_value("Header1: x".to_string()),
"'Header1: x'".to_string()
);
assert_eq!(
encode_value("Header1: '".to_string()),
"$'Header1: \\''".to_string()
);
},
)
}
}

View File

@ -0,0 +1,555 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use core::fmt;
use super::core::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestSpec {
pub method: Method,
pub url: String,
pub headers: Vec<Header>,
pub querystring: Vec<Param>,
pub form: Vec<Param>,
pub multipart: Vec<MultipartParam>,
pub cookies: Vec<RequestCookie>,
pub body: Body,
pub content_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultipartParam {
Param(Param),
FileParam(FileParam),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileParam {
pub name: String,
pub filename: String,
pub data: Vec<u8>,
pub content_type: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Body {
Text(String),
Binary(Vec<u8>),
File(Vec<u8>, String),
}
impl Body {
pub fn bytes(&self) -> Vec<u8> {
match self {
Body::Text(s) => s.as_bytes().to_vec(),
Body::Binary(bs) => bs.clone(),
Body::File(bs, _) => bs.clone(),
}
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH",
};
write!(f, "{}", value)
}
}
impl fmt::Display for MultipartParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MultipartParam::Param(param) => write!(f, "{}", param.to_string()),
MultipartParam::FileParam(param) => write!(f, "{}", param.to_string()),
}
}
}
impl fmt::Display for FileParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}: file,{}; {}",
self.name, self.filename, self.content_type
)
}
}
impl RequestSpec {
///
/// return request as curl arguments
/// It does not contain the requests cookies (they will be accessed from the client)
///
pub fn curl_args(&self, context_dir: String) -> Vec<String> {
let querystring = if self.querystring.is_empty() {
"".to_string()
} else {
let params = self
.querystring
.iter()
.map(|p| p.curl_arg_escape())
.collect::<Vec<String>>();
params.join("&")
};
let url = if querystring.as_str() == "" {
self.url.to_string()
} else if self.url.to_string().contains('?') {
format!("{}&{}", self.url.to_string(), querystring)
} else {
format!("{}?{}", self.url.to_string(), querystring)
};
let mut arguments = vec![format!("'{}'", url)];
let data =
!self.multipart.is_empty() || !self.form.is_empty() || !self.body.bytes().is_empty();
arguments.append(&mut self.method.curl_args(data));
for header in self.headers.clone() {
arguments.append(&mut header.curl_args());
}
let has_explicit_content_type = self
.headers
.iter()
.map(|h| h.name.clone())
.any(|n| n.as_str() == "Content-Type");
if !has_explicit_content_type {
if let Some(content_type) = self.content_type.clone() {
if content_type.as_str() != "application/x-www-form-urlencoded"
&& content_type.as_str() != "multipart/form-data"
{
arguments.push("-H".to_string());
arguments.push(format!("'Content-Type: {}'", content_type));
}
} else if !self.body.bytes().is_empty() {
match self.body.clone() {
Body::Text(_) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type:'".to_string())
}
Body::Binary(_) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type: application/octet-stream'".to_string())
}
Body::File(_, _) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type:'".to_string())
}
}
}
}
for param in self.form.clone() {
arguments.push("--data".to_string());
arguments.push(format!("'{}'", param.curl_arg_escape()));
}
for param in self.multipart.clone() {
arguments.push("-F".to_string());
arguments.push(format!("'{}'", param.curl_arg(context_dir.clone())));
}
if !self.body.bytes().is_empty() {
arguments.push("--data".to_string());
match self.body.clone() {
Body::Text(s) => {
let prefix = if s.contains('\n') { "$" } else { "" };
arguments.push(format!("{}'{}'", prefix, s.replace("\n", "\\n")))
}
Body::Binary(bytes) => arguments.push(format!("$'{}'", encode_bytes(bytes))),
Body::File(_, filename) => {
let prefix = if context_dir.as_str() == "." {
"".to_string()
} else {
format!("{}/", context_dir)
};
arguments.push(format!("'@{}{}'", prefix, filename))
}
}
}
arguments
}
}
fn encode_byte(b: u8) -> String {
format!("\\x{:02x}", b)
}
fn encode_bytes(b: Vec<u8>) -> String {
b.iter().map(|b| encode_byte(*b)).collect()
}
impl Method {
pub fn curl_args(&self, data: bool) -> Vec<String> {
match self {
Method::Get => {
if data {
vec!["-X".to_string(), "GET".to_string()]
} else {
vec![]
}
}
Method::Head => vec!["-X".to_string(), "HEAD".to_string()],
Method::Post => {
if data {
vec![]
} else {
vec!["-X".to_string(), "POST".to_string()]
}
}
Method::Put => vec!["-X".to_string(), "PUT".to_string()],
Method::Delete => vec!["-X".to_string(), "DELETE".to_string()],
Method::Connect => vec!["-X".to_string(), "CONNECT".to_string()],
Method::Options => vec!["-X".to_string(), "OPTIONS".to_string()],
Method::Trace => vec!["-X".to_string(), "TRACE".to_string()],
Method::Patch => vec!["-X".to_string(), "PATCH".to_string()],
}
}
}
impl Header {
pub fn curl_args(&self) -> Vec<String> {
let name = self.name.clone();
let value = self.value.clone();
vec![
"-H".to_string(),
encode_value(format!("{}: {}", name, value)),
]
}
}
impl Param {
pub fn curl_arg_escape(&self) -> String {
let name = self.name.clone();
let value = escape_url(self.value.clone());
format!("{}={}", name, value)
}
pub fn curl_arg(&self) -> String {
let name = self.name.clone();
let value = self.value.clone();
format!("{}={}", name, value)
}
}
impl MultipartParam {
pub fn curl_arg(&self, context_dir: String) -> String {
match self {
MultipartParam::Param(param) => param.curl_arg(),
MultipartParam::FileParam(FileParam {
name,
filename,
content_type,
..
}) => {
let prefix = if context_dir.as_str() == "." {
"".to_string()
} else {
format!("{}/", context_dir)
};
let value = format!("@{}{};type={}", prefix, filename, content_type);
format!("{}={}", name, value)
}
}
}
}
fn escape_single_quote(s: String) -> String {
s.chars()
.map(|c| {
if c == '\'' {
"\\'".to_string()
} else {
c.to_string()
}
})
.collect::<Vec<String>>()
.join("")
}
fn escape_url(s: String) -> String {
percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
}
// special encoding for the shell
// $'...'
fn encode_value(s: String) -> String {
if s.contains('\'') {
let s = escape_single_quote(s);
format!("$'{}'", s)
} else {
format!("'{}'", s)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn hello_http_request() -> RequestSpec {
RequestSpec {
method: Method::Get,
url: "http://localhost:8000/hello".to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn custom_http_request() -> RequestSpec {
RequestSpec {
method: Method::Get,
url: "http://localhost/custom".to_string(),
querystring: vec![],
headers: vec![
Header {
name: String::from("User-Agent"),
value: String::from("iPhone"),
},
Header {
name: String::from("Foo"),
value: String::from("Bar"),
},
],
cookies: vec![
RequestCookie {
name: String::from("theme"),
value: String::from("light"),
},
RequestCookie {
name: String::from("sessionToken"),
value: String::from("abc123"),
},
],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn query_http_request() -> RequestSpec {
RequestSpec {
method: Method::Get,
url: "http://localhost:8000/querystring-params".to_string(),
querystring: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
headers: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn form_http_request() -> RequestSpec {
RequestSpec {
method: Method::Post,
url: "http://localhost/form-params".to_string(),
querystring: vec![],
headers: vec![Header {
name: String::from("Content-Type"),
value: String::from("application/x-www-form-urlencoded"),
}],
cookies: vec![],
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
content_type: Some("multipart/form-data".to_string()),
}
}
#[test]
fn test_encode_byte() {
assert_eq!(encode_byte(1), "\\x01".to_string());
assert_eq!(encode_byte(32), "\\x20".to_string());
}
#[test]
fn method_curl_args() {
assert!(Method::Get.curl_args(false).is_empty());
assert_eq!(
Method::Get.curl_args(true),
vec!["-X".to_string(), "GET".to_string()]
);
assert_eq!(
Method::Post.curl_args(false),
vec!["-X".to_string(), "POST".to_string()]
);
assert!(Method::Post.curl_args(true).is_empty());
assert_eq!(
Method::Put.curl_args(false),
vec!["-X".to_string(), "PUT".to_string()]
);
assert_eq!(
Method::Put.curl_args(true),
vec!["-X".to_string(), "PUT".to_string()]
);
}
#[test]
fn header_curl_args() {
assert_eq!(
Header {
name: "Host".to_string(),
value: "example.com".to_string()
}
.curl_args(),
vec!["-H".to_string(), "'Host: example.com'".to_string()]
);
assert_eq!(
Header {
name: "If-Match".to_string(),
value: "\"e0023aa4e\"".to_string()
}
.curl_args(),
vec!["-H".to_string(), "'If-Match: \"e0023aa4e\"'".to_string()]
);
}
#[test]
fn param_curl_args() {
assert_eq!(
Param {
name: "param1".to_string(),
value: "value1".to_string()
}
.curl_arg(),
"param1=value1".to_string()
);
assert_eq!(
Param {
name: "param2".to_string(),
value: "".to_string()
}
.curl_arg(),
"param2=".to_string()
);
assert_eq!(
Param {
name: "param3".to_string(),
value: "a=b".to_string()
}
.curl_arg_escape(),
"param3=a%3Db".to_string()
);
assert_eq!(
Param {
name: "param4".to_string(),
value: "1,2,3".to_string()
}
.curl_arg_escape(),
"param4=1%2C2%2C3".to_string()
);
}
#[test]
fn requests_curl_args() {
assert_eq!(
hello_http_request().curl_args(".".to_string()),
vec!["'http://localhost:8000/hello'".to_string()]
);
assert_eq!(
custom_http_request().curl_args(".".to_string()),
vec![
"'http://localhost/custom'".to_string(),
"-H".to_string(),
"'User-Agent: iPhone'".to_string(),
"-H".to_string(),
"'Foo: Bar'".to_string(),
]
);
assert_eq!(
query_http_request().curl_args(".".to_string()),
vec![
"'http://localhost:8000/querystring-params?param1=value1&param2=a%20b'".to_string()
]
);
assert_eq!(
form_http_request().curl_args(".".to_string()),
vec![
"'http://localhost/form-params'".to_string(),
"-H".to_string(),
"'Content-Type: application/x-www-form-urlencoded'".to_string(),
"--data".to_string(),
"'param1=value1'".to_string(),
"--data".to_string(),
"'param2=a%20b'".to_string(),
]
);
}
#[test]
fn test_encode_value() {
assert_eq!(
encode_value("Header1: x".to_string()),
"'Header1: x'".to_string()
);
assert_eq!(
encode_value("Header1: '".to_string()),
"$'Header1: \\''".to_string()
);
}
}

View File

@ -29,6 +29,7 @@ use super::value::Value;
use crate::runner::request::{cookie_storage_clear, cookie_storage_set};
/// Run an entry with the hurl http client
/// Return one or more EntryResults (if following redirect)
///
/// # Examples
///
@ -54,18 +55,18 @@ pub fn run(
context_dir: String,
log_verbose: &impl Fn(&str),
log_error_message: &impl Fn(bool, &str),
) -> EntryResult {
) -> Vec<EntryResult> {
let http_request = match eval_request(entry.request.clone(), variables, context_dir.clone()) {
Ok(r) => r,
Err(error) => {
return EntryResult {
return vec![EntryResult {
request: None,
response: None,
captures: vec![],
asserts: vec![],
errors: vec![error],
time_in_ms: 0,
};
}];
}
};
@ -106,8 +107,8 @@ pub fn run(
.as_str(),
);
let http_response = match http_client.execute(&http_request, 0) {
Ok(response) => response,
let calls = match http_client.execute_with_redirect(&http_request) {
Ok(calls) => calls,
Err(http_error) => {
let runner_error = match http_error {
HttpError::CouldNotResolveProxyName => RunnerError::CouldNotResolveProxyName,
@ -129,87 +130,100 @@ pub fn run(
url: http_request.url.clone(),
},
};
return EntryResult {
request: Some(http_request.clone()),
return vec![EntryResult {
request: None,
response: None,
captures: vec![],
asserts: vec![],
errors: vec![Error {
source_info: SourceInfo {
start: entry.clone().request.url.source_info.start,
end: entry.clone().request.url.source_info.end,
start: entry.request.url.source_info.start,
end: entry.request.url.source_info.end,
},
inner: runner_error,
assert: false,
}],
time_in_ms: 0,
}];
}
};
let mut entry_results = vec![];
for (i, (http_request, http_response)) in calls.iter().enumerate() {
let mut captures = vec![];
let mut asserts = vec![];
let mut errors = vec![];
let time_in_ms = http_response.duration.as_millis();
// Last call
if i == calls.len() - 1 {
captures = match entry.response.clone() {
None => vec![],
Some(response) => match eval_captures(response, http_response, variables) {
Ok(captures) => captures,
Err(e) => {
return vec![EntryResult {
request: Some(http_request.clone()),
response: Some(http_response.clone()),
captures: vec![],
asserts: vec![],
errors: vec![e],
time_in_ms,
}];
}
},
};
}
};
let time_in_ms = http_response.duration.as_millis();
log_verbose(format!("Response Time: {}ms", time_in_ms.to_string()).as_str());
let captures = match entry.response.clone() {
None => vec![],
Some(response) => match eval_captures(response, http_response.clone(), variables) {
Ok(captures) => captures,
Err(e) => {
return EntryResult {
request: Some(http_request),
response: Some(http_response),
captures: vec![],
asserts: vec![],
errors: vec![e],
time_in_ms,
};
// update variables now!
for capture_result in captures.clone() {
variables.insert(capture_result.name, capture_result.value);
}
},
};
asserts = match entry.response.clone() {
None => vec![],
Some(response) => eval_asserts(
response,
variables,
http_response.clone(),
context_dir.clone(),
),
};
errors = asserts
.iter()
.filter_map(|assert| assert.clone().error())
.map(
|Error {
source_info, inner, ..
}| Error {
source_info,
inner,
assert: true,
},
)
.collect();
// update variables now!
for capture_result in captures.clone() {
variables.insert(capture_result.name, capture_result.value);
}
let asserts = match entry.response {
None => vec![],
Some(response) => eval_asserts(response, variables, http_response.clone(), context_dir),
};
let errors = asserts
.iter()
.filter_map(|assert| assert.clone().error())
.map(
|Error {
source_info, inner, ..
}| Error {
source_info,
inner,
assert: true,
},
)
.collect();
if !captures.is_empty() {
log_verbose("Captures");
for capture in captures.clone() {
log_verbose(format!("{}: {}", capture.name, capture.value).as_str());
if !captures.is_empty() {
log_verbose("Captures");
for capture in captures.clone() {
log_verbose(format!("{}: {}", capture.name, capture.value).as_str());
}
}
log_verbose("");
}
let entry_result = EntryResult {
request: Some(http_request.clone()),
response: Some(http_response.clone()),
captures,
asserts,
errors,
time_in_ms,
};
entry_results.push(entry_result);
}
log_verbose("");
EntryResult {
request: Some(http_request),
response: Some(http_response),
captures,
asserts,
errors,
time_in_ms,
}
entry_results
}
pub fn log_request(log_verbose: impl Fn(&str), request: &http::Request) {
pub fn log_request(log_verbose: impl Fn(&str), request: &http::RequestSpec) {
log_verbose("Request");
log_verbose(format!("{} {}", request.method, request.url).as_str());
for header in request.headers.clone() {

View File

@ -124,7 +124,7 @@ pub fn run(
break;
}
let entry_result = entry::run(
let entry_results = entry::run(
entry,
http_client,
entry_index,
@ -133,12 +133,15 @@ pub fn run(
&log_verbose,
&log_error_message,
);
entries.push(entry_result.clone());
for e in entry_result.errors.clone() {
log_error(&e, false);
for entry_result in entry_results.clone() {
for e in entry_result.errors.clone() {
log_error(&e, false);
}
entries.push(entry_result.clone());
}
let exit = (options.post_entry)();
if exit || (options.fail_fast && !entry_result.errors.is_empty()) {
if exit || (options.fail_fast && !entry_results.last().unwrap().errors.is_empty()) {
break;
}
}

View File

@ -16,7 +16,6 @@
*
*/
use crate::http;
use crate::http::*;
use super::cookie::*;
@ -99,11 +98,90 @@ fn parse_entry_result(value: serde_json::Value) -> Result<EntryResult, String> {
time_in_ms: 0,
})
}
// pub fn _parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
// if let serde_json::Value::Object(map) = value {
// let method = match map.get("method") {
// Some(serde_json::Value::String(s)) => parse_method(s.clone())?,
// _ => return Err("expecting a string for the method".to_string()),
// };
// let url = match map.get("url") {
// Some(serde_json::Value::String(s)) => s.to_string(),
// _ => return Err("expecting a string for the url".to_string()),
// };
//
// let headers = match map.get("headers") {
// Some(serde_json::Value::Array(values)) => {
// let mut headers = vec![];
// for value in values {
// let header = parse_header(value.clone())?;
// headers.push(header);
// }
// headers
// }
// _ => vec![],
// };
//
// let querystring = match map.get("queryString") {
// Some(serde_json::Value::Array(values)) => {
// let mut params = vec![];
// for value in values {
// let param = parse_param(value.clone())?;
// params.push(param);
// }
// params
// }
// _ => vec![],
// };
//
// let form = match map.get("form") {
// Some(serde_json::Value::Array(values)) => {
// let mut params = vec![];
// for value in values {
// let param = parse_param(value.clone())?;
// params.push(param);
// }
// params
// }
// _ => vec![],
// };
//
// let cookies = match map.get("cookies") {
// Some(serde_json::Value::Array(values)) => {
// let mut headers = vec![];
// for value in values {
// let header = parse_request_cookie(value.clone())?;
// headers.push(header);
// }
// headers
// }
// _ => vec![],
// };
//
// // TODO
// let multipart = vec![];
// let body = http::Body::Binary(vec![]);
// let content_type = None;
//
// Ok(Request {
// method,
// url,
// headers,
// querystring,
// form,
// multipart,
// cookies,
// body,
// content_type,
// })
// } else {
// Err("expecting an object for the request".to_string())
// }
// }
pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
if let serde_json::Value::Object(map) = value {
let method = match map.get("method") {
Some(serde_json::Value::String(s)) => parse_method(s.clone())?,
Some(serde_json::Value::String(s)) => parse_method(s.clone())?.to_string(),
_ => return Err("expecting a string for the method".to_string()),
};
let url = match map.get("url") {
@ -123,57 +201,10 @@ pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
_ => vec![],
};
let querystring = match map.get("queryString") {
Some(serde_json::Value::Array(values)) => {
let mut params = vec![];
for value in values {
let param = parse_param(value.clone())?;
params.push(param);
}
params
}
_ => vec![],
};
let form = match map.get("form") {
Some(serde_json::Value::Array(values)) => {
let mut params = vec![];
for value in values {
let param = parse_param(value.clone())?;
params.push(param);
}
params
}
_ => vec![],
};
let cookies = match map.get("cookies") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_request_cookie(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
// TODO
let multipart = vec![];
let body = http::Body::Binary(vec![]);
let content_type = None;
Ok(Request {
method,
url,
method,
headers,
querystring,
form,
multipart,
cookies,
body,
content_type,
})
} else {
Err("expecting an object for the request".to_string())
@ -256,37 +287,37 @@ fn parse_header(value: serde_json::Value) -> Result<Header, ParseError> {
}
}
pub fn parse_param(value: serde_json::Value) -> Result<Param, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie value".to_string()),
};
Ok(Param { name, value })
} else {
Err("Expecting object for the param".to_string())
}
}
// pub fn parse_param(value: serde_json::Value) -> Result<Param, ParseError> {
// if let serde_json::Value::Object(map) = value {
// let name = match map.get("name") {
// Some(serde_json::Value::String(s)) => s.to_string(),
// _ => return Err("expecting a string for the cookie name".to_string()),
// };
// let value = match map.get("value") {
// Some(serde_json::Value::String(s)) => s.to_string(),
// _ => return Err("expecting a string for the cookie value".to_string()),
// };
// Ok(Param { name, value })
// } else {
// Err("Expecting object for the param".to_string())
// }
// }
pub fn parse_request_cookie(value: serde_json::Value) -> Result<RequestCookie, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie value".to_string()),
};
Ok(RequestCookie { name, value })
} else {
Err("Expecting object for the request cookie".to_string())
}
}
// pub fn parse_request_cookie(value: serde_json::Value) -> Result<RequestCookie, ParseError> {
// if let serde_json::Value::Object(map) = value {
// let name = match map.get("name") {
// Some(serde_json::Value::String(s)) => s.to_string(),
// _ => return Err("expecting a string for the cookie name".to_string()),
// };
// let value = match map.get("value") {
// Some(serde_json::Value::String(s)) => s.to_string(),
// _ => return Err("expecting a string for the cookie value".to_string()),
// };
// Ok(RequestCookie { name, value })
// } else {
// Err("Expecting object for the request cookie".to_string())
// }
// }
#[allow(dead_code)]
pub fn parse_response_cookie(value: serde_json::Value) -> Result<ResponseCookie, ParseError> {
@ -383,59 +414,59 @@ mod tests {
use super::*;
use crate::runner::value::Value;
#[test]
fn test_parse_request() {
let v: serde_json::Value = serde_json::from_str(
r#"{
"method": "GET",
"url": "http://localhost:8000/hello",
"headers": []
}"#,
)
.unwrap();
assert_eq!(parse_request(v).unwrap(), hello_http_request());
let v: serde_json::Value = serde_json::from_str(
r#"{
"method": "GET",
"url": "http://localhost:8000/querystring-params?param1=value1&param2=a%20b",
"headers": []
}"#,
)
.unwrap();
assert_eq!(
parse_request(v).unwrap(),
http::Request {
method: http::Method::Get,
url: "http://localhost:8000/querystring-params?param1=value1&param2=a%20b"
.to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: http::Body::Binary(vec![]),
form: vec![],
multipart: vec![],
content_type: None,
}
);
let v: serde_json::Value = serde_json::from_str(
r#"{
"method": "GET",
"url": "http://localhost/custom",
"headers": [
{"name": "User-Agent", "value": "iPhone"},
{"name": "Foo", "value": "Bar"}
],
"cookies": [
{"name": "theme", "value": "light"},
{"name": "sessionToken", "value": "abc123"}
]
}"#,
)
.unwrap();
assert_eq!(parse_request(v).unwrap(), custom_http_request());
}
// #[test]
// fn test_parse_request() {
// let v: serde_json::Value = serde_json::from_str(
// r#"{
// "method": "GET",
// "url": "http://localhost:8000/hello",
// "headers": []
// }"#,
// )
// .unwrap();
// assert_eq!(_parse_request(v).unwrap(), hello_http_request());
//
// let v: serde_json::Value = serde_json::from_str(
// r#"{
// "method": "GET",
// "url": "http://localhost:8000/querystring-params?param1=value1&param2=a%20b",
// "headers": []
// }"#,
// )
// .unwrap();
// assert_eq!(
// _parse_request(v).unwrap(),
// http::Request {
// method: http::Method::Get,
// url: "http://localhost:8000/querystring-params?param1=value1&param2=a%20b"
// .to_string(),
// querystring: vec![],
// headers: vec![],
// cookies: vec![],
// body: http::Body::Binary(vec![]),
// form: vec![],
// multipart: vec![],
// content_type: None,
// }
// );
//
// let v: serde_json::Value = serde_json::from_str(
// r#"{
// "method": "GET",
// "url": "http://localhost/custom",
// "headers": [
// {"name": "User-Agent", "value": "iPhone"},
// {"name": "Foo", "value": "Bar"}
// ],
// "cookies": [
// {"name": "theme", "value": "light"},
// {"name": "sessionToken", "value": "abc123"}
// ]
// }"#,
// )
// .unwrap();
// assert_eq!(_parse_request(v).unwrap(), custom_http_request());
// }
#[test]
fn test_parse_response() {

View File

@ -94,20 +94,12 @@ impl Serialize for Request {
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("method", &self.clone().method.to_string())?;
let mut state = serializer.serialize_struct("Request", 5)?;
state.serialize_field("method", &self.clone().method)?;
state.serialize_field("url", &self.clone().url)?;
state.serialize_field("queryString", &self.clone().querystring)?;
state.serialize_field("headers", &self.clone().headers)?;
state.serialize_field("cookies", &self.clone().cookies)?;
state.serialize_field("multipartFormData", &self.clone().multipart)?;
if !self.clone().form.is_empty() {
state.serialize_field("form", &self.clone().form)?;
}
state.serialize_field("body", &base64::encode(&self.body.bytes()))?;
state.serialize_field("cookies", &self.clone().cookies())?;
state.serialize_field("queryString", &self.clone().query_string_params())?;
state.end()
}
}
@ -118,7 +110,7 @@ impl Serialize for Response {
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
let mut state = serializer.serialize_struct("Response", 3)?;
state.serialize_field("httpVersion", &self.clone().version)?;
state.serialize_field("status", &self.clone().status)?;
state.serialize_field("cookies", &self.clone().cookies())?;

View File

@ -36,7 +36,7 @@ pub fn eval_request(
request: Request,
variables: &HashMap<String, Value>,
context_dir: String,
) -> Result<http::Request, Error> {
) -> Result<http::RequestSpec, Error> {
let method = eval_method(request.method.clone());
let url = eval_template(request.clone().url, &variables)?;
@ -121,7 +121,7 @@ pub fn eval_request(
// }
// }
Ok(http::Request {
Ok(http::RequestSpec {
method,
url,
headers,

View File

@ -212,7 +212,7 @@ pub fn eval_asserts(
pub fn eval_captures(
response: Response,
http_response: http::Response,
http_response: &http::Response,
variables: &HashMap<String, Value>,
) -> Result<Vec<CaptureResult>, Error> {
let mut captures = vec![];
@ -321,7 +321,7 @@ mod tests {
assert_eq!(
eval_captures(
user_response(),
http::xml_two_users_http_response(),
&http::xml_two_users_http_response(),
&variables,
)
.unwrap(),

View File

@ -14,8 +14,8 @@ fn default_client() -> Client {
Client::init(options)
}
fn default_get_request(url: String) -> Request {
Request {
fn default_get_request(url: String) -> RequestSpec {
RequestSpec {
method: Method::Get,
url,
headers: vec![],
@ -33,13 +33,25 @@ fn default_get_request(url: String) -> Request {
#[test]
fn test_hello() {
let mut client = default_client();
let request = default_get_request("http://localhost:8000/hello".to_string());
let request_spec = default_get_request("http://localhost:8000/hello".to_string());
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/hello'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "GET".to_string());
assert_eq!(request.url, "http://localhost:8000/hello".to_string());
assert_eq!(request.headers.len(), 3);
assert!(request.headers.contains(&Header {
name: "Host".to_string(),
value: "localhost:8000".to_string()
}));
assert!(request.headers.contains(&Header {
name: "Accept".to_string(),
value: "*/*".to_string()
}));
assert_eq!(response.version, Version::Http10);
assert_eq!(response.status, 200);
assert_eq!(response.body, b"Hello World!".to_vec());
@ -63,7 +75,7 @@ fn test_hello() {
#[test]
fn test_put() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Put,
url: "http://localhost:8000/put".to_string(),
headers: vec![],
@ -75,11 +87,22 @@ fn test_put() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/put' -X PUT".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "PUT".to_string());
assert_eq!(request.url, "http://localhost:8000/put".to_string());
assert!(request.headers.contains(&Header {
name: "Host".to_string(),
value: "localhost:8000".to_string()
}));
assert!(request.headers.contains(&Header {
name: "Accept".to_string(),
value: "*/*".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -87,7 +110,7 @@ fn test_put() {
#[test]
fn test_patch() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Patch,
url: "http://localhost:8000/patch/file.txt".to_string(),
headers: vec![
@ -112,11 +135,25 @@ fn test_patch() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/patch/file.txt' -X PATCH -H 'Host: www.example.com' -H 'Content-Type: application/example' -H 'If-Match: \"e0023aa4e\"'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "PATCH".to_string());
assert_eq!(
request.url,
"http://localhost:8000/patch/file.txt".to_string()
);
assert!(request.headers.contains(&Header {
name: "Host".to_string(),
value: "www.example.com".to_string()
}));
assert!(request.headers.contains(&Header {
name: "Content-Type".to_string(),
value: "application/example".to_string()
}));
assert_eq!(response.status, 204);
assert!(response.body.is_empty());
}
@ -128,7 +165,7 @@ fn test_patch() {
#[test]
fn test_custom_headers() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/custom-headers".to_string(),
headers: vec![
@ -147,11 +184,20 @@ fn test_custom_headers() {
};
assert!(client.options.curl_args().is_empty());
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/custom-headers' -H 'Fruit: Raspberry' -H 'Fruit: Apple' -H 'Fruit: Banana' -H 'Fruit: Grape' -H 'Color: Green'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "GET".to_string());
assert_eq!(
request.url,
"http://localhost:8000/custom-headers".to_string()
);
assert!(request.headers.contains(&Header {
name: "Fruit".to_string(),
value: "Raspberry".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -163,7 +209,7 @@ fn test_custom_headers() {
#[test]
fn test_querystring_params() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/querystring-params".to_string(),
headers: vec![],
@ -192,10 +238,14 @@ fn test_querystring_params() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "GET".to_string());
assert_eq!(request.url, "http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3".to_string());
assert_eq!(request.headers.len(), 3);
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -207,7 +257,7 @@ fn test_querystring_params() {
#[test]
fn test_form_params() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Post,
url: "http://localhost:8000/form-params".to_string(),
headers: vec![],
@ -236,17 +286,27 @@ fn test_form_params() {
content_type: Some("application/x-www-form-urlencoded".to_string()),
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/form-params' --data 'param1=value1' --data 'param2=' --data 'param3=a%3Db' --data 'param4=a%253db'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "POST".to_string());
assert_eq!(request.url, "http://localhost:8000/form-params".to_string());
assert!(request.headers.contains(&Header {
name: "Content-Type".to_string(),
value: "application/x-www-form-urlencoded".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
// make sure you can reuse client for other request
let request = default_get_request("http://localhost:8000/hello".to_string());
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request).unwrap();
assert_eq!(request.method, "GET".to_string());
assert_eq!(request.url, "http://localhost:8000/hello".to_string());
assert_eq!(request.headers.len(), 3);
assert_eq!(response.status, 200);
assert_eq!(response.body, b"Hello World!".to_vec());
}
@ -256,11 +316,15 @@ fn test_form_params() {
// region redirect
#[test]
fn test_follow_location() {
let request = default_get_request("http://localhost:8000/redirect".to_string());
fn test_redirect() {
let request_spec = default_get_request("http://localhost:8000/redirect".to_string());
let mut client = default_client();
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "GET".to_string());
assert_eq!(request.url, "http://localhost:8000/redirect".to_string());
assert_eq!(request.headers.len(), 3);
assert_eq!(response.status, 302);
assert_eq!(
response
@ -270,6 +334,11 @@ fn test_follow_location() {
"http://localhost:8000/redirected"
);
assert_eq!(client.redirect_count, 0);
}
#[test]
fn test_follow_location() {
let request_spec = default_get_request("http://localhost:8000/redirect".to_string());
let options = ClientOptions {
follow_location: true,
@ -288,24 +357,35 @@ fn test_follow_location() {
let mut client = Client::init(options);
assert_eq!(client.options.curl_args(), vec!["-L".to_string(),]);
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/redirect' -L".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(
response
.get_header_values("Content-Length".to_string())
.get(0)
.unwrap(),
"0"
);
let calls = client.execute_with_redirect(&request_spec).unwrap();
assert_eq!(calls.len(), 2);
let (request1, response1) = calls.get(0).unwrap();
assert_eq!(request1.method, "GET".to_string());
assert_eq!(request1.url, "http://localhost:8000/redirect".to_string());
assert_eq!(request1.headers.len(), 3);
assert_eq!(response1.status, 302);
assert!(response1.headers.contains(&Header {
name: "Location".to_string(),
value: "http://localhost:8000/redirected".to_string()
}));
let (request2, response2) = calls.get(1).unwrap();
assert_eq!(request2.method, "GET".to_string());
assert_eq!(request2.url, "http://localhost:8000/redirected".to_string());
assert_eq!(request2.headers.len(), 3);
assert_eq!(response2.status, 200);
assert_eq!(client.redirect_count, 1);
// make sure that the redirect count is reset to 0
let request = default_get_request("http://localhost:8000/hello".to_string());
let response = client.execute(&request, 0).unwrap();
let calls = client.execute_with_redirect(&request).unwrap();
let (_, response) = calls.get(0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.body, b"Hello World!".to_vec());
assert_eq!(client.redirect_count, 0);
@ -328,18 +408,25 @@ fn test_max_redirect() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/redirect".to_string());
let request_spec = default_get_request("http://localhost:8000/redirect/15".to_string());
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/redirect' -L --max-redirs 10".to_string()
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/redirect/15' -L --max-redirs 10".to_string()
);
let response = client.execute(&request, 5).unwrap();
assert_eq!(response.status, 200);
assert_eq!(client.redirect_count, 6);
let error = client.execute(&request, 11).err().unwrap();
let error = client.execute_with_redirect(&request_spec).err().unwrap();
assert_eq!(error, HttpError::TooManyRedirect);
let request_spec = default_get_request("http://localhost:8000/redirect/8".to_string());
assert_eq!(
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/redirect/8' -L --max-redirs 10".to_string()
);
let calls = client.execute_with_redirect(&request_spec).unwrap();
let (request, response) = calls.last().unwrap();
assert_eq!(request.url, "http://localhost:8000/redirect/0".to_string());
assert_eq!(response.status, 200);
assert_eq!(client.redirect_count, 8);
}
// endregion
@ -349,7 +436,7 @@ fn test_max_redirect() {
#[test]
fn test_multipart_form_data() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Post,
url: "http://localhost:8000/multipart-form-data".to_string(),
headers: vec![],
@ -384,17 +471,22 @@ fn test_multipart_form_data() {
content_type: Some("multipart/form-data".to_string()),
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/multipart-form-data' -F 'key1=value1' -F 'upload1=@data.txt;type=text/plain' -F 'upload2=@data.html;type=text/html' -F 'upload3=@data.txt;type=text/html'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Content-Length".to_string(),
value: "616".to_string(),
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
// make sure you can reuse client for other request
let request = default_get_request("http://localhost:8000/hello".to_string());
let response = client.execute(&request, 0).unwrap();
let request_spec = default_get_request("http://localhost:8000/hello".to_string());
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.method, "GET".to_string());
assert_eq!(response.status, 200);
assert_eq!(response.body, b"Hello World!".to_vec());
}
@ -406,7 +498,7 @@ fn test_multipart_form_data() {
#[test]
fn test_post_bytes() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Post,
url: "http://localhost:8000/post-base64".to_string(),
headers: vec![],
@ -418,10 +510,15 @@ fn test_post_bytes() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/post-base64' -H 'Content-Type: application/octet-stream' --data $'\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64\\x21'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Content-Length".to_string(),
value: "12".to_string(),
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -431,7 +528,7 @@ fn test_post_bytes() {
#[test]
fn test_expect() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Post,
url: "http://localhost:8000/expect".to_string(),
headers: vec![Header {
@ -446,11 +543,15 @@ fn test_expect() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/expect' -H 'Expect: 100-continue' -H 'Content-Type:' --data 'data'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Expect".to_string(),
value: "100-continue".to_string(),
}));
assert_eq!(response.status, 200);
assert_eq!(response.version, Version::Http10);
assert!(response.body.is_empty());
@ -473,7 +574,7 @@ fn test_basic_authentication() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/basic-authentication".to_string(),
headers: vec![],
@ -485,16 +586,20 @@ fn test_basic_authentication() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/basic-authentication' --user 'bob:secret'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Authorization".to_string(),
value: "Basic Ym9iOnNlY3JldA==".to_string(),
}));
assert_eq!(response.status, 200);
assert_eq!(response.version, Version::Http10);
assert_eq!(response.body, b"You are authenticated".to_vec());
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://bob:secret@localhost:8000/basic-authentication".to_string(),
headers: vec![],
@ -506,10 +611,14 @@ fn test_basic_authentication() {
content_type: None,
};
assert_eq!(
request.curl_args(".".to_string()),
request_spec.curl_args(".".to_string()),
vec!["'http://bob:secret@localhost:8000/basic-authentication'".to_string()]
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Authorization".to_string(),
value: "Basic Ym9iOnNlY3JldA==".to_string(),
}));
assert_eq!(response.status, 200);
assert_eq!(response.version, Version::Http10);
assert_eq!(response.body, b"You are authenticated".to_vec());
@ -521,7 +630,7 @@ fn test_basic_authentication() {
fn test_error_could_not_resolve_host() {
let mut client = default_client();
let request = default_get_request("http://unknown".to_string());
let error = client.execute(&request, 0).err().unwrap();
let error = client.execute(&request).err().unwrap();
assert_eq!(error, HttpError::CouldNotResolveHost("unknown".to_string()));
}
@ -529,8 +638,8 @@ fn test_error_could_not_resolve_host() {
#[test]
fn test_error_fail_to_connect() {
let mut client = default_client();
let request = default_get_request("http://localhost:9999".to_string());
let error = client.execute(&request, 0).err().unwrap();
let request_spec = default_get_request("http://localhost:9999".to_string());
let error = client.execute(&request_spec).err().unwrap();
assert_eq!(error, HttpError::FailToConnect);
let options = ClientOptions {
@ -549,7 +658,7 @@ fn test_error_fail_to_connect() {
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
let error = client.execute(&request, 0).err().unwrap();
let error = client.execute(&request).err().unwrap();
assert_eq!(error, HttpError::FailToConnect);
}
@ -570,8 +679,8 @@ fn test_error_could_not_resolve_proxy_name() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
let error = client.execute(&request, 0).err().unwrap();
let request_spec = default_get_request("http://localhost:8000/hello".to_string());
let error = client.execute(&request_spec).err().unwrap();
assert_eq!(error, HttpError::CouldNotResolveProxyName);
}
@ -592,8 +701,8 @@ fn test_error_ssl() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("https://localhost:8001/hello".to_string());
let error = client.execute(&request, 0).err().unwrap();
let request_spec = default_get_request("https://localhost:8001/hello".to_string());
let error = client.execute(&request_spec).err().unwrap();
let message = if cfg!(windows) {
"schannel: SEC_E_UNTRUSTED_ROOT (0x80090325) - The certificate chain was issued by an authority that is not trusted.".to_string()
} else {
@ -619,8 +728,8 @@ fn test_timeout() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/timeout".to_string());
let error = client.execute(&request, 0).err().unwrap();
let request_spec = default_get_request("http://localhost:8000/timeout".to_string());
let error = client.execute(&request_spec).err().unwrap();
assert_eq!(error, HttpError::Timeout);
}
@ -642,7 +751,7 @@ fn test_accept_encoding() {
};
let mut client = Client::init(options);
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/compressed/gzip".to_string(),
headers: vec![],
@ -653,7 +762,11 @@ fn test_accept_encoding() {
body: Body::Binary(vec![]),
content_type: None,
};
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Accept-Encoding".to_string(),
value: "gzip, deflate, br".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.headers.contains(&Header {
name: "Content-Length".to_string(),
@ -678,12 +791,12 @@ fn test_connect_timeout() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://10.0.0.0".to_string());
let request_spec = default_get_request("http://10.0.0.0".to_string());
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://10.0.0.0' --connect-timeout 1".to_string()
);
let error = client.execute(&request, 0).err().unwrap();
let error = client.execute(&request_spec).err().unwrap();
if cfg!(target_os = "macos") {
assert_eq!(error, HttpError::FailToConnect);
} else {
@ -697,7 +810,7 @@ fn test_connect_timeout() {
#[test]
fn test_cookie() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/cookies/set-request-cookie1-valueA".to_string(),
headers: vec![],
@ -712,18 +825,22 @@ fn test_cookie() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/cookies/set-request-cookie1-valueA' --cookie 'cookie1=valueA'"
.to_string()
);
//assert_eq!(request.cookies(), vec!["cookie1=valueA".to_string(),]);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Cookie".to_string(),
value: "cookie1=valueA".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/cookies/assert-that-cookie1-is-not-in-session".to_string(),
headers: vec![],
@ -734,14 +851,14 @@ fn test_cookie() {
body: Body::Binary(vec![]),
content_type: None,
};
let response = client.execute(&request, 0).unwrap();
let (_request, response) = client.execute(&request_spec).unwrap();
assert_eq!(response.status, 200);
}
#[test]
fn test_multiple_request_cookies() {
let mut client = default_client();
let request = Request {
let request_spec = RequestSpec {
method: Method::Get,
url: "http://localhost:8000/cookies/set-multiple-request-cookies".to_string(),
headers: vec![],
@ -762,11 +879,15 @@ fn test_multiple_request_cookies() {
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/cookies/set-multiple-request-cookies' --cookie 'user1=Bob; user2=Bill'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Cookie".to_string(),
value: "user1=Bob; user2=Bill".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -774,9 +895,13 @@ fn test_multiple_request_cookies() {
#[test]
fn test_cookie_storage() {
let mut client = default_client();
let request =
let request_spec =
default_get_request("http://localhost:8000/cookies/set-session-cookie2-valueA".to_string());
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(
request.url,
"http://localhost:8000/cookies/set-session-cookie2-valueA".to_string()
);
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -791,14 +916,18 @@ fn test_cookie_storage() {
expires: "0".to_string(),
name: "cookie2".to_string(),
value: "valueA".to_string(),
http_only: false
http_only: false,
}
);
let request = default_get_request(
let request_spec = default_get_request(
"http://localhost:8000/cookies/assert-that-cookie2-is-valueA".to_string(),
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert!(request.headers.contains(&Header {
name: "Cookie".to_string(),
value: "cookie2=valueA".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -820,15 +949,24 @@ fn test_cookie_file() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request(
let request_spec = default_get_request(
"http://localhost:8000/cookies/assert-that-cookie2-is-valueA".to_string(),
);
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/cookies/assert-that-cookie2-is-valueA' --cookie tests/cookies.txt".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(
request.url,
"http://localhost:8000/cookies/assert-that-cookie2-is-valueA"
);
assert!(request.headers.contains(&Header {
name: "Cookie".to_string(),
value: "cookie2=valueA".to_string()
}));
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -855,12 +993,13 @@ fn test_proxy() {
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/proxy".to_string());
let request_spec = default_get_request("http://localhost:8000/proxy".to_string());
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'http://localhost:8000/proxy' --proxy 'localhost:8888'".to_string()
);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.url, "http://localhost:8000/proxy");
assert_eq!(response.status, 200);
}
@ -884,15 +1023,13 @@ fn test_insecure() {
};
let mut client = Client::init(options);
assert_eq!(client.options.curl_args(), vec!["--insecure".to_string()]);
let request = default_get_request("https://localhost:8001/hello".to_string());
let request_spec = default_get_request("https://localhost:8001/hello".to_string());
assert_eq!(
client.curl_command_line(&request),
client.curl_command_line(&request_spec),
"curl 'https://localhost:8001/hello' --insecure".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
let response = client.execute(&request, 0).unwrap();
let (request, response) = client.execute(&request_spec).unwrap();
assert_eq!(request.url, "https://localhost:8001/hello");
assert_eq!(response.status, 200);
}