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::server::MockServer;
use crate::api::{Method, Regex};
use crate::common::data::{ClosestMatch, Diff, DiffResult, Mismatch, Reason};
use crate::common::util::{get_test_resource_file_path, read_file, Join};
pub struct Mock<'a> {
pub id: usize,
pub(crate) server: &'a MockServer,
}
impl<'a> Mock<'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()
}
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
}
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 MockExt<'a> {
fn new(id: usize, mock_server: &'a MockServer) -> Mock<'a>;
fn id(&self) -> usize;
}
impl<'a> MockExt<'a> for Mock<'a> {
fn new(id: usize, mock_server: &'a MockServer) -> Mock<'a> {
Mock {
id,
server: mock_server,
}
}
fn id(&self) -> usize {
self.id
}
}
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::common::data::{
ClosestMatch, Diff, DiffResult, HttpMockRequest, Mismatch, Reason, Tokenizer,
};
#[test]
#[cfg(not(feature = "color"))]
#[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.0,
tokenizer: Tokenizer::Line,
}),
}],
};
fail_with(1, 2, Some(closest_match));
}
}