#![warn(missing_docs)]
#![doc(html_logo_url = "https://raw.githubusercontent.com/lipanski/mockito/master/docs/logo-black.png")]
extern crate httparse;
extern crate rand;
extern crate regex;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate log;
extern crate serde_json;
extern crate difference;
extern crate colored;
mod server;
mod request;
mod response;
mod diff;
type Request = request::Request;
type Response = response::Response;
use std::fs::File;
use std::io::Read;
use std::convert::{From, Into};
use std::ops::Drop;
use std::fmt;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use regex::Regex;
use std::sync::{Mutex, LockResult, MutexGuard};
use std::cell::RefCell;
lazy_static! {
static ref TEST_MUTEX: Mutex<()> = Mutex::new(());
}
thread_local!(
static LOCAL_TEST_MUTEX: RefCell<LockResult<MutexGuard<'static, ()>>> = RefCell::new(TEST_MUTEX.lock());
);
#[deprecated(note="Call server_address() instead")]
pub const SERVER_ADDRESS: &str = SERVER_ADDRESS_INTERNAL;
const SERVER_ADDRESS_INTERNAL: &str = "127.0.0.1:1234";
#[deprecated(note="Call server_url() instead")]
pub const SERVER_URL: &str = "http://127.0.0.1:1234";
pub use server::address as server_address;
pub use server::url as server_url;
pub fn mock<P: Into<Matcher>>(method: &str, path: P) -> Mock {
Mock::new(method, path)
}
pub fn reset() {
server::try_start();
let mut state = server::STATE.lock().unwrap();
state.mocks.clear();
}
#[allow(missing_docs)]
pub fn start() {
server::try_start();
}
#[derive(Clone, PartialEq, Debug)]
#[allow(deprecated)] pub enum Matcher {
Exact(String),
Regex(String),
Json(serde_json::Value),
JsonString(String),
AnyOf(Vec<Matcher>),
Any,
Missing,
}
impl<'a> From<&'a str> for Matcher {
fn from(value: &str) -> Self {
Matcher::Exact(value.to_string())
}
}
impl Matcher {
fn matches_values(&self, header_values: &[&str]) -> bool {
match self {
Matcher::Missing => header_values.is_empty(),
Matcher::AnyOf(ref matchers) if header_values.is_empty() => {
matchers.iter().any(|m| m.matches_values(header_values))
},
_ => !header_values.is_empty() && header_values.iter().all(|val| self.matches_value(val)),
}
}
#[allow(deprecated)]
fn matches_value(&self, other: &str) -> bool {
match self {
Matcher::Exact(ref value) => { value == other },
Matcher::Regex(ref regex) => { Regex::new(regex).unwrap().is_match(other) },
Matcher::Json(ref json_obj) => {
let other: serde_json::Value = serde_json::from_str(other).unwrap();
*json_obj == other
},
Matcher::JsonString(ref value) => {
let value: serde_json::Value = serde_json::from_str(value).unwrap();
let other: serde_json::Value = serde_json::from_str(other).unwrap();
value == other
},
Matcher::Any => true,
Matcher::AnyOf(ref matchers) => {
matchers.iter().any(|m| m.matches_value(other))
},
Matcher::Missing => false,
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct Mock {
id: String,
method: String,
path: Matcher,
headers: Vec<(String, Matcher)>,
body: Matcher,
response: Response,
hits: usize,
expected_hits: usize,
is_remote: bool,
}
impl Mock {
fn new<P: Into<Matcher>>(method: &str, path: P) -> Self {
Self {
id: thread_rng().sample_iter(&Alphanumeric).take(24).collect(),
method: method.to_owned().to_uppercase(),
path: path.into(),
headers: Vec::new(),
body: Matcher::Any,
response: Response::default(),
hits: 0,
expected_hits: 1,
is_remote: false,
}
}
pub fn match_header<M: Into<Matcher>>(mut self, field: &str, value: M) -> Self {
self.headers.push((field.to_owned().to_lowercase(), value.into()));
self
}
pub fn match_body<M: Into<Matcher>>(mut self, body: M) -> Self {
self.body = body.into();
self
}
pub fn with_status(mut self, status: usize) -> Self {
self.response.status = status.into();
self
}
pub fn with_header(mut self, field: &str, value: &str) -> Self {
self.response.headers.push((field.to_owned(), value.to_owned()));
self
}
pub fn with_body<StrOrBytes: AsRef<[u8]>>(mut self, body: StrOrBytes) -> Self {
self.response.body = body.as_ref().to_owned();
self
}
pub fn with_body_from_file(mut self, path: &str) -> Self {
let mut file = File::open(path).unwrap();
let mut body = Vec::new();
file.read_to_end(&mut body).unwrap();
self.response.body = body;
self
}
pub fn expect(mut self, hits: usize) -> Self {
self.expected_hits = hits;
self
}
pub fn assert(&self) {
let mut opt_hits = None;
let mut opt_message = None;
{
let state = server::STATE.lock().unwrap();
if let Some(remote_mock) = state.mocks.iter().find(|mock| mock.id == self.id) {
opt_hits = Some(remote_mock.hits);
let mut message = format!("\n> Expected {} request(s) to:\n{}\n...but received {}\n\n", self.expected_hits, self, remote_mock.hits);
if let Some(last_request) = state.unmatched_requests.last() {
message.push_str(&format!("> The last unmatched request was:\n{}\n", last_request));
let difference = diff::compare(&self.to_string(), &last_request.to_string());
message.push_str(&format!("> Difference:\n{}\n", difference));
}
opt_message = Some(message);
}
}
match (opt_hits, opt_message) {
(Some(hits), Some(message)) => assert_eq!(self.expected_hits, hits, "{}", message),
_ => panic!("Could not retrieve enough information about the remote mock."),
}
}
pub fn create(self) -> Self {
server::try_start();
LOCAL_TEST_MUTEX.with(|_| {});
let mut state = server::STATE.lock().unwrap();
let mut remote_mock = self.clone();
remote_mock.is_remote = true;
state.mocks.push(remote_mock);
debug!("Mock::create() called for {}", self);
self
}
fn is_local(&self) -> bool {
!self.is_remote
}
}
impl Drop for Mock {
fn drop(&mut self) {
if self.is_local() {
let mut state = server::STATE.lock().unwrap();
if let Some(pos) = state.mocks.iter().position(|mock| mock.id == self.id) {
state.mocks.remove(pos);
}
debug!("Mock::drop() called for {}", self);
}
}
}
impl fmt::Display for Mock {
#[allow(deprecated)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut formatted = String::new();
formatted.push_str("\r\n");
formatted.push_str(&self.method);
formatted.push_str(" ");
match self.path {
Matcher::Exact(ref value) => {
formatted.push_str(value);
formatted.push_str("\r\n");
},
Matcher::Regex(ref value) => {
formatted.push_str(value);
formatted.push_str(" (regex)\r\n")
},
Matcher::Json(ref json_obj) => {
formatted.push_str(&json_obj.to_string());
formatted.push_str(" (json)\r\n")
},
Matcher::JsonString(ref value) => {
formatted.push_str(value);
formatted.push_str(" (json)\r\n")
},
Matcher::Any => formatted.push_str("(any)\r\n"),
Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"),
Matcher::Missing => formatted.push_str("(missing)\r\n"),
}
for &(ref key, ref value) in &self.headers {
match value {
Matcher::Exact(ref value) => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str(value);
},
Matcher::Regex(ref value) => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str(value);
formatted.push_str(" (regex)")
},
Matcher::Json(ref json_obj) => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str(&json_obj.to_string());
formatted.push_str(" (json)")
},
Matcher::JsonString(ref value) => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str(value);
formatted.push_str(" (json)")
},
Matcher::Any => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str("(any)");
},
Matcher::Missing => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str("(missing)");
},
Matcher::AnyOf(..) => {
formatted.push_str(key);
formatted.push_str(": ");
formatted.push_str("(any of)");
},
}
formatted.push_str("\r\n");
}
match self.body {
Matcher::Exact(ref value) | Matcher::JsonString(ref value) | Matcher::Regex(ref value) => {
formatted.push_str(value);
formatted.push_str("\r\n");
},
Matcher::Json(ref json_obj) => {
formatted.push_str(&json_obj.to_string());
formatted.push_str("\r\n")
},
Matcher::Missing => formatted.push_str("(missing)\r\n"),
Matcher::AnyOf(..) => formatted.push_str("(any of)\r\n"),
Matcher::Any => {}
}
f.write_str(&formatted)
}
}