mockserver_client/client.rs
1//! The MockServer client and its builder.
2
3use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
4use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
5use reqwest::blocking::Client;
6use serde::Serialize;
7use serde_json::Value;
8
9/// Characters percent-encoded when interpolating a scenario name into a path
10/// segment — everything unsafe for a single segment (matches the other clients'
11/// path-escaping). Unreserved characters (`-` `_` `.` `~`) are left intact.
12const SCENARIO_NAME: &AsciiSet = &CONTROLS
13 .add(b' ')
14 .add(b'/')
15 .add(b'?')
16 .add(b'#')
17 .add(b'%')
18 .add(b'[')
19 .add(b']')
20 .add(b'{')
21 .add(b'}')
22 .add(b'|')
23 .add(b'\\')
24 .add(b'^')
25 .add(b'"')
26 .add(b'<')
27 .add(b'>')
28 .add(b'`');
29
30/// Build the control-plane path for a named scenario with the name
31/// percent-encoded as a single path segment (matching the other MockServer clients).
32fn scenario_path(name: &str) -> String {
33 format!(
34 "/mockserver/scenario/{}",
35 utf8_percent_encode(name, SCENARIO_NAME)
36 )
37}
38
39use crate::breakpoint::{
40 BreakpointMatcherList, BreakpointMatcherRegistration, BreakpointMatcherResponse,
41 BreakpointRequestHandler, BreakpointResponseHandler, BreakpointStreamFrameHandler,
42 BreakpointWebSocketClient,
43};
44use crate::error::{Error, Result};
45use crate::model::*;
46
47// ---------------------------------------------------------------------------
48// ClientBuilder
49// ---------------------------------------------------------------------------
50
51/// Builder for constructing a [`MockServerClient`].
52///
53/// # Example
54/// ```no_run
55/// use mockserver_client::ClientBuilder;
56///
57/// let client = ClientBuilder::new("localhost", 1080)
58/// .context_path("/api")
59/// .secure(true)
60/// .build()
61/// .unwrap();
62/// ```
63pub struct ClientBuilder {
64 host: String,
65 port: u16,
66 context_path: String,
67 secure: bool,
68 tls_verify: bool,
69 control_plane_bearer_token: Option<String>,
70 ca_cert_pem: Option<Vec<u8>>,
71 /// Client identity for mTLS as `(certificate PEM, private key PEM)`.
72 client_identity_pem: Option<(Vec<u8>, Vec<u8>)>,
73}
74
75impl ClientBuilder {
76 /// Create a new builder targeting the given host and port.
77 pub fn new(host: impl Into<String>, port: u16) -> Self {
78 Self {
79 host: host.into(),
80 port,
81 context_path: String::new(),
82 secure: false,
83 tls_verify: true,
84 control_plane_bearer_token: None,
85 ca_cert_pem: None,
86 client_identity_pem: None,
87 }
88 }
89
90 /// Set a context path prefix (e.g., "/mockserver" if deployed behind a reverse proxy).
91 pub fn context_path(mut self, path: impl Into<String>) -> Self {
92 self.context_path = path.into();
93 self
94 }
95
96 /// Use HTTPS instead of HTTP.
97 pub fn secure(mut self, secure: bool) -> Self {
98 self.secure = secure;
99 self
100 }
101
102 /// Whether to verify TLS certificates (default: true).
103 pub fn tls_verify(mut self, verify: bool) -> Self {
104 self.tls_verify = verify;
105 self
106 }
107
108 /// Attach an `Authorization: Bearer <token>` header to **every control-plane
109 /// request** the built client sends.
110 ///
111 /// Use this when the server requires a JWT on the control plane
112 /// (`mockserver.controlPlaneJWTAuthenticationRequired=true`). The client does
113 /// not generate the token — supply the JWT string here. The header is only
114 /// sent on control-plane (`/mockserver/*`) requests issued by this client; it
115 /// is not added to any proxied/data-plane traffic.
116 ///
117 /// # Example
118 /// ```no_run
119 /// use mockserver_client::ClientBuilder;
120 ///
121 /// let client = ClientBuilder::new("localhost", 1080)
122 /// .secure(true)
123 /// .control_plane_bearer_token("eyJhbGciOi...")
124 /// .build()
125 /// .unwrap();
126 /// ```
127 pub fn control_plane_bearer_token(mut self, token: impl Into<String>) -> Self {
128 self.control_plane_bearer_token = Some(token.into());
129 self
130 }
131
132 /// Trust the given CA certificate (PEM file path) when connecting over HTTPS.
133 ///
134 /// Reads the PEM file and adds it as an additional trusted root so a
135 /// MockServer HTTPS certificate issued by that CA validates. Compose with
136 /// [`secure(true)`](Self::secure). The CA is added to — not a replacement for
137 /// — the platform's default trust store.
138 ///
139 /// Errors from reading the file surface when [`build`](Self::build) is called.
140 pub fn ca_cert_pem_path(mut self, path: impl AsRef<std::path::Path>) -> Self {
141 self.ca_cert_pem = std::fs::read(path.as_ref()).ok();
142 // Defer error reporting to build(); but if the read failed we still want
143 // build() to fail loudly rather than silently ignore the CA, so record a
144 // sentinel empty Vec which Certificate::from_pem will reject.
145 if self.ca_cert_pem.is_none() {
146 self.ca_cert_pem = Some(Vec::new());
147 }
148 self
149 }
150
151 /// Trust the given CA certificate (PEM bytes) when connecting over HTTPS.
152 ///
153 /// In-memory counterpart to [`ca_cert_pem_path`](Self::ca_cert_pem_path).
154 pub fn ca_cert_pem(mut self, bytes: impl Into<Vec<u8>>) -> Self {
155 self.ca_cert_pem = Some(bytes.into());
156 self
157 }
158
159 /// Present a client certificate + private key (PEM) for mutual TLS (mTLS).
160 ///
161 /// Reads the certificate and PKCS#8 private key PEM files and configures them
162 /// as the client identity used in the TLS handshake — required when the
163 /// server enforces `mockserver.controlPlaneTLSMutualAuthenticationRequired`.
164 ///
165 /// Errors from reading either file, or from building the identity, surface
166 /// when [`build`](Self::build) is called.
167 pub fn client_cert_pem(
168 mut self,
169 cert_path: impl AsRef<std::path::Path>,
170 key_path: impl AsRef<std::path::Path>,
171 ) -> Self {
172 match (
173 std::fs::read(cert_path.as_ref()),
174 std::fs::read(key_path.as_ref()),
175 ) {
176 (Ok(cert), Ok(key)) => self.client_identity_pem = Some((cert, key)),
177 // Record a sentinel empty buffer so build() fails loudly rather than
178 // silently dropping the requested client certificate.
179 _ => self.client_identity_pem = Some((Vec::new(), Vec::new())),
180 }
181 self
182 }
183
184 /// Build the client.
185 pub fn build(self) -> Result<MockServerClient> {
186 let scheme = if self.secure { "https" } else { "http" };
187 let ctx = if self.context_path.is_empty() {
188 String::new()
189 } else if self.context_path.starts_with('/') {
190 self.context_path
191 } else {
192 format!("/{}", self.context_path)
193 };
194 let base_url = format!("{scheme}://{}:{}{ctx}", self.host, self.port);
195
196 let mut builder = Client::builder().danger_accept_invalid_certs(!self.tls_verify);
197
198 // Attach the control-plane bearer token as a default header so it rides
199 // on every control-plane request this client issues (this client only
200 // ever talks to the `/mockserver/*` control plane).
201 if let Some(token) = self.control_plane_bearer_token {
202 let mut headers = reqwest::header::HeaderMap::new();
203 let mut value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
204 .map_err(|e| Error::InvalidRequest(format!("invalid bearer token: {e}")))?;
205 value.set_sensitive(true);
206 headers.insert(reqwest::header::AUTHORIZATION, value);
207 builder = builder.default_headers(headers);
208 }
209
210 if let Some(ca) = self.ca_cert_pem {
211 let cert = reqwest::Certificate::from_pem(&ca)?;
212 builder = builder.add_root_certificate(cert);
213 }
214
215 if let Some((cert_pem, key_pem)) = self.client_identity_pem {
216 let identity = reqwest::Identity::from_pkcs8_pem(&cert_pem, &key_pem)?;
217 builder = builder.identity(identity);
218 }
219
220 let http_client = builder.build()?;
221
222 Ok(MockServerClient {
223 base_url,
224 http: http_client,
225 breakpoint_ws: std::sync::Mutex::new(None),
226 })
227 }
228}
229
230// ---------------------------------------------------------------------------
231// MockServerClient
232// ---------------------------------------------------------------------------
233
234/// A blocking client for the MockServer control-plane REST API.
235///
236/// Created via [`ClientBuilder`]. All methods are synchronous.
237pub struct MockServerClient {
238 pub(crate) base_url: String,
239 http: Client,
240 breakpoint_ws: std::sync::Mutex<Option<BreakpointWebSocketClient>>,
241}
242
243impl MockServerClient {
244 // ------------------------------------------------------------------
245 // Expectation creation
246 // ------------------------------------------------------------------
247
248 /// Create one or more expectations on the server.
249 ///
250 /// Returns the created expectations as echoed by the server.
251 pub fn upsert(&self, expectations: &[Expectation]) -> Result<Vec<Expectation>> {
252 let body = serde_json::to_value(expectations)?;
253 let resp = self
254 .http
255 .put(self.url("/mockserver/expectation"))
256 .json(&body)
257 .send()?;
258
259 let status = resp.status().as_u16();
260 match status {
261 200 | 201 => {
262 let text = resp.text()?;
263 if text.is_empty() {
264 Ok(expectations.to_vec())
265 } else {
266 Ok(serde_json::from_str(&text)?)
267 }
268 }
269 400 => Err(Error::InvalidRequest(resp.text()?)),
270 _ => Err(Error::UnexpectedStatus {
271 status,
272 body: resp.text().unwrap_or_default(),
273 }),
274 }
275 }
276
277 /// Create one or more expectations from raw JSON values.
278 ///
279 /// This is the lower-level counterpart to [`upsert`](Self::upsert) for
280 /// expectation shapes that the typed [`Expectation`] model does not (yet)
281 /// cover — notably the `httpLlmResponse` action and conversation scenario
282 /// fields produced by the [`crate::llm`] builders, and the Velocity/JSON-RPC
283 /// expectations produced by the [`crate::mcp`] builder.
284 ///
285 /// The `expectations` value should be a JSON object (single expectation) or
286 /// a JSON array of expectation objects. Returns the raw JSON the server
287 /// echoes back (or the submitted value if the server returns an empty body).
288 pub fn upsert_raw(&self, expectations: Value) -> Result<Value> {
289 let resp = self
290 .http
291 .put(self.url("/mockserver/expectation"))
292 .json(&expectations)
293 .send()?;
294
295 let status = resp.status().as_u16();
296 match status {
297 200 | 201 => {
298 let text = resp.text()?;
299 if text.is_empty() {
300 Ok(expectations)
301 } else {
302 Ok(serde_json::from_str(&text)?)
303 }
304 }
305 400 => Err(Error::InvalidRequest(resp.text()?)),
306 _ => Err(Error::UnexpectedStatus {
307 status,
308 body: resp.text().unwrap_or_default(),
309 }),
310 }
311 }
312
313 // ------------------------------------------------------------------
314 // OpenAPI import
315 // ------------------------------------------------------------------
316
317 /// Register expectations from an OpenAPI/Swagger specification.
318 ///
319 /// Sends a `PUT /mockserver/openapi` with the given [`OpenApiExpectation`].
320 /// MockServer parses the spec and creates request matchers and example
321 /// responses for the selected operations (or every operation when none are
322 /// specified). Returns the created expectations as echoed by the server.
323 ///
324 /// # Example
325 /// ```no_run
326 /// use mockserver_client::{ClientBuilder, OpenApiExpectation};
327 ///
328 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
329 /// client.openapi(
330 /// &OpenApiExpectation::new("https://example.com/petstore.yaml")
331 /// .operation("listPets", "200"),
332 /// ).unwrap();
333 /// ```
334 pub fn openapi(&self, expectation: &OpenApiExpectation) -> Result<Vec<Expectation>> {
335 let resp = self
336 .http
337 .put(self.url("/mockserver/openapi"))
338 .json(expectation)
339 .send()?;
340
341 let status = resp.status().as_u16();
342 match status {
343 200 | 201 => {
344 let text = resp.text()?;
345 if text.is_empty() {
346 Ok(vec![])
347 } else {
348 Ok(serde_json::from_str(&text)?)
349 }
350 }
351 400 => Err(Error::InvalidRequest(resp.text()?)),
352 _ => Err(Error::UnexpectedStatus {
353 status,
354 body: resp.text().unwrap_or_default(),
355 }),
356 }
357 }
358
359 // ------------------------------------------------------------------
360 // Fluent API entry point
361 // ------------------------------------------------------------------
362
363 /// Begin building an expectation with the fluent `when(...).respond(...)` API.
364 ///
365 /// # Example
366 /// ```no_run
367 /// use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};
368 ///
369 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
370 /// client.when(HttpRequest::new().method("GET").path("/foo"))
371 /// .respond(HttpResponse::new().status_code(200).body("bar"))
372 /// .unwrap();
373 /// ```
374 pub fn when(&self, request: HttpRequest) -> ForwardChainExpectation<'_> {
375 ForwardChainExpectation {
376 client: self,
377 request,
378 times: None,
379 time_to_live: None,
380 priority: None,
381 id: None,
382 }
383 }
384
385 // ------------------------------------------------------------------
386 // Verify
387 // ------------------------------------------------------------------
388
389 /// Verify that a request was received the specified number of times.
390 ///
391 /// Returns `Ok(())` if verification passes, or
392 /// `Err(Error::VerificationFailure)` with the server's failure message.
393 pub fn verify(&self, request: HttpRequest, times: VerificationTimes) -> Result<()> {
394 let verification = Verification {
395 http_request: Some(request),
396 http_response: None,
397 times: Some(times),
398 maximum_number_of_request_to_return_in_verification_failure: None,
399 };
400 self.do_verify(&verification)
401 }
402
403 /// Verify that a request/response pair was received the specified number of times.
404 ///
405 /// Both the request matcher and the response matcher must match for a
406 /// recorded exchange to count. The response matcher uses the same
407 /// [`HttpResponse`] type as expectations — the server matches against the
408 /// recorded response's status code, headers, and body.
409 pub fn verify_request_and_response(
410 &self,
411 request: HttpRequest,
412 response: HttpResponse,
413 times: VerificationTimes,
414 ) -> Result<()> {
415 let verification = Verification {
416 http_request: Some(request),
417 http_response: Some(response),
418 times: Some(times),
419 maximum_number_of_request_to_return_in_verification_failure: None,
420 };
421 self.do_verify(&verification)
422 }
423
424 /// Verify that a response (regardless of request) was returned the
425 /// specified number of times.
426 ///
427 /// The `httpRequest` field is omitted from the JSON so the server matches
428 /// any request.
429 pub fn verify_response(&self, response: HttpResponse, times: VerificationTimes) -> Result<()> {
430 let verification = Verification {
431 http_request: None,
432 http_response: Some(response),
433 times: Some(times),
434 maximum_number_of_request_to_return_in_verification_failure: None,
435 };
436 self.do_verify(&verification)
437 }
438
439 /// Verify that no requests at all were received by the server.
440 ///
441 /// Thin wrapper over [`verify`](Self::verify): matches any request (an empty
442 /// matcher) with `exactly(0)` times. Returns `Ok(())` if the server received
443 /// no requests, or `Err(Error::VerificationFailure)` otherwise.
444 pub fn verify_zero_interactions(&self) -> Result<()> {
445 self.verify(HttpRequest::new(), VerificationTimes::exactly(0))
446 }
447
448 /// Send a fully constructed [`Verification`] to the server.
449 ///
450 /// This is the most flexible form — callers can set every field,
451 /// including `maximum_number_of_request_to_return_in_verification_failure`.
452 pub fn verify_raw(&self, verification: &Verification) -> Result<()> {
453 self.do_verify(verification)
454 }
455
456 /// Verify that requests were received in the given order.
457 pub fn verify_sequence(&self, requests: Vec<HttpRequest>) -> Result<()> {
458 let verification = VerificationSequence {
459 http_requests: Some(requests),
460 http_responses: None,
461 };
462 self.do_verify_sequence(&verification)
463 }
464
465 /// Verify that request/response pairs were received in the given order.
466 ///
467 /// `responses` is index-aligned with `requests` — each entry constrains
468 /// the response that must have been returned for the corresponding request.
469 pub fn verify_sequence_with_responses(
470 &self,
471 requests: Vec<HttpRequest>,
472 responses: Vec<HttpResponse>,
473 ) -> Result<()> {
474 let verification = VerificationSequence {
475 http_requests: Some(requests),
476 http_responses: Some(responses),
477 };
478 self.do_verify_sequence(&verification)
479 }
480
481 /// Send a fully constructed [`VerificationSequence`] to the server.
482 pub fn verify_sequence_raw(&self, verification: &VerificationSequence) -> Result<()> {
483 self.do_verify_sequence(verification)
484 }
485
486 // ------------------------------------------------------------------
487 // Clear / Reset
488 // ------------------------------------------------------------------
489
490 /// Clear expectations and/or logs matching the given request.
491 ///
492 /// If `request` is `None`, clears everything of the specified type.
493 pub fn clear(
494 &self,
495 request: Option<&HttpRequest>,
496 clear_type: Option<ClearType>,
497 ) -> Result<()> {
498 let mut url = self.url("/mockserver/clear");
499 if let Some(ct) = clear_type {
500 url = format!("{url}?type={}", ct.as_str());
501 }
502
503 let mut builder = self.http.put(&url);
504 builder = builder.header("Content-Type", "application/json");
505 if let Some(req) = request {
506 builder = builder.json(req);
507 } else {
508 builder = builder.body("");
509 }
510
511 let resp = builder.send()?;
512 let status = resp.status().as_u16();
513 match status {
514 200 => Ok(()),
515 400 => Err(Error::InvalidRequest(resp.text()?)),
516 _ => Err(Error::UnexpectedStatus {
517 status,
518 body: resp.text().unwrap_or_default(),
519 }),
520 }
521 }
522
523 /// Clear expectations by expectation ID.
524 pub fn clear_by_id(
525 &self,
526 expectation_id: impl Into<String>,
527 clear_type: Option<ClearType>,
528 ) -> Result<()> {
529 let mut url = self.url("/mockserver/clear");
530 if let Some(ct) = clear_type {
531 url = format!("{url}?type={}", ct.as_str());
532 }
533
534 let body = serde_json::json!({ "id": expectation_id.into() });
535 let resp = self.http.put(&url).json(&body).send()?;
536
537 let status = resp.status().as_u16();
538 match status {
539 200 => Ok(()),
540 400 => Err(Error::InvalidRequest(resp.text()?)),
541 _ => Err(Error::UnexpectedStatus {
542 status,
543 body: resp.text().unwrap_or_default(),
544 }),
545 }
546 }
547
548 /// Reset all expectations and recorded requests.
549 pub fn reset(&self) -> Result<()> {
550 let resp = self
551 .http
552 .put(self.url("/mockserver/reset"))
553 .header("Content-Type", "application/json")
554 .body("")
555 .send()?;
556
557 let status = resp.status().as_u16();
558 match status {
559 200 => Ok(()),
560 _ => Err(Error::UnexpectedStatus {
561 status,
562 body: resp.text().unwrap_or_default(),
563 }),
564 }
565 }
566
567 // ------------------------------------------------------------------
568 // Retrieve
569 // ------------------------------------------------------------------
570
571 /// Retrieve recorded requests matching the optional filter.
572 pub fn retrieve_recorded_requests(
573 &self,
574 request: Option<&HttpRequest>,
575 ) -> Result<Vec<HttpRequest>> {
576 let text = self.do_retrieve(request, RetrieveType::Requests, RetrieveFormat::Json)?;
577 if text.is_empty() {
578 return Ok(vec![]);
579 }
580 Ok(serde_json::from_str(&text)?)
581 }
582
583 /// Retrieve active expectations matching the optional filter.
584 pub fn retrieve_active_expectations(
585 &self,
586 request: Option<&HttpRequest>,
587 ) -> Result<Vec<Expectation>> {
588 let text = self.do_retrieve(
589 request,
590 RetrieveType::ActiveExpectations,
591 RetrieveFormat::Json,
592 )?;
593 if text.is_empty() {
594 return Ok(vec![]);
595 }
596 Ok(serde_json::from_str(&text)?)
597 }
598
599 /// Retrieve recorded expectations matching the optional filter.
600 pub fn retrieve_recorded_expectations(
601 &self,
602 request: Option<&HttpRequest>,
603 ) -> Result<Vec<Expectation>> {
604 let text = self.do_retrieve(
605 request,
606 RetrieveType::RecordedExpectations,
607 RetrieveFormat::Json,
608 )?;
609 if text.is_empty() {
610 return Ok(vec![]);
611 }
612 Ok(serde_json::from_str(&text)?)
613 }
614
615 /// Retrieve the active expectations as MockServer SDK setup code (the
616 /// builder code that recreates the expectations) in the requested language.
617 ///
618 /// `format` must be one of the code-generation variants of
619 /// [`RetrieveFormat`] (e.g. [`RetrieveFormat::Java`],
620 /// [`RetrieveFormat::Rust`]). The generated code is returned as a string.
621 pub fn retrieve_expectations_as_code(
622 &self,
623 format: RetrieveFormat,
624 request: Option<&HttpRequest>,
625 ) -> Result<String> {
626 self.do_retrieve(request, RetrieveType::ActiveExpectations, format)
627 }
628
629 /// Retrieve the recorded (proxied) request/response pairs as MockServer SDK
630 /// setup code in the requested language.
631 ///
632 /// `format` must be one of the code-generation variants of
633 /// [`RetrieveFormat`]. The generated code is returned as a string.
634 pub fn retrieve_recorded_expectations_as_code(
635 &self,
636 format: RetrieveFormat,
637 request: Option<&HttpRequest>,
638 ) -> Result<String> {
639 self.do_retrieve(request, RetrieveType::RecordedExpectations, format)
640 }
641
642 /// Retrieve log messages matching the optional filter.
643 pub fn retrieve_log_messages(&self, request: Option<&HttpRequest>) -> Result<Vec<String>> {
644 let text = self.do_retrieve(request, RetrieveType::Logs, RetrieveFormat::LogEntries)?;
645 if text.is_empty() {
646 return Ok(vec![]);
647 }
648 // Log messages may be returned as a JSON array of strings or as a
649 // separator-delimited block. Try JSON first.
650 if let Ok(arr) = serde_json::from_str::<Vec<String>>(&text) {
651 return Ok(arr);
652 }
653 // Fall back to splitting on the separator used by MockServer.
654 Ok(text
655 .split("------------------------------------\n")
656 .map(|s| s.to_string())
657 .filter(|s| !s.is_empty())
658 .collect())
659 }
660
661 /// Retrieve recorded request/response pairs.
662 pub fn retrieve_request_responses(&self, request: Option<&HttpRequest>) -> Result<Vec<Value>> {
663 let text = self.do_retrieve(
664 request,
665 RetrieveType::RequestResponses,
666 RetrieveFormat::Json,
667 )?;
668 if text.is_empty() {
669 return Ok(vec![]);
670 }
671 Ok(serde_json::from_str(&text)?)
672 }
673
674 // ------------------------------------------------------------------
675 // Status / Bind
676 // ------------------------------------------------------------------
677
678 /// Query the server's listening ports.
679 pub fn status(&self) -> Result<Ports> {
680 let resp = self
681 .http
682 .put(self.url("/mockserver/status"))
683 .header("Content-Type", "application/json")
684 .body("")
685 .send()?;
686
687 let status = resp.status().as_u16();
688 match status {
689 200 => {
690 let text = resp.text()?;
691 Ok(serde_json::from_str(&text)?)
692 }
693 _ => Err(Error::UnexpectedStatus {
694 status,
695 body: resp.text().unwrap_or_default(),
696 }),
697 }
698 }
699
700 /// Bind additional listening ports.
701 pub fn bind(&self, ports: &[u16]) -> Result<Ports> {
702 let body = Ports {
703 ports: ports.to_vec(),
704 };
705 let resp = self
706 .http
707 .put(self.url("/mockserver/bind"))
708 .json(&body)
709 .send()?;
710
711 let status = resp.status().as_u16();
712 match status {
713 200 => {
714 let text = resp.text()?;
715 Ok(serde_json::from_str(&text)?)
716 }
717 400 => Err(Error::InvalidRequest(resp.text()?)),
718 406 => Err(Error::VerificationFailure(resp.text()?)),
719 _ => Err(Error::UnexpectedStatus {
720 status,
721 body: resp.text().unwrap_or_default(),
722 }),
723 }
724 }
725
726 /// Check if the MockServer has started (polls with retries).
727 pub fn has_started(&self, attempts: u32, timeout_ms: u64) -> bool {
728 for i in 0..attempts {
729 match self.status() {
730 Ok(_) => return true,
731 Err(_) => {
732 if i < attempts - 1 {
733 std::thread::sleep(std::time::Duration::from_millis(timeout_ms));
734 }
735 }
736 }
737 }
738 false
739 }
740
741 // ------------------------------------------------------------------
742 // Breakpoints
743 // ------------------------------------------------------------------
744
745 /// Ensure the breakpoint WebSocket client is connected and return the clientId.
746 /// If the existing connection's read loop has exited, it is replaced transparently.
747 fn ensure_breakpoint_ws(&self) -> Result<String> {
748 let mut guard = self.breakpoint_ws.lock().unwrap();
749 let needs_connect = match guard.as_ref() {
750 None => true,
751 Some(ws) => ws.is_dead(),
752 };
753 if needs_connect {
754 // Close the old dead connection if present
755 if let Some(old) = guard.take() {
756 old.close();
757 }
758 let ws = BreakpointWebSocketClient::connect(&self.base_url)?;
759 *guard = Some(ws);
760 }
761 Ok(guard.as_ref().unwrap().client_id.clone())
762 }
763
764 /// Register a breakpoint matcher with the given phases and handlers.
765 /// Returns the server-assigned breakpoint id.
766 pub fn add_breakpoint(
767 &self,
768 matcher: HttpRequest,
769 phases: &[&str],
770 request_handler: Option<BreakpointRequestHandler>,
771 response_handler: Option<BreakpointResponseHandler>,
772 stream_frame_handler: Option<BreakpointStreamFrameHandler>,
773 ) -> Result<String> {
774 if phases.is_empty() {
775 return Err(Error::InvalidRequest(
776 "At least one phase is required".into(),
777 ));
778 }
779
780 let client_id = self.ensure_breakpoint_ws()?;
781
782 let reg = BreakpointMatcherRegistration {
783 http_request: matcher,
784 phases: phases.iter().map(|s| s.to_string()).collect(),
785 client_id: Some(client_id),
786 };
787
788 let resp = self
789 .http
790 .put(self.url("/mockserver/breakpoint/matcher"))
791 .json(®)
792 .send()?;
793
794 let status = resp.status().as_u16();
795 let text = resp.text()?;
796 if status >= 400 {
797 return Err(Error::UnexpectedStatus { status, body: text });
798 }
799
800 let result: BreakpointMatcherResponse = serde_json::from_str(&text)?;
801 let id = result.id.clone();
802
803 // Register handlers
804 let guard = self.breakpoint_ws.lock().unwrap();
805 if let Some(ws) = guard.as_ref() {
806 if let Some(h) = request_handler {
807 ws.set_request_handler(&id, h);
808 }
809 if let Some(h) = response_handler {
810 ws.set_response_handler(&id, h);
811 }
812 if let Some(h) = stream_frame_handler {
813 ws.set_stream_frame_handler(&id, h);
814 }
815 }
816
817 Ok(id)
818 }
819
820 /// Convenience: register a REQUEST-only breakpoint.
821 pub fn add_request_breakpoint(
822 &self,
823 matcher: HttpRequest,
824 handler: BreakpointRequestHandler,
825 ) -> Result<String> {
826 self.add_breakpoint(
827 matcher,
828 &[crate::breakpoint::phase::REQUEST],
829 Some(handler),
830 None,
831 None,
832 )
833 }
834
835 /// Convenience: register a REQUEST + RESPONSE breakpoint.
836 pub fn add_request_response_breakpoint(
837 &self,
838 matcher: HttpRequest,
839 request_handler: BreakpointRequestHandler,
840 response_handler: BreakpointResponseHandler,
841 ) -> Result<String> {
842 self.add_breakpoint(
843 matcher,
844 &[
845 crate::breakpoint::phase::REQUEST,
846 crate::breakpoint::phase::RESPONSE,
847 ],
848 Some(request_handler),
849 Some(response_handler),
850 None,
851 )
852 }
853
854 /// Convenience: register a streaming-phase breakpoint.
855 pub fn add_stream_breakpoint(
856 &self,
857 matcher: HttpRequest,
858 phases: &[&str],
859 handler: BreakpointStreamFrameHandler,
860 ) -> Result<String> {
861 self.add_breakpoint(matcher, phases, None, None, Some(handler))
862 }
863
864 /// List all registered breakpoint matchers.
865 pub fn list_breakpoint_matchers(&self) -> Result<BreakpointMatcherList> {
866 let resp = self
867 .http
868 .get(self.url("/mockserver/breakpoint/matchers"))
869 .send()?;
870
871 let status = resp.status().as_u16();
872 let text = resp.text()?;
873 if status >= 400 {
874 return Err(Error::UnexpectedStatus { status, body: text });
875 }
876
877 Ok(serde_json::from_str(&text)?)
878 }
879
880 /// Remove a breakpoint matcher by id.
881 pub fn remove_breakpoint_matcher(&self, id: impl Into<String>) -> Result<()> {
882 let id = id.into();
883 let body = serde_json::json!({ "id": &id });
884 let resp = self
885 .http
886 .put(self.url("/mockserver/breakpoint/matcher/remove"))
887 .json(&body)
888 .send()?;
889
890 let status = resp.status().as_u16();
891 match status {
892 200 => {
893 let guard = self.breakpoint_ws.lock().unwrap();
894 if let Some(ws) = guard.as_ref() {
895 ws.remove_handlers(&id);
896 }
897 Ok(())
898 }
899 404 => Err(Error::InvalidRequest(format!(
900 "Breakpoint matcher not found: {id}"
901 ))),
902 _ => Err(Error::UnexpectedStatus {
903 status,
904 body: resp.text().unwrap_or_default(),
905 }),
906 }
907 }
908
909 /// Remove all registered breakpoint matchers.
910 pub fn clear_breakpoint_matchers(&self) -> Result<()> {
911 let resp = self
912 .http
913 .put(self.url("/mockserver/breakpoint/matcher/clear"))
914 .header("Content-Type", "application/json")
915 .body("")
916 .send()?;
917
918 let status = resp.status().as_u16();
919 if status >= 400 {
920 return Err(Error::UnexpectedStatus {
921 status,
922 body: resp.text().unwrap_or_default(),
923 });
924 }
925
926 let guard = self.breakpoint_ws.lock().unwrap();
927 if let Some(ws) = guard.as_ref() {
928 ws.clear_handlers();
929 }
930 Ok(())
931 }
932
933 /// Close the breakpoint callback WebSocket connection.
934 pub fn close_breakpoint_websocket(&self) {
935 let mut guard = self.breakpoint_ws.lock().unwrap();
936 if let Some(ws) = guard.take() {
937 ws.close();
938 }
939 }
940
941 // ------------------------------------------------------------------
942 // Object (closure) callbacks
943 // ------------------------------------------------------------------
944
945 /// Register an expectation whose response is produced by a Rust closure
946 /// invoked over the callback WebSocket (an `httpResponseObjectCallback`).
947 ///
948 /// When a request matches `matcher`, MockServer pushes it to this client over
949 /// the shared callback WebSocket; `handler` receives the [`HttpRequest`] and
950 /// returns the [`HttpResponse`] to send back. The closure runs on the client's
951 /// background WebSocket-read thread, so it must be `Send + 'static`.
952 ///
953 /// The callback WebSocket is shared with breakpoints — only one socket is
954 /// opened per client. There is a single object-response handler per client;
955 /// calling this again replaces it. Narrow which requests reach the closure
956 /// with the `matcher`.
957 ///
958 /// # Example
959 /// ```no_run
960 /// use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};
961 ///
962 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
963 /// client.mock_with_callback(
964 /// HttpRequest::new().method("GET").path("/echo"),
965 /// |req| {
966 /// HttpResponse::new()
967 /// .status_code(200)
968 /// .body(format!("you asked for {}", req.path.unwrap_or_default()))
969 /// },
970 /// ).unwrap();
971 /// ```
972 pub fn mock_with_callback<F>(
973 &self,
974 matcher: HttpRequest,
975 handler: F,
976 ) -> Result<Vec<Expectation>>
977 where
978 F: Fn(HttpRequest) -> HttpResponse + Send + 'static,
979 {
980 // Ensure the shared callback WebSocket is connected and learn its clientId.
981 let client_id = self.ensure_breakpoint_ws()?;
982
983 // Adapt the typed closure to the JSON-level ObjectResponseHandler the WS
984 // read loop drives. The reply must echo the WebSocketCorrelationId header,
985 // which route_object_callback re-applies after the closure returns.
986 let object_handler: crate::breakpoint::ObjectResponseHandler =
987 Box::new(move |request_json: Value| {
988 let request: HttpRequest =
989 serde_json::from_value(request_json).unwrap_or_default();
990 let response = handler(request);
991 serde_json::to_value(&response).unwrap_or_else(|_| serde_json::json!({}))
992 });
993
994 {
995 let guard = self.breakpoint_ws.lock().unwrap();
996 if let Some(ws) = guard.as_ref() {
997 ws.set_object_response_handler(object_handler);
998 }
999 }
1000
1001 let expectation = Expectation::new(matcher)
1002 .respond_object_callback(HttpObjectCallback::new(client_id));
1003 self.upsert(&[expectation])
1004 }
1005
1006 // ------------------------------------------------------------------
1007 // gRPC descriptor management
1008 // ------------------------------------------------------------------
1009
1010 /// Upload a compiled protobuf descriptor set so gRPC requests can be matched.
1011 ///
1012 /// `descriptor` must be the raw bytes of a `FileDescriptorSet` (e.g. the
1013 /// output of `protoc --descriptor_set_out=... --include_imports`). The bytes
1014 /// are sent verbatim as `application/octet-stream` — they are **not**
1015 /// base64-encoded. Sends a `PUT /mockserver/grpc/descriptors`; the server
1016 /// responds `201 Created` on success.
1017 ///
1018 /// # Example
1019 /// ```no_run
1020 /// use mockserver_client::ClientBuilder;
1021 ///
1022 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
1023 /// let descriptor_set: Vec<u8> = std::fs::read("greeter.desc").unwrap();
1024 /// client.upload_grpc_descriptor(&descriptor_set).unwrap();
1025 /// ```
1026 pub fn upload_grpc_descriptor(&self, descriptor: &[u8]) -> Result<()> {
1027 if descriptor.is_empty() {
1028 return Err(Error::InvalidRequest(
1029 "descriptor set bytes must not be empty".into(),
1030 ));
1031 }
1032 let resp = self
1033 .http
1034 .put(self.url("/mockserver/grpc/descriptors"))
1035 .header("Content-Type", "application/octet-stream")
1036 .body(descriptor.to_vec())
1037 .send()?;
1038
1039 let status = resp.status().as_u16();
1040 match status {
1041 200 | 201 => Ok(()),
1042 400 => Err(Error::InvalidRequest(resp.text()?)),
1043 _ => Err(Error::UnexpectedStatus {
1044 status,
1045 body: resp.text().unwrap_or_default(),
1046 }),
1047 }
1048 }
1049
1050 /// Retrieve the gRPC services registered from uploaded descriptor sets.
1051 ///
1052 /// Sends a `PUT /mockserver/grpc/services` and returns the parsed list of
1053 /// [`GrpcService`]s, each with its [`GrpcMethod`]s.
1054 pub fn retrieve_grpc_services(&self) -> Result<Vec<GrpcService>> {
1055 let resp = self
1056 .http
1057 .put(self.url("/mockserver/grpc/services"))
1058 .header("Content-Type", "application/json")
1059 .body("")
1060 .send()?;
1061
1062 let status = resp.status().as_u16();
1063 match status {
1064 200 => {
1065 let text = resp.text()?;
1066 if text.is_empty() {
1067 Ok(vec![])
1068 } else {
1069 Ok(serde_json::from_str(&text)?)
1070 }
1071 }
1072 400 => Err(Error::InvalidRequest(resp.text()?)),
1073 _ => Err(Error::UnexpectedStatus {
1074 status,
1075 body: resp.text().unwrap_or_default(),
1076 }),
1077 }
1078 }
1079
1080 /// Clear all uploaded gRPC descriptor sets and registered services.
1081 ///
1082 /// Sends a `PUT /mockserver/grpc/clear`; the server responds `200 OK`.
1083 pub fn clear_grpc_descriptors(&self) -> Result<()> {
1084 let resp = self
1085 .http
1086 .put(self.url("/mockserver/grpc/clear"))
1087 .header("Content-Type", "application/json")
1088 .body("")
1089 .send()?;
1090
1091 let status = resp.status().as_u16();
1092 match status {
1093 200 => Ok(()),
1094 400 => Err(Error::InvalidRequest(resp.text()?)),
1095 _ => Err(Error::UnexpectedStatus {
1096 status,
1097 body: resp.text().unwrap_or_default(),
1098 }),
1099 }
1100 }
1101
1102 // ------------------------------------------------------------------
1103 // Stateful scenarios
1104 // ------------------------------------------------------------------
1105
1106 /// Obtain a handle for inspecting and driving a named scenario state-machine.
1107 ///
1108 /// The returned [`Scenario`] borrows the client and issues control-plane
1109 /// requests against `/mockserver/scenario/{name}`.
1110 ///
1111 /// # Example
1112 /// ```no_run
1113 /// use mockserver_client::ClientBuilder;
1114 ///
1115 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
1116 /// client.scenario("Deploy").set("Deploying").unwrap();
1117 /// client.scenario("Deploy").set_timed("Deploying", 5000, "Deployed").unwrap();
1118 /// client.scenario("Deploy").trigger("Failed").unwrap();
1119 /// let state = client.scenario("Deploy").state().unwrap();
1120 /// assert_eq!(state, "Failed");
1121 /// ```
1122 pub fn scenario(&self, name: &str) -> Scenario<'_> {
1123 Scenario {
1124 client: self,
1125 name: name.to_string(),
1126 }
1127 }
1128
1129 /// List every known scenario and its current state.
1130 ///
1131 /// Sends a `GET /mockserver/scenario` and returns the parsed list of
1132 /// [`ScenarioState`]s.
1133 pub fn scenarios(&self) -> Result<Vec<ScenarioState>> {
1134 let resp = self.http.get(self.url("/mockserver/scenario")).send()?;
1135 let status = resp.status().as_u16();
1136 match status {
1137 200 => {
1138 let text = resp.text()?;
1139 if text.is_empty() {
1140 Ok(vec![])
1141 } else {
1142 let list: ScenarioList = serde_json::from_str(&text)?;
1143 Ok(list.scenarios)
1144 }
1145 }
1146 400 => Err(Error::InvalidRequest(resp.text()?)),
1147 _ => Err(Error::UnexpectedStatus {
1148 status,
1149 body: resp.text().unwrap_or_default(),
1150 }),
1151 }
1152 }
1153
1154 /// Get the current state of a named scenario.
1155 fn scenario_state(&self, name: &str) -> Result<String> {
1156 let resp = self
1157 .http
1158 .get(self.url(&scenario_path(name)))
1159 .send()?;
1160 let status = resp.status().as_u16();
1161 match status {
1162 200 => {
1163 let text = resp.text()?;
1164 let state: ScenarioState = serde_json::from_str(&text)?;
1165 Ok(state.current_state)
1166 }
1167 400 => Err(Error::InvalidRequest(resp.text()?)),
1168 _ => Err(Error::UnexpectedStatus {
1169 status,
1170 body: resp.text().unwrap_or_default(),
1171 }),
1172 }
1173 }
1174
1175 /// Set a scenario's state, optionally scheduling a timed transition.
1176 fn scenario_set(
1177 &self,
1178 name: &str,
1179 state: &str,
1180 transition_after_ms: Option<u64>,
1181 next_state: Option<&str>,
1182 ) -> Result<()> {
1183 let mut body = serde_json::json!({ "state": state });
1184 if let Some(ms) = transition_after_ms {
1185 body["transitionAfterMs"] = serde_json::json!(ms);
1186 }
1187 if let Some(next) = next_state {
1188 body["nextState"] = serde_json::json!(next);
1189 }
1190 let resp = self
1191 .http
1192 .put(self.url(&scenario_path(name)))
1193 .json(&body)
1194 .send()?;
1195 self.scenario_ok(resp)
1196 }
1197
1198 /// Externally trigger a scenario state transition.
1199 fn scenario_trigger(&self, name: &str, new_state: &str) -> Result<()> {
1200 let body = serde_json::json!({ "newState": new_state });
1201 let resp = self
1202 .http
1203 .put(self.url(&format!("{}/trigger", scenario_path(name))))
1204 .json(&body)
1205 .send()?;
1206 self.scenario_ok(resp)
1207 }
1208
1209 /// Map a scenario REST response to `Ok(())` on `200`, surfacing the server's
1210 /// error body on `400` and any other status as [`Error::UnexpectedStatus`].
1211 fn scenario_ok(&self, resp: reqwest::blocking::Response) -> Result<()> {
1212 let status = resp.status().as_u16();
1213 match status {
1214 200 => Ok(()),
1215 400 => Err(Error::InvalidRequest(resp.text()?)),
1216 _ => Err(Error::UnexpectedStatus {
1217 status,
1218 body: resp.text().unwrap_or_default(),
1219 }),
1220 }
1221 }
1222
1223 // ------------------------------------------------------------------
1224 // SRE control plane — load scenario registry
1225 // ------------------------------------------------------------------
1226
1227 /// Register (load) a load scenario in the registry without running it.
1228 ///
1229 /// Sends a `PUT /mockserver/loadScenario` with the given [`LoadScenario`].
1230 /// The scenario's [`name`](LoadScenario::name) is the unique registry key
1231 /// used later by [`start_load_scenarios`](Self::start_load_scenarios) /
1232 /// [`stop_load_scenarios`](Self::stop_load_scenarios) and the per-scenario
1233 /// fetch/delete endpoints.
1234 ///
1235 /// Registering is always allowed — even when load generation is disabled on
1236 /// the server — so this does not surface [`Error::FeatureDisabled`]. Returns
1237 /// the raw JSON the server echoes (`{"name":..,"state":"LOADED"}`).
1238 pub fn load_scenario(&self, scenario: &LoadScenario) -> Result<Value> {
1239 let resp = self
1240 .http
1241 .put(self.url("/mockserver/loadScenario"))
1242 .json(scenario)
1243 .send()?;
1244 self.load_scenario_json(resp)
1245 }
1246
1247 /// List every registered load scenario.
1248 ///
1249 /// Sends a `GET /mockserver/loadScenario` and returns the raw JSON
1250 /// (`{"scenarios":[{"name":..,"state":..,"definition":..,"status":..?}]}`).
1251 pub fn load_scenarios(&self) -> Result<Value> {
1252 let resp = self.http.get(self.url("/mockserver/loadScenario")).send()?;
1253 self.load_scenario_json(resp)
1254 }
1255
1256 /// Fetch a single registered load scenario by name.
1257 ///
1258 /// Sends a `GET /mockserver/loadScenario/{name}`. Returns
1259 /// [`Error::NotFound`] when no scenario with that name is registered.
1260 pub fn get_load_scenario(&self, name: impl AsRef<str>) -> Result<Value> {
1261 let resp = self
1262 .http
1263 .get(self.url(&format!("/mockserver/loadScenario/{}", name.as_ref())))
1264 .send()?;
1265 self.load_scenario_json(resp)
1266 }
1267
1268 /// Remove a single registered load scenario by name.
1269 ///
1270 /// Sends a `DELETE /mockserver/loadScenario/{name}`. Returns
1271 /// [`Error::NotFound`] when no scenario with that name is registered.
1272 pub fn delete_load_scenario(&self, name: impl AsRef<str>) -> Result<Value> {
1273 let resp = self
1274 .http
1275 .delete(self.url(&format!("/mockserver/loadScenario/{}", name.as_ref())))
1276 .send()?;
1277 self.load_scenario_json(resp)
1278 }
1279
1280 /// Clear all registered load scenarios.
1281 ///
1282 /// Sends a `DELETE /mockserver/loadScenario`. Idempotent.
1283 pub fn clear_load_scenarios(&self) -> Result<Value> {
1284 let resp = self
1285 .http
1286 .delete(self.url("/mockserver/loadScenario"))
1287 .send()?;
1288 self.load_scenario_json(resp)
1289 }
1290
1291 /// Start one or more registered load scenarios by name.
1292 ///
1293 /// Sends a `PUT /mockserver/loadScenario/start` with `{"names":[...]}`.
1294 /// Requires load generation to be enabled on the server — returns
1295 /// [`Error::FeatureDisabled`] on `403` (`loadGenerationEnabled=false`) — and
1296 /// [`Error::NotFound`] when a name is not registered. Honours each
1297 /// scenario's `startDelayMillis`. Returns the raw JSON
1298 /// (`{"started":[{"name":..,"state":..}],"status":..}`).
1299 pub fn start_load_scenarios<S: AsRef<str>>(&self, names: &[S]) -> Result<Value> {
1300 let names: Vec<&str> = names.iter().map(|n| n.as_ref()).collect();
1301 let body = serde_json::json!({ "names": names });
1302 let resp = self
1303 .http
1304 .put(self.url("/mockserver/loadScenario/start"))
1305 .json(&body)
1306 .send()?;
1307 self.load_scenario_json(resp)
1308 }
1309
1310 /// Stop running load scenarios.
1311 ///
1312 /// Sends a `PUT /mockserver/loadScenario/stop`. When `names` is non-empty the
1313 /// body is `{"names":[...]}`; when it is empty (or `&[]`) the body is empty,
1314 /// which the server treats as "stop all". Returns the raw JSON
1315 /// (`{"stopped":[..],"status":..}`).
1316 pub fn stop_load_scenarios<S: AsRef<str>>(&self, names: &[S]) -> Result<Value> {
1317 let mut req = self.http.put(self.url("/mockserver/loadScenario/stop"));
1318 if names.is_empty() {
1319 req = req.header("Content-Type", "application/json").body("");
1320 } else {
1321 let names: Vec<&str> = names.iter().map(|n| n.as_ref()).collect();
1322 req = req.json(&serde_json::json!({ "names": names }));
1323 }
1324 let resp = req.send()?;
1325 self.load_scenario_json(resp)
1326 }
1327
1328 /// Convenience: register `scenario` then immediately start it by name.
1329 ///
1330 /// Equivalent to [`load_scenario`](Self::load_scenario) followed by
1331 /// [`start_load_scenarios`](Self::start_load_scenarios) with the scenario's
1332 /// own name. Returns the JSON from the `start` call. Surfaces
1333 /// [`Error::FeatureDisabled`] from `start` when load generation is disabled.
1334 pub fn run_load_scenario(&self, scenario: &LoadScenario) -> Result<Value> {
1335 self.load_scenario(scenario)?;
1336 self.start_load_scenarios(&[scenario.name.as_str()])
1337 }
1338
1339 /// Fetch the end-of-run summary report for a load scenario run.
1340 ///
1341 /// Sends a `GET /mockserver/loadScenario/{name}/report`. When `format` is
1342 /// `Some("junit")` a `?format=junit` query is appended and the server returns
1343 /// a JUnit-XML `<testsuite>` document; omit `format` (`None`) for the JSON
1344 /// report. The raw response body is returned as a string so either form (JSON
1345 /// or XML) passes through verbatim. Returns [`Error::NotFound`] when the
1346 /// scenario never ran.
1347 pub fn get_load_scenario_report(
1348 &self,
1349 name: impl AsRef<str>,
1350 format: Option<&str>,
1351 ) -> Result<String> {
1352 let mut path = format!("/mockserver/loadScenario/{}/report", name.as_ref());
1353 if let Some(format) = format {
1354 path.push_str("?format=");
1355 path.push_str(format);
1356 }
1357 let resp = self.http.get(self.url(&path)).send()?;
1358 let status = resp.status().as_u16();
1359 match status {
1360 200 | 201 => Ok(resp.text()?),
1361 400 => Err(Error::InvalidRequest(resp.text()?)),
1362 403 => Err(Error::FeatureDisabled(resp.text()?)),
1363 404 => Err(Error::NotFound(resp.text()?)),
1364 _ => Err(Error::UnexpectedStatus {
1365 status,
1366 body: resp.text().unwrap_or_default(),
1367 }),
1368 }
1369 }
1370
1371 /// Generate (and register) an editable load scenario from an OpenAPI spec.
1372 ///
1373 /// Sends a `PUT /mockserver/loadScenario/generateFromOpenAPI`. The `body`
1374 /// carries the generated scenario `name`, the `specUrlOrPayload`, and an
1375 /// optional `target` and `profile`. Like [`load_scenario`](Self::load_scenario)
1376 /// this only registers (LOADED) the scenario — it generates no traffic and is
1377 /// allowed even when load generation is disabled. Returns the raw JSON
1378 /// (`{"status":"loaded","name":..,"state":..,"scenario":..}`).
1379 pub fn generate_load_scenario_from_openapi<T: Serialize + ?Sized>(
1380 &self,
1381 body: &T,
1382 ) -> Result<Value> {
1383 let resp = self
1384 .http
1385 .put(self.url("/mockserver/loadScenario/generateFromOpenAPI"))
1386 .json(body)
1387 .send()?;
1388 self.load_scenario_json(resp)
1389 }
1390
1391 /// Generate (and register) an editable load scenario from recorded proxy
1392 /// traffic.
1393 ///
1394 /// Sends a `PUT /mockserver/loadScenario/generateFromRecording`. The `body`
1395 /// carries the generated scenario `name` and the recording selection/options.
1396 /// Like [`load_scenario`](Self::load_scenario) this only registers (LOADED)
1397 /// the scenario — it generates no traffic. Returns the raw JSON
1398 /// (`{"status":"loaded","name":..,"state":..,"scenario":..}`).
1399 pub fn generate_load_scenario_from_recording<T: Serialize + ?Sized>(
1400 &self,
1401 body: &T,
1402 ) -> Result<Value> {
1403 let resp = self
1404 .http
1405 .put(self.url("/mockserver/loadScenario/generateFromRecording"))
1406 .json(body)
1407 .send()?;
1408 self.load_scenario_json(resp)
1409 }
1410
1411 // ------------------------------------------------------------------
1412 // SRE control plane — service chaos
1413 // ------------------------------------------------------------------
1414
1415 /// Register a service-scoped HTTP chaos profile for a downstream host.
1416 ///
1417 /// Sends a `PUT /mockserver/serviceChaos`. `ttl_millis`, when supplied, sets
1418 /// an optional time-to-live after which the registration auto-reverts.
1419 /// Returns the raw JSON the server echoes.
1420 pub fn set_service_chaos(
1421 &self,
1422 host: impl Into<String>,
1423 profile: &HttpChaosProfile,
1424 ttl_millis: Option<u64>,
1425 ) -> Result<Value> {
1426 let mut body = serde_json::json!({
1427 "host": host.into(),
1428 "chaos": profile,
1429 });
1430 if let Some(ttl) = ttl_millis {
1431 body["ttlMillis"] = serde_json::json!(ttl);
1432 }
1433 let resp = self
1434 .http
1435 .put(self.url("/mockserver/serviceChaos"))
1436 .json(&body)
1437 .send()?;
1438 self.json_or_feature_error(resp)
1439 }
1440
1441 /// Remove a single host's service-scoped chaos profile.
1442 ///
1443 /// Sends a `PUT /mockserver/serviceChaos` with `{"host":..,"remove":true}`.
1444 pub fn remove_service_chaos(&self, host: impl Into<String>) -> Result<Value> {
1445 let body = serde_json::json!({ "host": host.into(), "remove": true });
1446 let resp = self
1447 .http
1448 .put(self.url("/mockserver/serviceChaos"))
1449 .json(&body)
1450 .send()?;
1451 self.json_or_feature_error(resp)
1452 }
1453
1454 /// Clear all service-scoped chaos.
1455 ///
1456 /// Sends a `PUT /mockserver/serviceChaos` with `{"clear":true}`.
1457 pub fn clear_service_chaos(&self) -> Result<Value> {
1458 let body = serde_json::json!({ "clear": true });
1459 let resp = self
1460 .http
1461 .put(self.url("/mockserver/serviceChaos"))
1462 .json(&body)
1463 .send()?;
1464 self.json_or_feature_error(resp)
1465 }
1466
1467 // ------------------------------------------------------------------
1468 // SRE control plane — SLO verdicts
1469 // ------------------------------------------------------------------
1470
1471 /// Verify a set of service-level objectives over a window.
1472 ///
1473 /// Sends a `PUT /mockserver/verifySLO`. The HTTP status encodes the verdict:
1474 /// `200` for PASS or INCONCLUSIVE, `406` for FAIL — both deserialize into a
1475 /// [`SloVerdict`] (inspect [`SloVerdict::result`]). A `400` (malformed
1476 /// criteria, or SLO tracking disabled) surfaces as [`Error::FeatureDisabled`].
1477 ///
1478 /// Returns `Ok(SloVerdict)` for both PASS/INCONCLUSIVE (200) and FAIL (406)
1479 /// so callers can branch on the verdict; transport/parse failures and `400`
1480 /// are returned as `Err`.
1481 pub fn verify_slo(&self, criteria: &SloCriteria) -> Result<SloVerdict> {
1482 let resp = self
1483 .http
1484 .put(self.url("/mockserver/verifySLO"))
1485 .json(criteria)
1486 .send()?;
1487
1488 let status = resp.status().as_u16();
1489 match status {
1490 // PASS / INCONCLUSIVE (200) and FAIL (406) both carry a SloVerdict.
1491 200 | 406 => {
1492 let text = resp.text()?;
1493 Ok(serde_json::from_str(&text)?)
1494 }
1495 400 => Err(Error::FeatureDisabled(resp.text()?)),
1496 403 => Err(Error::FeatureDisabled(resp.text()?)),
1497 _ => Err(Error::UnexpectedStatus {
1498 status,
1499 body: resp.text().unwrap_or_default(),
1500 }),
1501 }
1502 }
1503
1504 // ------------------------------------------------------------------
1505 // SRE control plane — preemption
1506 // ------------------------------------------------------------------
1507
1508 /// Cordon and drain the server (preemption simulation).
1509 ///
1510 /// Sends a `PUT /mockserver/preemption` with the given [`PreemptionRequest`]
1511 /// and returns the resulting [`PreemptionStatus`].
1512 pub fn set_preemption(&self, request: &PreemptionRequest) -> Result<PreemptionStatus> {
1513 let resp = self
1514 .http
1515 .put(self.url("/mockserver/preemption"))
1516 .json(request)
1517 .send()?;
1518
1519 let status = resp.status().as_u16();
1520 match status {
1521 200 => {
1522 let text = resp.text()?;
1523 Ok(serde_json::from_str(&text)?)
1524 }
1525 400 => Err(Error::InvalidRequest(resp.text()?)),
1526 403 => Err(Error::FeatureDisabled(resp.text()?)),
1527 _ => Err(Error::UnexpectedStatus {
1528 status,
1529 body: resp.text().unwrap_or_default(),
1530 }),
1531 }
1532 }
1533
1534 /// Retrieve the current preemption status.
1535 ///
1536 /// Sends a `GET /mockserver/preemption` and returns the [`PreemptionStatus`].
1537 pub fn preemption_status(&self) -> Result<PreemptionStatus> {
1538 let resp = self
1539 .http
1540 .get(self.url("/mockserver/preemption"))
1541 .send()?;
1542
1543 let status = resp.status().as_u16();
1544 match status {
1545 200 => {
1546 let text = resp.text()?;
1547 Ok(serde_json::from_str(&text)?)
1548 }
1549 403 => Err(Error::FeatureDisabled(resp.text()?)),
1550 _ => Err(Error::UnexpectedStatus {
1551 status,
1552 body: resp.text().unwrap_or_default(),
1553 }),
1554 }
1555 }
1556
1557 /// Uncordon the server (clear any active preemption simulation).
1558 ///
1559 /// Sends a `DELETE /mockserver/preemption`. Idempotent — succeeds whether or
1560 /// not a simulation was active. Returns the raw JSON status.
1561 pub fn clear_preemption(&self) -> Result<Value> {
1562 let resp = self
1563 .http
1564 .delete(self.url("/mockserver/preemption"))
1565 .send()?;
1566 self.json_or_feature_error(resp)
1567 }
1568
1569 // ------------------------------------------------------------------
1570 // SRE control plane — chaos experiments
1571 // ------------------------------------------------------------------
1572
1573 /// Start a scheduled multi-stage chaos experiment.
1574 ///
1575 /// Sends a `PUT /mockserver/chaosExperiment` with the given
1576 /// [`ChaosExperiment`]. Only one experiment may be active at a time; starting
1577 /// a new one stops the previous one. Returns the raw JSON status
1578 /// (`{"status":"started","name":..}`).
1579 pub fn start_chaos_experiment(&self, experiment: &ChaosExperiment) -> Result<Value> {
1580 let resp = self
1581 .http
1582 .put(self.url("/mockserver/chaosExperiment"))
1583 .json(experiment)
1584 .send()?;
1585 self.json_or_feature_error(resp)
1586 }
1587
1588 // ------------------------------------------------------------------
1589 // Internal helpers
1590 // ------------------------------------------------------------------
1591
1592 /// Common handler for SRE endpoints returning JSON on `200`, mapping `403`
1593 /// to [`Error::FeatureDisabled`] (the feature is disabled on the server),
1594 /// `400` to [`Error::InvalidRequest`], and any other status to
1595 /// [`Error::UnexpectedStatus`]. An empty `200` body deserializes to JSON
1596 /// `null`.
1597 fn json_or_feature_error(&self, resp: reqwest::blocking::Response) -> Result<Value> {
1598 let status = resp.status().as_u16();
1599 match status {
1600 200 | 201 => {
1601 let text = resp.text()?;
1602 if text.is_empty() {
1603 Ok(Value::Null)
1604 } else {
1605 Ok(serde_json::from_str(&text)?)
1606 }
1607 }
1608 400 => Err(Error::InvalidRequest(resp.text()?)),
1609 403 => Err(Error::FeatureDisabled(resp.text()?)),
1610 _ => Err(Error::UnexpectedStatus {
1611 status,
1612 body: resp.text().unwrap_or_default(),
1613 }),
1614 }
1615 }
1616
1617 /// Handler for load-scenario registry endpoints. Like
1618 /// [`json_or_feature_error`](Self::json_or_feature_error) but additionally
1619 /// maps `404` to [`Error::NotFound`] (an unknown scenario name).
1620 fn load_scenario_json(&self, resp: reqwest::blocking::Response) -> Result<Value> {
1621 let status = resp.status().as_u16();
1622 match status {
1623 200 | 201 => {
1624 let text = resp.text()?;
1625 if text.is_empty() {
1626 Ok(Value::Null)
1627 } else {
1628 Ok(serde_json::from_str(&text)?)
1629 }
1630 }
1631 400 => Err(Error::InvalidRequest(resp.text()?)),
1632 403 => Err(Error::FeatureDisabled(resp.text()?)),
1633 404 => Err(Error::NotFound(resp.text()?)),
1634 _ => Err(Error::UnexpectedStatus {
1635 status,
1636 body: resp.text().unwrap_or_default(),
1637 }),
1638 }
1639 }
1640
1641 // ------------------------------------------------------------------
1642 // Clock control
1643 // ------------------------------------------------------------------
1644
1645 /// Freeze the simulated clock (`PUT /mockserver/clock`, `action=freeze`).
1646 ///
1647 /// Pass `Some(instant)` — an ISO-8601 instant string such as
1648 /// `"2024-01-01T00:00:00Z"` — to freeze at a specific time, or `None` to
1649 /// freeze at the current time. Returns the clock-status JSON the server
1650 /// echoes back.
1651 pub fn freeze_clock(&self, instant: Option<&str>) -> Result<String> {
1652 let body = match instant {
1653 Some(i) => serde_json::json!({ "action": "freeze", "instant": i }),
1654 None => serde_json::json!({ "action": "freeze" }),
1655 };
1656 self.clock_put(&body)
1657 }
1658
1659 /// Advance the simulated clock by `duration_millis` (`PUT /mockserver/clock`,
1660 /// `action=advance`). The value must be positive — the server returns `400`
1661 /// for `<= 0`. Returns the clock-status JSON the server echoes back.
1662 pub fn advance_clock(&self, duration_millis: i64) -> Result<String> {
1663 let body = serde_json::json!({ "action": "advance", "durationMillis": duration_millis });
1664 self.clock_put(&body)
1665 }
1666
1667 /// Reset the simulated clock back to the real system clock
1668 /// (`PUT /mockserver/clock`, `action=reset`). Returns the clock-status JSON.
1669 pub fn reset_clock(&self) -> Result<String> {
1670 let body = serde_json::json!({ "action": "reset" });
1671 self.clock_put(&body)
1672 }
1673
1674 /// Read the current clock status (`GET /mockserver/clock`). Returns the JSON
1675 /// body verbatim, e.g.
1676 /// `{"currentInstant":"...","currentEpochMillis":...,"frozen":true}`.
1677 pub fn clock_status(&self) -> Result<String> {
1678 let resp = self.http.get(self.url("/mockserver/clock")).send()?;
1679 let status = resp.status().as_u16();
1680 match status {
1681 200 => Ok(resp.text()?),
1682 400 => Err(Error::InvalidRequest(resp.text()?)),
1683 _ => Err(Error::UnexpectedStatus {
1684 status,
1685 body: resp.text().unwrap_or_default(),
1686 }),
1687 }
1688 }
1689
1690 fn clock_put(&self, body: &Value) -> Result<String> {
1691 let resp = self
1692 .http
1693 .put(self.url("/mockserver/clock"))
1694 .json(body)
1695 .send()?;
1696 let status = resp.status().as_u16();
1697 match status {
1698 200 => Ok(resp.text()?),
1699 400 => Err(Error::InvalidRequest(resp.text()?)),
1700 _ => Err(Error::UnexpectedStatus {
1701 status,
1702 body: resp.text().unwrap_or_default(),
1703 }),
1704 }
1705 }
1706
1707 // ------------------------------------------------------------------
1708 // Metrics
1709 // ------------------------------------------------------------------
1710
1711 /// Retrieve the JSON metrics counter snapshot
1712 /// (`PUT /mockserver/retrieve?type=METRICS`). Returns a flat JSON object
1713 /// mapping each metric name to its long value (`{}` when metrics are
1714 /// disabled).
1715 pub fn retrieve_metrics(&self) -> Result<String> {
1716 let url = format!(
1717 "{}?type=METRICS",
1718 self.url("/mockserver/retrieve")
1719 );
1720 let resp = self
1721 .http
1722 .put(&url)
1723 .header("Content-Type", "application/json")
1724 .body("")
1725 .send()?;
1726 let status = resp.status().as_u16();
1727 match status {
1728 200 => Ok(resp.text()?),
1729 400 => Err(Error::InvalidRequest(resp.text()?)),
1730 _ => Err(Error::UnexpectedStatus {
1731 status,
1732 body: resp.text().unwrap_or_default(),
1733 }),
1734 }
1735 }
1736
1737 /// Scrape the Prometheus exposition metrics (`GET /mockserver/metrics`).
1738 /// Returns the exposition text. When metrics are disabled the server replies
1739 /// `404`, surfaced as [`Error::NotFound`].
1740 pub fn scrape_metrics(&self) -> Result<String> {
1741 let resp = self.http.get(self.url("/mockserver/metrics")).send()?;
1742 let status = resp.status().as_u16();
1743 match status {
1744 200 => Ok(resp.text()?),
1745 404 => Err(Error::NotFound(resp.text().unwrap_or_default())),
1746 _ => Err(Error::UnexpectedStatus {
1747 status,
1748 body: resp.text().unwrap_or_default(),
1749 }),
1750 }
1751 }
1752
1753 // ------------------------------------------------------------------
1754 // Configuration
1755 // ------------------------------------------------------------------
1756
1757 /// Read the effective live configuration (`GET /mockserver/configuration`).
1758 /// Returns the serialized `Configuration` JSON.
1759 pub fn retrieve_configuration(&self) -> Result<String> {
1760 let resp = self
1761 .http
1762 .get(self.url("/mockserver/configuration"))
1763 .send()?;
1764 let status = resp.status().as_u16();
1765 match status {
1766 200 => Ok(resp.text()?),
1767 400 => Err(Error::InvalidRequest(resp.text()?)),
1768 _ => Err(Error::UnexpectedStatus {
1769 status,
1770 body: resp.text().unwrap_or_default(),
1771 }),
1772 }
1773 }
1774
1775 /// Update the live configuration (`PUT /mockserver/configuration`).
1776 ///
1777 /// `config_json` is a `ConfigurationDTO` JSON document — only the fields
1778 /// present are applied (partial update). Returns the serialized *updated*
1779 /// configuration JSON.
1780 pub fn update_configuration(&self, config_json: &str) -> Result<String> {
1781 let resp = self
1782 .http
1783 .put(self.url("/mockserver/configuration"))
1784 .header("Content-Type", "application/json")
1785 .body(config_json.to_string())
1786 .send()?;
1787 let status = resp.status().as_u16();
1788 match status {
1789 200 => Ok(resp.text()?),
1790 400 => Err(Error::InvalidRequest(resp.text()?)),
1791 _ => Err(Error::UnexpectedStatus {
1792 status,
1793 body: resp.text().unwrap_or_default(),
1794 }),
1795 }
1796 }
1797
1798 // ------------------------------------------------------------------
1799 // Drift detection
1800 // ------------------------------------------------------------------
1801
1802 /// Retrieve the recorded mock drift report (`GET /mockserver/drift`).
1803 ///
1804 /// Returns the serialized report JSON, of the form
1805 /// `{"count": <n>, "drifts": [ ... ]}`, where each entry describes a
1806 /// difference detected between a mock's configured response and the live
1807 /// upstream response for the same request.
1808 pub fn retrieve_drift(&self) -> Result<String> {
1809 let resp = self.http.get(self.url("/mockserver/drift")).send()?;
1810 let status = resp.status().as_u16();
1811 match status {
1812 200 => Ok(resp.text()?),
1813 _ => Err(Error::UnexpectedStatus {
1814 status,
1815 body: resp.text().unwrap_or_default(),
1816 }),
1817 }
1818 }
1819
1820 /// Clear all recorded mock drift (`PUT /mockserver/drift/clear`).
1821 pub fn clear_drift(&self) -> Result<()> {
1822 let resp = self
1823 .http
1824 .put(self.url("/mockserver/drift/clear"))
1825 .header("Content-Type", "application/json")
1826 .body("")
1827 .send()?;
1828
1829 let status = resp.status().as_u16();
1830 match status {
1831 200 => Ok(()),
1832 _ => Err(Error::UnexpectedStatus {
1833 status,
1834 body: resp.text().unwrap_or_default(),
1835 }),
1836 }
1837 }
1838
1839 // ------------------------------------------------------------------
1840 // Pact (import / export / verify)
1841 // ------------------------------------------------------------------
1842
1843 /// Import a Pact v3 contract (`PUT /mockserver/pact/import`).
1844 ///
1845 /// Returns the JSON array of upserted expectations the server creates.
1846 pub fn pact_import(&self, json: &str) -> Result<Vec<Expectation>> {
1847 let resp = self
1848 .http
1849 .put(self.url("/mockserver/pact/import"))
1850 .header("Content-Type", "application/json")
1851 .body(json.to_string())
1852 .send()?;
1853 self.expectations_response(resp)
1854 }
1855
1856 /// Export the active expectations as a Pact v3 contract
1857 /// (`PUT /mockserver/pact?consumer=&provider=`).
1858 ///
1859 /// A query parameter is only added when the corresponding value is non-blank
1860 /// (matching the Java client); blank values fall back to the server defaults.
1861 /// Returns the generated Pact JSON.
1862 pub fn pact_export(&self, consumer: &str, provider: &str) -> Result<String> {
1863 let mut params: Vec<(&str, &str)> = Vec::new();
1864 if !consumer.trim().is_empty() {
1865 params.push(("consumer", consumer));
1866 }
1867 if !provider.trim().is_empty() {
1868 params.push(("provider", provider));
1869 }
1870 let mut builder = self
1871 .http
1872 .put(self.url("/mockserver/pact"))
1873 .header("Content-Type", "application/json")
1874 .body("");
1875 if !params.is_empty() {
1876 builder = builder.query(¶ms);
1877 }
1878 let resp = builder.send()?;
1879 let status = resp.status().as_u16();
1880 match status {
1881 200 => Ok(resp.text()?),
1882 400 => Err(Error::InvalidRequest(resp.text()?)),
1883 _ => Err(Error::UnexpectedStatus {
1884 status,
1885 body: resp.text().unwrap_or_default(),
1886 }),
1887 }
1888 }
1889
1890 /// Verify a Pact v3 contract against the active expectations
1891 /// (`PUT /mockserver/pact/verify`).
1892 ///
1893 /// The verification *outcome* is returned in the [`Ok`] value rather than as
1894 /// an error: `202 ACCEPTED` maps to [`PactVerification`] with `passed = true`
1895 /// and `406 NOT_ACCEPTABLE` to `passed = false`. Both carry the server's
1896 /// verification report. A `400` (bad input) is surfaced as
1897 /// [`Error::InvalidRequest`].
1898 pub fn pact_verify(&self, json: &str) -> Result<PactVerification> {
1899 let resp = self
1900 .http
1901 .put(self.url("/mockserver/pact/verify"))
1902 .header("Content-Type", "application/json")
1903 .body(json.to_string())
1904 .send()?;
1905 let status = resp.status().as_u16();
1906 match status {
1907 202 => Ok(PactVerification {
1908 passed: true,
1909 report: resp.text()?,
1910 }),
1911 406 => Ok(PactVerification {
1912 passed: false,
1913 report: resp.text()?,
1914 }),
1915 400 => Err(Error::InvalidRequest(resp.text()?)),
1916 _ => Err(Error::UnexpectedStatus {
1917 status,
1918 body: resp.text().unwrap_or_default(),
1919 }),
1920 }
1921 }
1922
1923 // ------------------------------------------------------------------
1924 // File store
1925 // ------------------------------------------------------------------
1926
1927 /// Store a file in the in-memory file store (`PUT /mockserver/files/store`).
1928 ///
1929 /// `content` is sent base64-encoded (with `"base64": true`) so arbitrary
1930 /// binary data round-trips intact. Returns the `{"name":..,"size":..}` JSON.
1931 pub fn store_file(&self, name: &str, content: &[u8]) -> Result<String> {
1932 let body = serde_json::json!({
1933 "name": name,
1934 "content": BASE64.encode(content),
1935 "base64": true,
1936 });
1937 let resp = self
1938 .http
1939 .put(self.url("/mockserver/files/store"))
1940 .json(&body)
1941 .send()?;
1942 let status = resp.status().as_u16();
1943 match status {
1944 200 | 201 => Ok(resp.text()?),
1945 400 => Err(Error::InvalidRequest(resp.text()?)),
1946 _ => Err(Error::UnexpectedStatus {
1947 status,
1948 body: resp.text().unwrap_or_default(),
1949 }),
1950 }
1951 }
1952
1953 /// Retrieve a file's raw bytes (`PUT /mockserver/files/retrieve`).
1954 ///
1955 /// Returns the raw `200` body bytes. An unknown file (`404`) is surfaced as
1956 /// [`Error::NotFound`] (mirroring the crate's status-to-error mapping).
1957 pub fn retrieve_file(&self, name: &str) -> Result<Vec<u8>> {
1958 let body = serde_json::json!({ "name": name });
1959 let resp = self
1960 .http
1961 .put(self.url("/mockserver/files/retrieve"))
1962 .json(&body)
1963 .send()?;
1964 let status = resp.status().as_u16();
1965 match status {
1966 200 => Ok(resp.bytes()?.to_vec()),
1967 400 => Err(Error::InvalidRequest(resp.text()?)),
1968 404 => Err(Error::NotFound(resp.text().unwrap_or_default())),
1969 _ => Err(Error::UnexpectedStatus {
1970 status,
1971 body: resp.text().unwrap_or_default(),
1972 }),
1973 }
1974 }
1975
1976 /// List the names of all stored files (`PUT /mockserver/files/list`).
1977 pub fn list_files(&self) -> Result<Vec<String>> {
1978 let resp = self
1979 .http
1980 .put(self.url("/mockserver/files/list"))
1981 .header("Content-Type", "application/json")
1982 .body("")
1983 .send()?;
1984 let status = resp.status().as_u16();
1985 match status {
1986 200 => {
1987 let text = resp.text()?;
1988 if text.trim().is_empty() {
1989 Ok(vec![])
1990 } else {
1991 Ok(serde_json::from_str(&text)?)
1992 }
1993 }
1994 400 => Err(Error::InvalidRequest(resp.text()?)),
1995 _ => Err(Error::UnexpectedStatus {
1996 status,
1997 body: resp.text().unwrap_or_default(),
1998 }),
1999 }
2000 }
2001
2002 /// Delete a stored file (`PUT /mockserver/files/delete`). An unknown file
2003 /// (`404`) is surfaced as [`Error::NotFound`].
2004 pub fn delete_file(&self, name: &str) -> Result<()> {
2005 let body = serde_json::json!({ "name": name });
2006 let resp = self
2007 .http
2008 .put(self.url("/mockserver/files/delete"))
2009 .json(&body)
2010 .send()?;
2011 let status = resp.status().as_u16();
2012 match status {
2013 200 => Ok(()),
2014 400 => Err(Error::InvalidRequest(resp.text()?)),
2015 404 => Err(Error::NotFound(resp.text().unwrap_or_default())),
2016 _ => Err(Error::UnexpectedStatus {
2017 status,
2018 body: resp.text().unwrap_or_default(),
2019 }),
2020 }
2021 }
2022
2023 // ------------------------------------------------------------------
2024 // Import (HAR / Postman)
2025 // ------------------------------------------------------------------
2026
2027 /// Import a HAR document (`PUT /mockserver/import?format=har`). Returns the
2028 /// upserted expectations.
2029 pub fn import_har(&self, har_json: &str) -> Result<Vec<Expectation>> {
2030 self.import_document(har_json, "har")
2031 }
2032
2033 /// Import a Postman collection
2034 /// (`PUT /mockserver/import?format=postman`). Returns the upserted
2035 /// expectations.
2036 pub fn import_postman_collection(&self, collection_json: &str) -> Result<Vec<Expectation>> {
2037 self.import_document(collection_json, "postman")
2038 }
2039
2040 fn import_document(&self, json: &str, format: &str) -> Result<Vec<Expectation>> {
2041 let url = format!("{}?format={format}", self.url("/mockserver/import"));
2042 let resp = self
2043 .http
2044 .put(&url)
2045 .header("Content-Type", "application/json")
2046 .body(json.to_string())
2047 .send()?;
2048 self.expectations_response(resp)
2049 }
2050
2051 // ------------------------------------------------------------------
2052 // Operating mode
2053 // ------------------------------------------------------------------
2054
2055 /// Set the high-level operating mode (`PUT /mockserver/mode?mode=<MODE>`).
2056 /// Returns the `{"mode":..,"proxyUnmatchedRequests":..}` JSON.
2057 pub fn set_mode(&self, mode: MockMode) -> Result<String> {
2058 let url = format!("{}?mode={}", self.url("/mockserver/mode"), mode.as_str());
2059 let resp = self
2060 .http
2061 .put(&url)
2062 .header("Content-Type", "application/json")
2063 .body("")
2064 .send()?;
2065 let status = resp.status().as_u16();
2066 match status {
2067 200 => Ok(resp.text()?),
2068 400 => Err(Error::InvalidRequest(resp.text()?)),
2069 _ => Err(Error::UnexpectedStatus {
2070 status,
2071 body: resp.text().unwrap_or_default(),
2072 }),
2073 }
2074 }
2075
2076 /// Read the current operating mode (`GET /mockserver/mode`). Returns the
2077 /// `{"mode":..,"proxyUnmatchedRequests":..}` JSON.
2078 pub fn retrieve_mode(&self) -> Result<String> {
2079 let resp = self.http.get(self.url("/mockserver/mode")).send()?;
2080 let status = resp.status().as_u16();
2081 match status {
2082 200 => Ok(resp.text()?),
2083 400 => Err(Error::InvalidRequest(resp.text()?)),
2084 _ => Err(Error::UnexpectedStatus {
2085 status,
2086 body: resp.text().unwrap_or_default(),
2087 }),
2088 }
2089 }
2090
2091 // ------------------------------------------------------------------
2092 // WSDL
2093 // ------------------------------------------------------------------
2094
2095 /// Generate expectations from a WSDL document (`PUT /mockserver/wsdl`).
2096 ///
2097 /// The raw WSDL XML is sent as the request body (Content-Type `text/xml`).
2098 /// Returns the generated (upserted) expectations.
2099 pub fn wsdl_expectation(&self, wsdl: &str) -> Result<Vec<Expectation>> {
2100 let resp = self
2101 .http
2102 .put(self.url("/mockserver/wsdl"))
2103 .header("Content-Type", "text/xml")
2104 .body(wsdl.to_string())
2105 .send()?;
2106 self.expectations_response(resp)
2107 }
2108
2109 /// Map a `201`/`200` JSON-array-of-expectations response (used by import,
2110 /// pact-import and WSDL) to `Vec<Expectation>`, applying the crate's status
2111 /// conventions.
2112 fn expectations_response(
2113 &self,
2114 resp: reqwest::blocking::Response,
2115 ) -> Result<Vec<Expectation>> {
2116 let status = resp.status().as_u16();
2117 match status {
2118 200 | 201 => {
2119 let text = resp.text()?;
2120 if text.trim().is_empty() {
2121 Ok(vec![])
2122 } else {
2123 Ok(serde_json::from_str(&text)?)
2124 }
2125 }
2126 400 => Err(Error::InvalidRequest(resp.text()?)),
2127 _ => Err(Error::UnexpectedStatus {
2128 status,
2129 body: resp.text().unwrap_or_default(),
2130 }),
2131 }
2132 }
2133
2134 fn do_verify(&self, verification: &Verification) -> Result<()> {
2135 let resp = self
2136 .http
2137 .put(self.url("/mockserver/verify"))
2138 .json(verification)
2139 .send()?;
2140
2141 let status = resp.status().as_u16();
2142 match status {
2143 200 | 202 => Ok(()),
2144 406 => Err(Error::VerificationFailure(resp.text()?)),
2145 400 => Err(Error::InvalidRequest(resp.text()?)),
2146 _ => Err(Error::UnexpectedStatus {
2147 status,
2148 body: resp.text().unwrap_or_default(),
2149 }),
2150 }
2151 }
2152
2153 fn do_verify_sequence(&self, verification: &VerificationSequence) -> Result<()> {
2154 let resp = self
2155 .http
2156 .put(self.url("/mockserver/verifySequence"))
2157 .json(verification)
2158 .send()?;
2159
2160 let status = resp.status().as_u16();
2161 match status {
2162 200 | 202 => Ok(()),
2163 406 => Err(Error::VerificationFailure(resp.text()?)),
2164 400 => Err(Error::InvalidRequest(resp.text()?)),
2165 _ => Err(Error::UnexpectedStatus {
2166 status,
2167 body: resp.text().unwrap_or_default(),
2168 }),
2169 }
2170 }
2171
2172 fn url(&self, path: &str) -> String {
2173 format!("{}{path}", self.base_url)
2174 }
2175
2176 fn do_retrieve(
2177 &self,
2178 request: Option<&HttpRequest>,
2179 retrieve_type: RetrieveType,
2180 format: RetrieveFormat,
2181 ) -> Result<String> {
2182 let url = format!(
2183 "{}?type={}&format={}",
2184 self.url("/mockserver/retrieve"),
2185 retrieve_type.as_str(),
2186 format.as_str(),
2187 );
2188
2189 let mut builder = self.http.put(&url);
2190 builder = builder.header("Content-Type", "application/json");
2191 if let Some(req) = request {
2192 builder = builder.json(req);
2193 } else {
2194 builder = builder.body("");
2195 }
2196
2197 let resp = builder.send()?;
2198 let status = resp.status().as_u16();
2199 match status {
2200 200 => Ok(resp.text()?),
2201 400 => Err(Error::InvalidRequest(resp.text()?)),
2202 _ => Err(Error::UnexpectedStatus {
2203 status,
2204 body: resp.text().unwrap_or_default(),
2205 }),
2206 }
2207 }
2208}
2209
2210// ---------------------------------------------------------------------------
2211// ForwardChainExpectation (fluent builder)
2212// ---------------------------------------------------------------------------
2213
2214/// Fluent builder for creating an expectation via `client.when(...).respond(...)`.
2215pub struct ForwardChainExpectation<'a> {
2216 client: &'a MockServerClient,
2217 request: HttpRequest,
2218 times: Option<Times>,
2219 time_to_live: Option<TimeToLive>,
2220 priority: Option<i32>,
2221 id: Option<String>,
2222}
2223
2224impl<'a> ForwardChainExpectation<'a> {
2225 /// Set how many times this expectation should match.
2226 pub fn times(mut self, times: Times) -> Self {
2227 self.times = Some(times);
2228 self
2229 }
2230
2231 /// Set the time-to-live for this expectation.
2232 pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
2233 self.time_to_live = Some(ttl);
2234 self
2235 }
2236
2237 /// Set the priority for this expectation.
2238 pub fn priority(mut self, priority: i32) -> Self {
2239 self.priority = Some(priority);
2240 self
2241 }
2242
2243 /// Set the expectation ID (for upsert semantics).
2244 pub fn with_id(mut self, id: impl Into<String>) -> Self {
2245 self.id = Some(id.into());
2246 self
2247 }
2248
2249 /// Complete the expectation with a response action.
2250 pub fn respond(self, response: HttpResponse) -> Result<Vec<Expectation>> {
2251 let (client, expectation) = self.into_parts();
2252 let expectation = expectation.respond(response);
2253 client.upsert(&[expectation])
2254 }
2255
2256 /// Complete the expectation with a forward action.
2257 pub fn forward(self, forward: HttpForward) -> Result<Vec<Expectation>> {
2258 let (client, expectation) = self.into_parts();
2259 let expectation = expectation.forward(forward);
2260 client.upsert(&[expectation])
2261 }
2262
2263 /// Complete the expectation with an error action.
2264 pub fn error(self, error: HttpError) -> Result<Vec<Expectation>> {
2265 let (client, expectation) = self.into_parts();
2266 let expectation = expectation.error(error);
2267 client.upsert(&[expectation])
2268 }
2269
2270 /// Complete the expectation with a Server-Sent Events (SSE) response action.
2271 pub fn respond_sse(self, sse: HttpSseResponse) -> Result<Vec<Expectation>> {
2272 let (client, expectation) = self.into_parts();
2273 let expectation = expectation.respond_sse(sse);
2274 client.upsert(&[expectation])
2275 }
2276
2277 /// Complete the expectation with a WebSocket response action.
2278 pub fn respond_web_socket(self, ws: HttpWebSocketResponse) -> Result<Vec<Expectation>> {
2279 let (client, expectation) = self.into_parts();
2280 let expectation = expectation.respond_web_socket(ws);
2281 client.upsert(&[expectation])
2282 }
2283
2284 /// Complete the expectation with a DNS response action.
2285 pub fn respond_dns(self, dns: DnsResponse) -> Result<Vec<Expectation>> {
2286 let (client, expectation) = self.into_parts();
2287 let expectation = expectation.respond_dns(dns);
2288 client.upsert(&[expectation])
2289 }
2290
2291 /// Complete the expectation with a raw binary response action.
2292 pub fn respond_binary(self, binary: BinaryResponse) -> Result<Vec<Expectation>> {
2293 let (client, expectation) = self.into_parts();
2294 let expectation = expectation.respond_binary(binary);
2295 client.upsert(&[expectation])
2296 }
2297
2298 /// Complete the expectation with a gRPC streaming response action.
2299 pub fn respond_grpc_stream(self, grpc: GrpcStreamResponse) -> Result<Vec<Expectation>> {
2300 let (client, expectation) = self.into_parts();
2301 let expectation = expectation.respond_grpc_stream(grpc);
2302 client.upsert(&[expectation])
2303 }
2304
2305 // ------------------------------------------------------------------
2306 // `respond_with_*` aliases (cross-client naming parity)
2307 //
2308 // The Python, PHP, and .NET clients expose these advanced response
2309 // builders under `respond_with_*` names. These aliases give the Rust
2310 // fluent chain the same surface so examples translate verbatim across
2311 // clients; each simply delegates to its idiomatic Rust counterpart.
2312 // ------------------------------------------------------------------
2313
2314 /// Alias of [`respond_sse`](Self::respond_sse) for cross-client naming parity.
2315 pub fn respond_with_sse(self, sse: HttpSseResponse) -> Result<Vec<Expectation>> {
2316 self.respond_sse(sse)
2317 }
2318
2319 /// Alias of [`respond_web_socket`](Self::respond_web_socket) for cross-client naming parity.
2320 pub fn respond_with_web_socket(self, ws: HttpWebSocketResponse) -> Result<Vec<Expectation>> {
2321 self.respond_web_socket(ws)
2322 }
2323
2324 /// Alias of [`respond_dns`](Self::respond_dns) for cross-client naming parity.
2325 pub fn respond_with_dns(self, dns: DnsResponse) -> Result<Vec<Expectation>> {
2326 self.respond_dns(dns)
2327 }
2328
2329 /// Alias of [`respond_binary`](Self::respond_binary) for cross-client naming parity.
2330 pub fn respond_with_binary(self, binary: BinaryResponse) -> Result<Vec<Expectation>> {
2331 self.respond_binary(binary)
2332 }
2333
2334 /// Alias of [`respond_grpc_stream`](Self::respond_grpc_stream) for cross-client naming parity.
2335 pub fn respond_with_grpc_stream(self, grpc: GrpcStreamResponse) -> Result<Vec<Expectation>> {
2336 self.respond_grpc_stream(grpc)
2337 }
2338
2339 fn into_parts(self) -> (&'a MockServerClient, Expectation) {
2340 let ForwardChainExpectation {
2341 client,
2342 request,
2343 times,
2344 time_to_live,
2345 priority,
2346 id,
2347 } = self;
2348 let mut exp = Expectation::new(request);
2349 exp.times = times;
2350 exp.time_to_live = time_to_live;
2351 exp.priority = priority;
2352 exp.id = id;
2353 (client, exp)
2354 }
2355}
2356
2357// ---------------------------------------------------------------------------
2358// Scenario (control-plane handle)
2359// ---------------------------------------------------------------------------
2360
2361/// A handle for inspecting and driving a named scenario state-machine.
2362///
2363/// Obtained via [`MockServerClient::scenario`]. Each method issues a single
2364/// control-plane request against `/mockserver/scenario/{name}`.
2365pub struct Scenario<'a> {
2366 client: &'a MockServerClient,
2367 name: String,
2368}
2369
2370impl Scenario<'_> {
2371 /// Get the scenario's current state.
2372 ///
2373 /// Sends `GET /mockserver/scenario/{name}`.
2374 pub fn state(&self) -> Result<String> {
2375 self.client.scenario_state(&self.name)
2376 }
2377
2378 /// Set the scenario's state.
2379 ///
2380 /// Sends `PUT /mockserver/scenario/{name}` with `{"state": state}`.
2381 pub fn set(&self, state: &str) -> Result<()> {
2382 self.client.scenario_set(&self.name, state, None, None)
2383 }
2384
2385 /// Set the scenario's state and schedule an automatic transition to
2386 /// `next_state` after `transition_after_ms` milliseconds.
2387 ///
2388 /// Sends `PUT /mockserver/scenario/{name}` with
2389 /// `{"state", "transitionAfterMs", "nextState"}`.
2390 pub fn set_timed(
2391 &self,
2392 state: &str,
2393 transition_after_ms: u64,
2394 next_state: &str,
2395 ) -> Result<()> {
2396 self.client.scenario_set(
2397 &self.name,
2398 state,
2399 Some(transition_after_ms),
2400 Some(next_state),
2401 )
2402 }
2403
2404 /// Externally trigger a transition to `new_state`.
2405 ///
2406 /// Sends `PUT /mockserver/scenario/{name}/trigger` with `{"newState": new_state}`.
2407 pub fn trigger(&self, new_state: &str) -> Result<()> {
2408 self.client.scenario_trigger(&self.name, new_state)
2409 }
2410}