use reqwest::blocking::Client;
use serde_json::Value;
use crate::breakpoint::{
BreakpointMatcherList, BreakpointMatcherRegistration, BreakpointMatcherResponse,
BreakpointRequestHandler, BreakpointResponseHandler, BreakpointStreamFrameHandler,
BreakpointWebSocketClient,
};
use crate::error::{Error, Result};
use crate::model::*;
pub struct ClientBuilder {
host: String,
port: u16,
context_path: String,
secure: bool,
tls_verify: bool,
}
impl ClientBuilder {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Self {
host: host.into(),
port,
context_path: String::new(),
secure: false,
tls_verify: true,
}
}
pub fn context_path(mut self, path: impl Into<String>) -> Self {
self.context_path = path.into();
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn tls_verify(mut self, verify: bool) -> Self {
self.tls_verify = verify;
self
}
pub fn build(self) -> Result<MockServerClient> {
let scheme = if self.secure { "https" } else { "http" };
let ctx = if self.context_path.is_empty() {
String::new()
} else if self.context_path.starts_with('/') {
self.context_path
} else {
format!("/{}", self.context_path)
};
let base_url = format!("{scheme}://{}:{}{ctx}", self.host, self.port);
let http_client = Client::builder()
.danger_accept_invalid_certs(!self.tls_verify)
.build()?;
Ok(MockServerClient {
base_url,
http: http_client,
breakpoint_ws: std::sync::Mutex::new(None),
})
}
}
pub struct MockServerClient {
pub(crate) base_url: String,
http: Client,
breakpoint_ws: std::sync::Mutex<Option<BreakpointWebSocketClient>>,
}
impl MockServerClient {
pub fn upsert(&self, expectations: &[Expectation]) -> Result<Vec<Expectation>> {
let body = serde_json::to_value(expectations)?;
let resp = self
.http
.put(self.url("/mockserver/expectation"))
.json(&body)
.send()?;
let status = resp.status().as_u16();
match status {
200 | 201 => {
let text = resp.text()?;
if text.is_empty() {
Ok(expectations.to_vec())
} else {
Ok(serde_json::from_str(&text)?)
}
}
400 => Err(Error::InvalidRequest(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn when(&self, request: HttpRequest) -> ForwardChainExpectation<'_> {
ForwardChainExpectation {
client: self,
request,
times: None,
time_to_live: None,
priority: None,
id: None,
}
}
pub fn verify(&self, request: HttpRequest, times: VerificationTimes) -> Result<()> {
let verification = Verification {
http_request: Some(request),
http_response: None,
times: Some(times),
maximum_number_of_request_to_return_in_verification_failure: None,
};
self.do_verify(&verification)
}
pub fn verify_request_and_response(
&self,
request: HttpRequest,
response: HttpResponse,
times: VerificationTimes,
) -> Result<()> {
let verification = Verification {
http_request: Some(request),
http_response: Some(response),
times: Some(times),
maximum_number_of_request_to_return_in_verification_failure: None,
};
self.do_verify(&verification)
}
pub fn verify_response(
&self,
response: HttpResponse,
times: VerificationTimes,
) -> Result<()> {
let verification = Verification {
http_request: None,
http_response: Some(response),
times: Some(times),
maximum_number_of_request_to_return_in_verification_failure: None,
};
self.do_verify(&verification)
}
pub fn verify_raw(&self, verification: &Verification) -> Result<()> {
self.do_verify(verification)
}
pub fn verify_sequence(&self, requests: Vec<HttpRequest>) -> Result<()> {
let verification = VerificationSequence {
http_requests: Some(requests),
http_responses: None,
};
self.do_verify_sequence(&verification)
}
pub fn verify_sequence_with_responses(
&self,
requests: Vec<HttpRequest>,
responses: Vec<HttpResponse>,
) -> Result<()> {
let verification = VerificationSequence {
http_requests: Some(requests),
http_responses: Some(responses),
};
self.do_verify_sequence(&verification)
}
pub fn verify_sequence_raw(&self, verification: &VerificationSequence) -> Result<()> {
self.do_verify_sequence(verification)
}
pub fn clear(
&self,
request: Option<&HttpRequest>,
clear_type: Option<ClearType>,
) -> Result<()> {
let mut url = self.url("/mockserver/clear");
if let Some(ct) = clear_type {
url = format!("{url}?type={}", ct.as_str());
}
let mut builder = self.http.put(&url);
builder = builder.header("Content-Type", "application/json");
if let Some(req) = request {
builder = builder.json(req);
} else {
builder = builder.body("");
}
let resp = builder.send()?;
let status = resp.status().as_u16();
match status {
200 => Ok(()),
400 => Err(Error::InvalidRequest(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn clear_by_id(
&self,
expectation_id: impl Into<String>,
clear_type: Option<ClearType>,
) -> Result<()> {
let mut url = self.url("/mockserver/clear");
if let Some(ct) = clear_type {
url = format!("{url}?type={}", ct.as_str());
}
let body = serde_json::json!({ "id": expectation_id.into() });
let resp = self.http.put(&url).json(&body).send()?;
let status = resp.status().as_u16();
match status {
200 => Ok(()),
400 => Err(Error::InvalidRequest(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn reset(&self) -> Result<()> {
let resp = self
.http
.put(self.url("/mockserver/reset"))
.header("Content-Type", "application/json")
.body("")
.send()?;
let status = resp.status().as_u16();
match status {
200 => Ok(()),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn retrieve_recorded_requests(
&self,
request: Option<&HttpRequest>,
) -> Result<Vec<HttpRequest>> {
let text =
self.do_retrieve(request, RetrieveType::Requests, RetrieveFormat::Json)?;
if text.is_empty() {
return Ok(vec![]);
}
Ok(serde_json::from_str(&text)?)
}
pub fn retrieve_active_expectations(
&self,
request: Option<&HttpRequest>,
) -> Result<Vec<Expectation>> {
let text = self.do_retrieve(
request,
RetrieveType::ActiveExpectations,
RetrieveFormat::Json,
)?;
if text.is_empty() {
return Ok(vec![]);
}
Ok(serde_json::from_str(&text)?)
}
pub fn retrieve_recorded_expectations(
&self,
request: Option<&HttpRequest>,
) -> Result<Vec<Expectation>> {
let text = self.do_retrieve(
request,
RetrieveType::RecordedExpectations,
RetrieveFormat::Json,
)?;
if text.is_empty() {
return Ok(vec![]);
}
Ok(serde_json::from_str(&text)?)
}
pub fn retrieve_log_messages(
&self,
request: Option<&HttpRequest>,
) -> Result<Vec<String>> {
let text =
self.do_retrieve(request, RetrieveType::Logs, RetrieveFormat::LogEntries)?;
if text.is_empty() {
return Ok(vec![]);
}
if let Ok(arr) = serde_json::from_str::<Vec<String>>(&text) {
return Ok(arr);
}
Ok(text
.split("------------------------------------\n")
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.collect())
}
pub fn retrieve_request_responses(
&self,
request: Option<&HttpRequest>,
) -> Result<Vec<Value>> {
let text = self.do_retrieve(
request,
RetrieveType::RequestResponses,
RetrieveFormat::Json,
)?;
if text.is_empty() {
return Ok(vec![]);
}
Ok(serde_json::from_str(&text)?)
}
pub fn status(&self) -> Result<Ports> {
let resp = self
.http
.put(self.url("/mockserver/status"))
.header("Content-Type", "application/json")
.body("")
.send()?;
let status = resp.status().as_u16();
match status {
200 => {
let text = resp.text()?;
Ok(serde_json::from_str(&text)?)
}
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn bind(&self, ports: &[u16]) -> Result<Ports> {
let body = Ports {
ports: ports.to_vec(),
};
let resp = self
.http
.put(self.url("/mockserver/bind"))
.json(&body)
.send()?;
let status = resp.status().as_u16();
match status {
200 => {
let text = resp.text()?;
Ok(serde_json::from_str(&text)?)
}
400 => Err(Error::InvalidRequest(resp.text()?)),
406 => Err(Error::VerificationFailure(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn has_started(&self, attempts: u32, timeout_ms: u64) -> bool {
for i in 0..attempts {
match self.status() {
Ok(_) => return true,
Err(_) => {
if i < attempts - 1 {
std::thread::sleep(std::time::Duration::from_millis(timeout_ms));
}
}
}
}
false
}
fn ensure_breakpoint_ws(&self) -> Result<String> {
let mut guard = self.breakpoint_ws.lock().unwrap();
let needs_connect = match guard.as_ref() {
None => true,
Some(ws) => ws.is_dead(),
};
if needs_connect {
if let Some(old) = guard.take() {
old.close();
}
let ws = BreakpointWebSocketClient::connect(&self.base_url)?;
*guard = Some(ws);
}
Ok(guard.as_ref().unwrap().client_id.clone())
}
pub fn add_breakpoint(
&self,
matcher: HttpRequest,
phases: &[&str],
request_handler: Option<BreakpointRequestHandler>,
response_handler: Option<BreakpointResponseHandler>,
stream_frame_handler: Option<BreakpointStreamFrameHandler>,
) -> Result<String> {
if phases.is_empty() {
return Err(Error::InvalidRequest(
"At least one phase is required".into(),
));
}
let client_id = self.ensure_breakpoint_ws()?;
let reg = BreakpointMatcherRegistration {
http_request: matcher,
phases: phases.iter().map(|s| s.to_string()).collect(),
client_id: Some(client_id),
};
let resp = self
.http
.put(self.url("/mockserver/breakpoint/matcher"))
.json(®)
.send()?;
let status = resp.status().as_u16();
let text = resp.text()?;
if status >= 400 {
return Err(Error::UnexpectedStatus {
status,
body: text,
});
}
let result: BreakpointMatcherResponse = serde_json::from_str(&text)?;
let id = result.id.clone();
let guard = self.breakpoint_ws.lock().unwrap();
if let Some(ws) = guard.as_ref() {
if let Some(h) = request_handler {
ws.set_request_handler(&id, h);
}
if let Some(h) = response_handler {
ws.set_response_handler(&id, h);
}
if let Some(h) = stream_frame_handler {
ws.set_stream_frame_handler(&id, h);
}
}
Ok(id)
}
pub fn add_request_breakpoint(
&self,
matcher: HttpRequest,
handler: BreakpointRequestHandler,
) -> Result<String> {
self.add_breakpoint(
matcher,
&[crate::breakpoint::phase::REQUEST],
Some(handler),
None,
None,
)
}
pub fn add_request_response_breakpoint(
&self,
matcher: HttpRequest,
request_handler: BreakpointRequestHandler,
response_handler: BreakpointResponseHandler,
) -> Result<String> {
self.add_breakpoint(
matcher,
&[
crate::breakpoint::phase::REQUEST,
crate::breakpoint::phase::RESPONSE,
],
Some(request_handler),
Some(response_handler),
None,
)
}
pub fn add_stream_breakpoint(
&self,
matcher: HttpRequest,
phases: &[&str],
handler: BreakpointStreamFrameHandler,
) -> Result<String> {
self.add_breakpoint(matcher, phases, None, None, Some(handler))
}
pub fn list_breakpoint_matchers(&self) -> Result<BreakpointMatcherList> {
let resp = self
.http
.get(self.url("/mockserver/breakpoint/matchers"))
.send()?;
let status = resp.status().as_u16();
let text = resp.text()?;
if status >= 400 {
return Err(Error::UnexpectedStatus {
status,
body: text,
});
}
Ok(serde_json::from_str(&text)?)
}
pub fn remove_breakpoint_matcher(&self, id: impl Into<String>) -> Result<()> {
let id = id.into();
let body = serde_json::json!({ "id": &id });
let resp = self
.http
.put(self.url("/mockserver/breakpoint/matcher/remove"))
.json(&body)
.send()?;
let status = resp.status().as_u16();
match status {
200 => {
let guard = self.breakpoint_ws.lock().unwrap();
if let Some(ws) = guard.as_ref() {
ws.remove_handlers(&id);
}
Ok(())
}
404 => Err(Error::InvalidRequest(format!(
"Breakpoint matcher not found: {id}"
))),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
pub fn clear_breakpoint_matchers(&self) -> Result<()> {
let resp = self
.http
.put(self.url("/mockserver/breakpoint/matcher/clear"))
.header("Content-Type", "application/json")
.body("")
.send()?;
let status = resp.status().as_u16();
if status >= 400 {
return Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
});
}
let guard = self.breakpoint_ws.lock().unwrap();
if let Some(ws) = guard.as_ref() {
ws.clear_handlers();
}
Ok(())
}
pub fn close_breakpoint_websocket(&self) {
let mut guard = self.breakpoint_ws.lock().unwrap();
if let Some(ws) = guard.take() {
ws.close();
}
}
fn do_verify(&self, verification: &Verification) -> Result<()> {
let resp = self
.http
.put(self.url("/mockserver/verify"))
.json(verification)
.send()?;
let status = resp.status().as_u16();
match status {
200 | 202 => Ok(()),
406 => Err(Error::VerificationFailure(resp.text()?)),
400 => Err(Error::InvalidRequest(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
fn do_verify_sequence(&self, verification: &VerificationSequence) -> Result<()> {
let resp = self
.http
.put(self.url("/mockserver/verifySequence"))
.json(verification)
.send()?;
let status = resp.status().as_u16();
match status {
200 | 202 => Ok(()),
406 => Err(Error::VerificationFailure(resp.text()?)),
400 => Err(Error::InvalidRequest(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
fn url(&self, path: &str) -> String {
format!("{}{path}", self.base_url)
}
fn do_retrieve(
&self,
request: Option<&HttpRequest>,
retrieve_type: RetrieveType,
format: RetrieveFormat,
) -> Result<String> {
let url = format!(
"{}?type={}&format={}",
self.url("/mockserver/retrieve"),
retrieve_type.as_str(),
format.as_str(),
);
let mut builder = self.http.put(&url);
builder = builder.header("Content-Type", "application/json");
if let Some(req) = request {
builder = builder.json(req);
} else {
builder = builder.body("");
}
let resp = builder.send()?;
let status = resp.status().as_u16();
match status {
200 => Ok(resp.text()?),
400 => Err(Error::InvalidRequest(resp.text()?)),
_ => Err(Error::UnexpectedStatus {
status,
body: resp.text().unwrap_or_default(),
}),
}
}
}
pub struct ForwardChainExpectation<'a> {
client: &'a MockServerClient,
request: HttpRequest,
times: Option<Times>,
time_to_live: Option<TimeToLive>,
priority: Option<i32>,
id: Option<String>,
}
impl<'a> ForwardChainExpectation<'a> {
pub fn times(mut self, times: Times) -> Self {
self.times = Some(times);
self
}
pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
self.time_to_live = Some(ttl);
self
}
pub fn priority(mut self, priority: i32) -> Self {
self.priority = Some(priority);
self
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn respond(self, response: HttpResponse) -> Result<Vec<Expectation>> {
let (client, expectation) = self.into_parts();
let expectation = expectation.respond(response);
client.upsert(&[expectation])
}
pub fn forward(self, forward: HttpForward) -> Result<Vec<Expectation>> {
let (client, expectation) = self.into_parts();
let expectation = expectation.forward(forward);
client.upsert(&[expectation])
}
pub fn error(self, error: HttpError) -> Result<Vec<Expectation>> {
let (client, expectation) = self.into_parts();
let expectation = expectation.error(error);
client.upsert(&[expectation])
}
fn into_parts(self) -> (&'a MockServerClient, Expectation) {
let ForwardChainExpectation {
client,
request,
times,
time_to_live,
priority,
id,
} = self;
let mut exp = Expectation::new(request);
exp.times = times;
exp.time_to_live = time_to_live;
exp.priority = priority;
exp.id = id;
(client, exp)
}
}