use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
#[cfg(feature = "color")]
use colored::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::api::{Method, Regex};
use crate::data::{
ActiveMock, ClosestMatch, HttpMockRequest, MockDefinition, MockMatcherFunction,
MockServerHttpResponse, Pattern, RequestRequirements,
};
use crate::server::{Diff, DiffResult, Mismatch, Reason, Tokenizer};
use crate::util::{get_test_resource_file_path, read_file, Join};
use crate::MockServer;
pub struct MockRef<'a> {
pub id: usize,
server: &'a MockServer,
}
impl<'a> MockRef<'a> {
pub fn new(id: usize, server: &'a MockServer) -> Self {
Self { id, server }
}
pub fn assert(&self) {
self.assert_async().join()
}
pub async fn assert_async(&self) {
self.assert_hits_async(1).await
}
pub fn assert_hits(&self, hits: usize) {
self.assert_hits_async(hits).join()
}
pub async fn assert_hits_async(&self, hits: usize) {
let active_mock = self
.server
.server_adapter
.as_ref()
.unwrap()
.fetch_mock(self.id)
.await
.expect("cannot deserialize mock server response");
if active_mock.call_counter == hits {
return;
}
if active_mock.call_counter > hits {
assert_eq!(
active_mock.call_counter, hits,
"The number of matching requests was higher than expected (expected {} but was {})",
hits, active_mock.call_counter
)
}
let closest_match = self
.server
.server_adapter
.as_ref()
.unwrap()
.verify(&active_mock.definition.request)
.await
.expect("Cannot contact mock server");
fail_with(active_mock.call_counter, hits, closest_match)
}
pub fn hits(&self) -> usize {
self.hits_async().join()
}
#[deprecated(since = "0.5.0", note = "Please use 'hits' function instead")]
pub fn times_called(&self) -> usize {
self.hits()
}
pub async fn hits_async(&self) -> usize {
let response = self
.server
.server_adapter
.as_ref()
.unwrap()
.fetch_mock(self.id)
.await
.expect("cannot deserialize mock server response");
response.call_counter
}
#[deprecated(since = "0.5.0", note = "Please use 'hits_async' function instead")]
pub async fn times_called_async(&self) -> usize {
self.hits_async().await
}
pub fn delete(&mut self) {
self.delete_async().join();
}
pub async fn delete_async(&self) {
self.server
.server_adapter
.as_ref()
.unwrap()
.delete_mock(self.id)
.await
.expect("could not delete mock from server");
}
pub fn server_address(&self) -> &SocketAddr {
self.server.server_adapter.as_ref().unwrap().address()
}
}
pub trait MockRefExt<'a> {
fn new(id: usize, mock_server: &'a MockServer) -> MockRef<'a>;
fn id(&self) -> usize;
}
impl<'a> MockRefExt<'a> for MockRef<'a> {
fn new(id: usize, mock_server: &'a MockServer) -> MockRef<'a> {
MockRef {
id,
server: mock_server,
}
}
fn id(&self) -> usize {
self.id
}
}
#[deprecated(
since = "0.5.0",
note = "Please use newer API (see: https://github.com/alexliesenfeld/httpmock/blob/master/CHANGELOG.md#version-050)"
)]
pub struct Mock {
mock: MockDefinition,
}
impl Mock {
pub fn new() -> Self {
Mock {
mock: MockDefinition {
request: RequestRequirements {
method: None,
path: None,
path_contains: None,
headers: None,
header_exists: None,
cookies: None,
cookie_exists: None,
body: None,
json_body: None,
json_body_includes: None,
body_contains: None,
path_matches: None,
body_matches: None,
query_param_exists: None,
query_param: None,
matchers: None,
},
response: MockServerHttpResponse {
status: None,
headers: None,
body: None,
delay: None,
},
},
}
}
pub fn expect_path<S: Into<String>>(mut self, path: S) -> Self {
self.mock.request.path = Some(path.into());
self
}
pub fn expect_path_contains<S: Into<String>>(mut self, substring: S) -> Self {
if self.mock.request.path_contains.is_none() {
self.mock.request.path_contains = Some(Vec::new());
}
self.mock
.request
.path_contains
.as_mut()
.unwrap()
.push(substring.into());
self
}
pub fn expect_path_matches<R: Into<Regex>>(mut self, regex: R) -> Self {
if self.mock.request.path_matches.is_none() {
self.mock.request.path_matches = Some(Vec::new());
}
self.mock
.request
.path_matches
.as_mut()
.unwrap()
.push(Pattern::from_regex(regex.into()));
self
}
pub fn expect_method<M: Into<Method>>(mut self, method: M) -> Self {
self.mock.request.method = Some(method.into().to_string());
self
}
pub fn expect_header<S: Into<String>>(mut self, name: S, value: S) -> Self {
if self.mock.request.headers.is_none() {
self.mock.request.headers = Some(Vec::new());
}
self.mock
.request
.headers
.as_mut()
.unwrap()
.push((name.into(), value.into()));
self
}
pub fn expect_header_exists<S: Into<String>>(mut self, name: S) -> Self {
if self.mock.request.header_exists.is_none() {
self.mock.request.header_exists = Some(Vec::new());
}
self.mock
.request
.header_exists
.as_mut()
.unwrap()
.push(name.into());
self
}
pub fn expect_cookie<S: Into<String>>(mut self, name: S, value: S) -> Self {
if self.mock.request.cookies.is_none() {
self.mock.request.cookies = Some(Vec::new());
}
self.mock
.request
.cookies
.as_mut()
.unwrap()
.push((name.into(), value.into()));
self
}
pub fn expect_cookie_exists<S: Into<String>>(mut self, name: S) -> Self {
if self.mock.request.cookie_exists.is_none() {
self.mock.request.cookie_exists = Some(Vec::new());
}
self.mock
.request
.cookie_exists
.as_mut()
.unwrap()
.push(name.into());
self
}
pub fn expect_body<S: Into<String>>(mut self, body: S) -> Self {
self.mock.request.body = Some(body.into());
self
}
pub fn expect_json_body_obj<'a, T>(self, body: &T) -> Self
where
T: Serialize + Deserialize<'a>,
{
let json_value = serde_json::to_value(body).expect("Cannot serialize json body to JSON");
self.expect_json_body(json_value)
}
pub fn expect_json_body<V: Into<Value>>(mut self, body: V) -> Self {
self.mock.request.json_body = Some(body.into());
self
}
pub fn expect_json_body_partial<S: Into<String>>(mut self, partial_body: S) -> Self {
if self.mock.request.json_body_includes.is_none() {
self.mock.request.json_body_includes = Some(Vec::new());
}
let value = Value::from_str(&partial_body.into())
.expect("cannot convert JSON string to serde value");
self.mock
.request
.json_body_includes
.as_mut()
.unwrap()
.push(value);
self
}
pub fn expect_body_contains<S: Into<String>>(mut self, substring: S) -> Self {
if self.mock.request.body_contains.is_none() {
self.mock.request.body_contains = Some(Vec::new());
}
self.mock
.request
.body_contains
.as_mut()
.unwrap()
.push(substring.into());
self
}
pub fn expect_body_matches<R: Into<Regex>>(mut self, regex: R) -> Self {
if self.mock.request.body_matches.is_none() {
self.mock.request.body_matches = Some(Vec::new());
}
self.mock
.request
.body_matches
.as_mut()
.unwrap()
.push(Pattern::from_regex(regex.into()));
self
}
pub fn expect_query_param<S: Into<String>>(mut self, name: S, value: S) -> Self {
if self.mock.request.query_param.is_none() {
self.mock.request.query_param = Some(Vec::new());
}
self.mock
.request
.query_param
.as_mut()
.unwrap()
.push((name.into(), value.into()));
self
}
pub fn expect_query_param_exists<S: Into<String>>(mut self, name: S) -> Self {
if self.mock.request.query_param_exists.is_none() {
self.mock.request.query_param_exists = Some(Vec::new());
}
self.mock
.request
.query_param_exists
.as_mut()
.unwrap()
.push(name.into());
self
}
pub fn expect_match(mut self, request_matcher: MockMatcherFunction) -> Self {
if self.mock.request.matchers.is_none() {
self.mock.request.matchers = Some(Vec::new());
}
self.mock
.request
.matchers
.as_mut()
.unwrap()
.push(request_matcher);
self
}
pub fn return_status(mut self, status: u16) -> Self {
self.mock.response.status = Some(status);
self
}
pub fn return_body(mut self, body: impl AsRef<[u8]>) -> Self {
self.mock.response.body = Some(body.as_ref().to_vec());
self
}
pub fn return_body_from_file<S: Into<String>>(mut self, resource_file_path: S) -> Self {
let resource_file_path = resource_file_path.into();
let path = Path::new(&resource_file_path);
let absolute_path = match path.is_absolute() {
true => path.to_path_buf(),
false => get_test_resource_file_path(&resource_file_path).expect(&format!(
"Cannot create absolute path from string '{}'",
&resource_file_path
)),
};
let content = read_file(&absolute_path).expect(&format!(
"Cannot read from file {}",
absolute_path.to_str().expect("Invalid OS path")
));
self.return_body(content)
}
pub fn return_json_body<V: Into<Value>>(mut self, body: V) -> Self {
self.mock.response.body = Some(body.into().to_string().into_bytes());
self
}
pub fn return_json_body_obj<T>(self, body: &T) -> Self
where
T: Serialize,
{
let json_body =
serde_json::to_value(body).expect("cannot serialize json body to JSON string ");
self.return_json_body(json_body)
}
pub fn return_header<S: Into<String>>(mut self, name: S, value: S) -> Self {
if self.mock.response.headers.is_none() {
self.mock.response.headers = Some(Vec::new());
}
self.mock
.response
.headers
.as_mut()
.unwrap()
.push((name.into(), value.into()));
self
}
#[deprecated(
since = "0.5.6",
note = "Please use desired response code and headers instead"
)]
pub fn return_permanent_redirect<S: Into<String>>(mut self, redirect_url: S) -> Self {
if self.mock.response.status.is_none() {
self = self.return_status(301);
}
if self.mock.response.body.is_none() {
self = self.return_body("Moved Permanently");
}
self.return_header("Location", &redirect_url.into())
}
#[deprecated(
since = "0.5.6",
note = "Please use desired response code and headers instead"
)]
pub fn return_temporary_redirect<S: Into<String>>(mut self, redirect_url: S) -> Self {
if self.mock.response.status.is_none() {
self = self.return_status(302);
}
if self.mock.response.body.is_none() {
self = self.return_body("Found");
}
self.return_header("Location", &redirect_url.into())
}
pub fn return_with_delay<D: Into<Duration>>(mut self, duration: D) -> Self {
self.mock.response.delay = Some(duration.into());
self
}
pub fn create_on<'a>(self, server: &'a MockServer) -> MockRef<'a> {
self.create_on_async(server).join()
}
pub async fn create_on_async<'a>(self, server: &'a MockServer) -> MockRef<'a> {
let response = server
.server_adapter
.as_ref()
.unwrap()
.create_mock(&self.mock)
.await
.expect("Cannot deserialize mock server response");
MockRef {
id: response.mock_id,
server,
}
}
}
impl Default for Mock {
fn default() -> Self {
Self::new()
}
}
fn create_reason_output(reason: &Reason) -> String {
let mut output = String::new();
let offsets = match reason.best_match {
true => ("\t".repeat(5), "\t".repeat(2)),
false => ("\t".repeat(1), "\t".repeat(2)),
};
let actual_text = match reason.best_match {
true => "Actual (closest match):",
false => "Actual:",
};
output.push_str(&format!(
"Expected:{}[{}]\t\t{}\n",
offsets.0, reason.comparison, &reason.expected
));
output.push_str(&format!(
"{}{}{}\t{}\n",
actual_text,
offsets.1,
" ".repeat(reason.comparison.len() + 7),
&reason.actual
));
output
}
fn create_diff_result_output(dd: &DiffResult) -> String {
let mut output = String::new();
output.push_str("Diff:");
if dd.differences.is_empty() {
output.push_str("<empty>");
}
output.push_str("\n");
dd.differences.iter().for_each(|d| {
match d {
Diff::Same(e) => {
output.push_str(&format!(" | {}", e));
}
Diff::Add(e) => {
#[cfg(feature = "color")]
output.push_str(&format!("+++| {}", e).green().to_string());
#[cfg(not(feature = "color"))]
output.push_str(&format!("+++| {}", e));
}
Diff::Rem(e) => {
#[cfg(feature = "color")]
output.push_str(&format!("---| {}", e).red().to_string());
#[cfg(not(feature = "color"))]
output.push_str(&format!("---| {}", e));
}
}
output.push_str("\n")
});
output.push_str("\n");
output
}
fn create_mismatch_output(idx: usize, mm: &Mismatch) -> String {
let mut output = String::new();
output.push_str(&format!("{} : {}", idx + 1, &mm.title));
output.push_str("\n");
output.push_str(&"-".repeat(90));
output.push_str("\n");
mm.reason
.as_ref()
.map(|reason| output.push_str(&create_reason_output(reason)));
mm.diff
.as_ref()
.map(|diff_result| output.push_str(&create_diff_result_output(diff_result)));
output.push_str("\n");
output
}
fn fail_with(actual_hits: usize, expected_hits: usize, closest_match: Option<ClosestMatch>) {
match closest_match {
None => assert!(false, "No request has been received by the mock server."),
Some(closest_match) => {
let mut output = String::new();
output.push_str(&format!(
"{} of {} expected requests matched the mock specification, .\n",
actual_hits, expected_hits
));
output.push_str(&format!(
"Here is a comparison with the most similar non-matching request (request number {}): \n\n",
closest_match.request_index + 1
));
for (idx, mm) in closest_match.mismatches.iter().enumerate() {
output.push_str(&create_mismatch_output(idx, &mm));
}
closest_match.mismatches.first().map(|mismatch| {
mismatch
.reason
.as_ref()
.map(|reason| assert_eq!(reason.expected, reason.actual, "{}", output))
});
assert!(false, output)
}
}
}
#[cfg(test)]
mod test {
use crate::api::mock::fail_with;
use crate::data::{ClosestMatch, HttpMockRequest};
use crate::server::{Diff, DiffResult, Mismatch, Reason, Tokenizer};
use crate::Mock;
#[test]
#[should_panic(expected = "1 : This is a title\n\
------------------------------------------------------------------------------------------\n\
Expected: [equals] /toast\n\
Actual: /test\n\
Diff:\n | t\n---| e\n+++| oa\n | st")]
fn fail_with_message_test() {
let closest_match = ClosestMatch {
request: HttpMockRequest {
path: "/test".to_string(),
method: "GET".to_string(),
headers: None,
query_params: None,
body: None,
},
request_index: 0,
mismatches: vec![Mismatch {
title: "This is a title".to_string(),
reason: Some(Reason {
expected: "/toast".to_string(),
actual: "/test".to_string(),
comparison: "equals".to_string(),
best_match: false,
}),
diff: Some(DiffResult {
differences: vec![
Diff::Same(String::from("t")),
Diff::Rem(String::from("e")),
Diff::Add(String::from("oa")),
Diff::Same(String::from("st")),
],
distance: 5,
tokenizer: Tokenizer::Line,
}),
}],
};
fail_with(1, 2, Some(closest_match));
}
}