use super::Method;
use super::Response;
use super::response::HeaderPair;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
static MOCK_ACTIVE: AtomicBool = AtomicBool::new(false);
static MOCK_REGISTRY: Mutex<Option<Vec<MockEntry>>> = Mutex::new(None);
struct MockEntry {
method: Option<Method>,
url: Box<str>,
status: u16,
body: bytes::Bytes,
headers: Arc<[HeaderPair]>,
call_count: Arc<AtomicUsize>,
}
fn with_registry<F, R>(f: F) -> R
where
F: FnOnce(&mut Vec<MockEntry>) -> R,
{
let mut guard = MOCK_REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
let entries = guard.get_or_insert_with(Vec::new);
f(entries)
}
pub(crate) fn try_intercept(method: Method, url: &str) -> Option<Response> {
if !MOCK_ACTIVE.load(Ordering::Acquire) {
return None;
}
with_registry(|entries| {
let entry = find_mock_entry(entries, method, url)?;
entry.call_count.fetch_add(1, Ordering::Release);
let headers: Vec<HeaderPair> = entry
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Some(Response::new(entry.status, entry.body.clone(), headers))
})
}
fn find_mock_entry<'a>(
entries: &'a [MockEntry],
method: Method,
url: &str,
) -> Option<&'a MockEntry> {
entries
.iter()
.find(|e| e.url.as_ref() == url && e.method == Some(method))
.or_else(|| {
entries
.iter()
.find(|e| e.url.as_ref() == url && e.method.is_none())
})
}
pub fn http(url: &str) -> MockHttpBuilder {
MockHttpBuilder {
method: None,
url: url.into(),
response: None,
}
}
pub fn http_method(method: Method, url: &str) -> MockHttpBuilder {
MockHttpBuilder {
method: Some(method),
url: url.into(),
response: None,
}
}
pub struct MockHttpBuilder {
method: Option<Method>,
url: Box<str>,
response: Option<Response>,
}
impl MockHttpBuilder {
pub fn returns(mut self, response: Response) -> MockHttp {
self.response = Some(response);
self.install()
}
fn install(self) -> MockHttp {
let resp = match self.response {
Some(r) => r,
None => Response::empty_raw(200),
};
let call_count = Arc::new(AtomicUsize::new(0));
let method = self.method;
let url = self.url.clone();
let entry = MockEntry {
method,
url: self.url,
status: resp.status(),
body: bytes::Bytes::copy_from_slice(resp.body_bytes()),
headers: resp.headers().to_vec().into(),
call_count: Arc::clone(&call_count),
};
with_registry(|entries| {
entries.push(entry);
MOCK_ACTIVE.store(true, Ordering::Release);
});
MockHttp {
method,
url,
call_count,
}
}
}
pub struct MockHttp {
method: Option<Method>,
url: Box<str>,
call_count: Arc<AtomicUsize>,
}
impl MockHttp {
pub fn assert_called_once(&self) {
let count = self.call_count.load(Ordering::Acquire);
assert!(
count == 1,
"expected mock for {} {} to be called once, was called {count} times",
match self.method {
Some(m) => m.as_str(),
None => "*",
},
self.url
);
}
}
impl Drop for MockHttp {
fn drop(&mut self) {
let method = self.method;
let url = &self.url;
with_registry(|entries| {
entries.retain(|e| !(e.url == *url && e.method == method));
if entries.is_empty() {
MOCK_ACTIVE.store(false, Ordering::Release);
}
});
}
}