mockserver_client/client.rs
1//! The MockServer client and its builder.
2
3use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
4use reqwest::blocking::Client;
5use serde_json::Value;
6
7/// Characters percent-encoded when interpolating a scenario name into a path
8/// segment — everything unsafe for a single segment (matches the other clients'
9/// path-escaping). Unreserved characters (`-` `_` `.` `~`) are left intact.
10const SCENARIO_NAME: &AsciiSet = &CONTROLS
11 .add(b' ')
12 .add(b'/')
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
28/// Build the control-plane path for a named scenario with the name
29/// percent-encoded as a single path segment (matching the other MockServer clients).
30fn scenario_path(name: &str) -> String {
31 format!(
32 "/mockserver/scenario/{}",
33 utf8_percent_encode(name, SCENARIO_NAME)
34 )
35}
36
37use crate::breakpoint::{
38 BreakpointMatcherList, BreakpointMatcherRegistration, BreakpointMatcherResponse,
39 BreakpointRequestHandler, BreakpointResponseHandler, BreakpointStreamFrameHandler,
40 BreakpointWebSocketClient,
41};
42use crate::error::{Error, Result};
43use crate::model::*;
44
45// ---------------------------------------------------------------------------
46// ClientBuilder
47// ---------------------------------------------------------------------------
48
49/// Builder for constructing a [`MockServerClient`].
50///
51/// # Example
52/// ```no_run
53/// use mockserver_client::ClientBuilder;
54///
55/// let client = ClientBuilder::new("localhost", 1080)
56/// .context_path("/api")
57/// .secure(true)
58/// .build()
59/// .unwrap();
60/// ```
61pub struct ClientBuilder {
62 host: String,
63 port: u16,
64 context_path: String,
65 secure: bool,
66 tls_verify: bool,
67 control_plane_bearer_token: Option<String>,
68 ca_cert_pem: Option<Vec<u8>>,
69 /// Client identity for mTLS as `(certificate PEM, private key PEM)`.
70 client_identity_pem: Option<(Vec<u8>, Vec<u8>)>,
71}
72
73impl ClientBuilder {
74 /// Create a new builder targeting the given host and port.
75 pub fn new(host: impl Into<String>, port: u16) -> Self {
76 Self {
77 host: host.into(),
78 port,
79 context_path: String::new(),
80 secure: false,
81 tls_verify: true,
82 control_plane_bearer_token: None,
83 ca_cert_pem: None,
84 client_identity_pem: None,
85 }
86 }
87
88 /// Set a context path prefix (e.g., "/mockserver" if deployed behind a reverse proxy).
89 pub fn context_path(mut self, path: impl Into<String>) -> Self {
90 self.context_path = path.into();
91 self
92 }
93
94 /// Use HTTPS instead of HTTP.
95 pub fn secure(mut self, secure: bool) -> Self {
96 self.secure = secure;
97 self
98 }
99
100 /// Whether to verify TLS certificates (default: true).
101 pub fn tls_verify(mut self, verify: bool) -> Self {
102 self.tls_verify = verify;
103 self
104 }
105
106 /// Attach an `Authorization: Bearer <token>` header to **every control-plane
107 /// request** the built client sends.
108 ///
109 /// Use this when the server requires a JWT on the control plane
110 /// (`mockserver.controlPlaneJWTAuthenticationRequired=true`). The client does
111 /// not generate the token — supply the JWT string here. The header is only
112 /// sent on control-plane (`/mockserver/*`) requests issued by this client; it
113 /// is not added to any proxied/data-plane traffic.
114 ///
115 /// # Example
116 /// ```no_run
117 /// use mockserver_client::ClientBuilder;
118 ///
119 /// let client = ClientBuilder::new("localhost", 1080)
120 /// .secure(true)
121 /// .control_plane_bearer_token("eyJhbGciOi...")
122 /// .build()
123 /// .unwrap();
124 /// ```
125 pub fn control_plane_bearer_token(mut self, token: impl Into<String>) -> Self {
126 self.control_plane_bearer_token = Some(token.into());
127 self
128 }
129
130 /// Trust the given CA certificate (PEM file path) when connecting over HTTPS.
131 ///
132 /// Reads the PEM file and adds it as an additional trusted root so a
133 /// MockServer HTTPS certificate issued by that CA validates. Compose with
134 /// [`secure(true)`](Self::secure). The CA is added to — not a replacement for
135 /// — the platform's default trust store.
136 ///
137 /// Errors from reading the file surface when [`build`](Self::build) is called.
138 pub fn ca_cert_pem_path(mut self, path: impl AsRef<std::path::Path>) -> Self {
139 self.ca_cert_pem = std::fs::read(path.as_ref()).ok();
140 // Defer error reporting to build(); but if the read failed we still want
141 // build() to fail loudly rather than silently ignore the CA, so record a
142 // sentinel empty Vec which Certificate::from_pem will reject.
143 if self.ca_cert_pem.is_none() {
144 self.ca_cert_pem = Some(Vec::new());
145 }
146 self
147 }
148
149 /// Trust the given CA certificate (PEM bytes) when connecting over HTTPS.
150 ///
151 /// In-memory counterpart to [`ca_cert_pem_path`](Self::ca_cert_pem_path).
152 pub fn ca_cert_pem(mut self, bytes: impl Into<Vec<u8>>) -> Self {
153 self.ca_cert_pem = Some(bytes.into());
154 self
155 }
156
157 /// Present a client certificate + private key (PEM) for mutual TLS (mTLS).
158 ///
159 /// Reads the certificate and PKCS#8 private key PEM files and configures them
160 /// as the client identity used in the TLS handshake — required when the
161 /// server enforces `mockserver.controlPlaneTLSMutualAuthenticationRequired`.
162 ///
163 /// Errors from reading either file, or from building the identity, surface
164 /// when [`build`](Self::build) is called.
165 pub fn client_cert_pem(
166 mut self,
167 cert_path: impl AsRef<std::path::Path>,
168 key_path: impl AsRef<std::path::Path>,
169 ) -> Self {
170 match (
171 std::fs::read(cert_path.as_ref()),
172 std::fs::read(key_path.as_ref()),
173 ) {
174 (Ok(cert), Ok(key)) => self.client_identity_pem = Some((cert, key)),
175 // Record a sentinel empty buffer so build() fails loudly rather than
176 // silently dropping the requested client certificate.
177 _ => self.client_identity_pem = Some((Vec::new(), Vec::new())),
178 }
179 self
180 }
181
182 /// Build the client.
183 pub fn build(self) -> Result<MockServerClient> {
184 let scheme = if self.secure { "https" } else { "http" };
185 let ctx = if self.context_path.is_empty() {
186 String::new()
187 } else if self.context_path.starts_with('/') {
188 self.context_path
189 } else {
190 format!("/{}", self.context_path)
191 };
192 let base_url = format!("{scheme}://{}:{}{ctx}", self.host, self.port);
193
194 let mut builder = Client::builder().danger_accept_invalid_certs(!self.tls_verify);
195
196 // Attach the control-plane bearer token as a default header so it rides
197 // on every control-plane request this client issues (this client only
198 // ever talks to the `/mockserver/*` control plane).
199 if let Some(token) = self.control_plane_bearer_token {
200 let mut headers = reqwest::header::HeaderMap::new();
201 let mut value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
202 .map_err(|e| Error::InvalidRequest(format!("invalid bearer token: {e}")))?;
203 value.set_sensitive(true);
204 headers.insert(reqwest::header::AUTHORIZATION, value);
205 builder = builder.default_headers(headers);
206 }
207
208 if let Some(ca) = self.ca_cert_pem {
209 let cert = reqwest::Certificate::from_pem(&ca)?;
210 builder = builder.add_root_certificate(cert);
211 }
212
213 if let Some((cert_pem, key_pem)) = self.client_identity_pem {
214 let identity = reqwest::Identity::from_pkcs8_pem(&cert_pem, &key_pem)?;
215 builder = builder.identity(identity);
216 }
217
218 let http_client = builder.build()?;
219
220 Ok(MockServerClient {
221 base_url,
222 http: http_client,
223 breakpoint_ws: std::sync::Mutex::new(None),
224 })
225 }
226}
227
228// ---------------------------------------------------------------------------
229// MockServerClient
230// ---------------------------------------------------------------------------
231
232/// A blocking client for the MockServer control-plane REST API.
233///
234/// Created via [`ClientBuilder`]. All methods are synchronous.
235pub struct MockServerClient {
236 pub(crate) base_url: String,
237 http: Client,
238 breakpoint_ws: std::sync::Mutex<Option<BreakpointWebSocketClient>>,
239}
240
241impl MockServerClient {
242 // ------------------------------------------------------------------
243 // Expectation creation
244 // ------------------------------------------------------------------
245
246 /// Create one or more expectations on the server.
247 ///
248 /// Returns the created expectations as echoed by the server.
249 pub fn upsert(&self, expectations: &[Expectation]) -> Result<Vec<Expectation>> {
250 let body = serde_json::to_value(expectations)?;
251 let resp = self
252 .http
253 .put(self.url("/mockserver/expectation"))
254 .json(&body)
255 .send()?;
256
257 let status = resp.status().as_u16();
258 match status {
259 200 | 201 => {
260 let text = resp.text()?;
261 if text.is_empty() {
262 Ok(expectations.to_vec())
263 } else {
264 Ok(serde_json::from_str(&text)?)
265 }
266 }
267 400 => Err(Error::InvalidRequest(resp.text()?)),
268 _ => Err(Error::UnexpectedStatus {
269 status,
270 body: resp.text().unwrap_or_default(),
271 }),
272 }
273 }
274
275 /// Create one or more expectations from raw JSON values.
276 ///
277 /// This is the lower-level counterpart to [`upsert`](Self::upsert) for
278 /// expectation shapes that the typed [`Expectation`] model does not (yet)
279 /// cover — notably the `httpLlmResponse` action and conversation scenario
280 /// fields produced by the [`crate::llm`] builders, and the Velocity/JSON-RPC
281 /// expectations produced by the [`crate::mcp`] builder.
282 ///
283 /// The `expectations` value should be a JSON object (single expectation) or
284 /// a JSON array of expectation objects. Returns the raw JSON the server
285 /// echoes back (or the submitted value if the server returns an empty body).
286 pub fn upsert_raw(&self, expectations: Value) -> Result<Value> {
287 let resp = self
288 .http
289 .put(self.url("/mockserver/expectation"))
290 .json(&expectations)
291 .send()?;
292
293 let status = resp.status().as_u16();
294 match status {
295 200 | 201 => {
296 let text = resp.text()?;
297 if text.is_empty() {
298 Ok(expectations)
299 } else {
300 Ok(serde_json::from_str(&text)?)
301 }
302 }
303 400 => Err(Error::InvalidRequest(resp.text()?)),
304 _ => Err(Error::UnexpectedStatus {
305 status,
306 body: resp.text().unwrap_or_default(),
307 }),
308 }
309 }
310
311 // ------------------------------------------------------------------
312 // OpenAPI import
313 // ------------------------------------------------------------------
314
315 /// Register expectations from an OpenAPI/Swagger specification.
316 ///
317 /// Sends a `PUT /mockserver/openapi` with the given [`OpenApiExpectation`].
318 /// MockServer parses the spec and creates request matchers and example
319 /// responses for the selected operations (or every operation when none are
320 /// specified). Returns the created expectations as echoed by the server.
321 ///
322 /// # Example
323 /// ```no_run
324 /// use mockserver_client::{ClientBuilder, OpenApiExpectation};
325 ///
326 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
327 /// client.openapi(
328 /// &OpenApiExpectation::new("https://example.com/petstore.yaml")
329 /// .operation("listPets", "200"),
330 /// ).unwrap();
331 /// ```
332 pub fn openapi(&self, expectation: &OpenApiExpectation) -> Result<Vec<Expectation>> {
333 let resp = self
334 .http
335 .put(self.url("/mockserver/openapi"))
336 .json(expectation)
337 .send()?;
338
339 let status = resp.status().as_u16();
340 match status {
341 200 | 201 => {
342 let text = resp.text()?;
343 if text.is_empty() {
344 Ok(vec![])
345 } else {
346 Ok(serde_json::from_str(&text)?)
347 }
348 }
349 400 => Err(Error::InvalidRequest(resp.text()?)),
350 _ => Err(Error::UnexpectedStatus {
351 status,
352 body: resp.text().unwrap_or_default(),
353 }),
354 }
355 }
356
357 // ------------------------------------------------------------------
358 // Fluent API entry point
359 // ------------------------------------------------------------------
360
361 /// Begin building an expectation with the fluent `when(...).respond(...)` API.
362 ///
363 /// # Example
364 /// ```no_run
365 /// use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};
366 ///
367 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
368 /// client.when(HttpRequest::new().method("GET").path("/foo"))
369 /// .respond(HttpResponse::new().status_code(200).body("bar"))
370 /// .unwrap();
371 /// ```
372 pub fn when(&self, request: HttpRequest) -> ForwardChainExpectation<'_> {
373 ForwardChainExpectation {
374 client: self,
375 request,
376 times: None,
377 time_to_live: None,
378 priority: None,
379 id: None,
380 }
381 }
382
383 // ------------------------------------------------------------------
384 // Verify
385 // ------------------------------------------------------------------
386
387 /// Verify that a request was received the specified number of times.
388 ///
389 /// Returns `Ok(())` if verification passes, or
390 /// `Err(Error::VerificationFailure)` with the server's failure message.
391 pub fn verify(&self, request: HttpRequest, times: VerificationTimes) -> Result<()> {
392 let verification = Verification {
393 http_request: Some(request),
394 http_response: None,
395 times: Some(times),
396 maximum_number_of_request_to_return_in_verification_failure: None,
397 };
398 self.do_verify(&verification)
399 }
400
401 /// Verify that a request/response pair was received the specified number of times.
402 ///
403 /// Both the request matcher and the response matcher must match for a
404 /// recorded exchange to count. The response matcher uses the same
405 /// [`HttpResponse`] type as expectations — the server matches against the
406 /// recorded response's status code, headers, and body.
407 pub fn verify_request_and_response(
408 &self,
409 request: HttpRequest,
410 response: HttpResponse,
411 times: VerificationTimes,
412 ) -> Result<()> {
413 let verification = Verification {
414 http_request: Some(request),
415 http_response: Some(response),
416 times: Some(times),
417 maximum_number_of_request_to_return_in_verification_failure: None,
418 };
419 self.do_verify(&verification)
420 }
421
422 /// Verify that a response (regardless of request) was returned the
423 /// specified number of times.
424 ///
425 /// The `httpRequest` field is omitted from the JSON so the server matches
426 /// any request.
427 pub fn verify_response(&self, response: HttpResponse, times: VerificationTimes) -> Result<()> {
428 let verification = Verification {
429 http_request: None,
430 http_response: Some(response),
431 times: Some(times),
432 maximum_number_of_request_to_return_in_verification_failure: None,
433 };
434 self.do_verify(&verification)
435 }
436
437 /// Verify that no requests at all were received by the server.
438 ///
439 /// Thin wrapper over [`verify`](Self::verify): matches any request (an empty
440 /// matcher) with `exactly(0)` times. Returns `Ok(())` if the server received
441 /// no requests, or `Err(Error::VerificationFailure)` otherwise.
442 pub fn verify_zero_interactions(&self) -> Result<()> {
443 self.verify(HttpRequest::new(), VerificationTimes::exactly(0))
444 }
445
446 /// Send a fully constructed [`Verification`] to the server.
447 ///
448 /// This is the most flexible form — callers can set every field,
449 /// including `maximum_number_of_request_to_return_in_verification_failure`.
450 pub fn verify_raw(&self, verification: &Verification) -> Result<()> {
451 self.do_verify(verification)
452 }
453
454 /// Verify that requests were received in the given order.
455 pub fn verify_sequence(&self, requests: Vec<HttpRequest>) -> Result<()> {
456 let verification = VerificationSequence {
457 http_requests: Some(requests),
458 http_responses: None,
459 };
460 self.do_verify_sequence(&verification)
461 }
462
463 /// Verify that request/response pairs were received in the given order.
464 ///
465 /// `responses` is index-aligned with `requests` — each entry constrains
466 /// the response that must have been returned for the corresponding request.
467 pub fn verify_sequence_with_responses(
468 &self,
469 requests: Vec<HttpRequest>,
470 responses: Vec<HttpResponse>,
471 ) -> Result<()> {
472 let verification = VerificationSequence {
473 http_requests: Some(requests),
474 http_responses: Some(responses),
475 };
476 self.do_verify_sequence(&verification)
477 }
478
479 /// Send a fully constructed [`VerificationSequence`] to the server.
480 pub fn verify_sequence_raw(&self, verification: &VerificationSequence) -> Result<()> {
481 self.do_verify_sequence(verification)
482 }
483
484 // ------------------------------------------------------------------
485 // Clear / Reset
486 // ------------------------------------------------------------------
487
488 /// Clear expectations and/or logs matching the given request.
489 ///
490 /// If `request` is `None`, clears everything of the specified type.
491 pub fn clear(
492 &self,
493 request: Option<&HttpRequest>,
494 clear_type: Option<ClearType>,
495 ) -> Result<()> {
496 let mut url = self.url("/mockserver/clear");
497 if let Some(ct) = clear_type {
498 url = format!("{url}?type={}", ct.as_str());
499 }
500
501 let mut builder = self.http.put(&url);
502 builder = builder.header("Content-Type", "application/json");
503 if let Some(req) = request {
504 builder = builder.json(req);
505 } else {
506 builder = builder.body("");
507 }
508
509 let resp = builder.send()?;
510 let status = resp.status().as_u16();
511 match status {
512 200 => Ok(()),
513 400 => Err(Error::InvalidRequest(resp.text()?)),
514 _ => Err(Error::UnexpectedStatus {
515 status,
516 body: resp.text().unwrap_or_default(),
517 }),
518 }
519 }
520
521 /// Clear expectations by expectation ID.
522 pub fn clear_by_id(
523 &self,
524 expectation_id: impl Into<String>,
525 clear_type: Option<ClearType>,
526 ) -> Result<()> {
527 let mut url = self.url("/mockserver/clear");
528 if let Some(ct) = clear_type {
529 url = format!("{url}?type={}", ct.as_str());
530 }
531
532 let body = serde_json::json!({ "id": expectation_id.into() });
533 let resp = self.http.put(&url).json(&body).send()?;
534
535 let status = resp.status().as_u16();
536 match status {
537 200 => Ok(()),
538 400 => Err(Error::InvalidRequest(resp.text()?)),
539 _ => Err(Error::UnexpectedStatus {
540 status,
541 body: resp.text().unwrap_or_default(),
542 }),
543 }
544 }
545
546 /// Reset all expectations and recorded requests.
547 pub fn reset(&self) -> Result<()> {
548 let resp = self
549 .http
550 .put(self.url("/mockserver/reset"))
551 .header("Content-Type", "application/json")
552 .body("")
553 .send()?;
554
555 let status = resp.status().as_u16();
556 match status {
557 200 => Ok(()),
558 _ => Err(Error::UnexpectedStatus {
559 status,
560 body: resp.text().unwrap_or_default(),
561 }),
562 }
563 }
564
565 // ------------------------------------------------------------------
566 // Retrieve
567 // ------------------------------------------------------------------
568
569 /// Retrieve recorded requests matching the optional filter.
570 pub fn retrieve_recorded_requests(
571 &self,
572 request: Option<&HttpRequest>,
573 ) -> Result<Vec<HttpRequest>> {
574 let text = self.do_retrieve(request, RetrieveType::Requests, RetrieveFormat::Json)?;
575 if text.is_empty() {
576 return Ok(vec![]);
577 }
578 Ok(serde_json::from_str(&text)?)
579 }
580
581 /// Retrieve active expectations matching the optional filter.
582 pub fn retrieve_active_expectations(
583 &self,
584 request: Option<&HttpRequest>,
585 ) -> Result<Vec<Expectation>> {
586 let text = self.do_retrieve(
587 request,
588 RetrieveType::ActiveExpectations,
589 RetrieveFormat::Json,
590 )?;
591 if text.is_empty() {
592 return Ok(vec![]);
593 }
594 Ok(serde_json::from_str(&text)?)
595 }
596
597 /// Retrieve recorded expectations matching the optional filter.
598 pub fn retrieve_recorded_expectations(
599 &self,
600 request: Option<&HttpRequest>,
601 ) -> Result<Vec<Expectation>> {
602 let text = self.do_retrieve(
603 request,
604 RetrieveType::RecordedExpectations,
605 RetrieveFormat::Json,
606 )?;
607 if text.is_empty() {
608 return Ok(vec![]);
609 }
610 Ok(serde_json::from_str(&text)?)
611 }
612
613 /// Retrieve the active expectations as MockServer SDK setup code (the
614 /// builder code that recreates the expectations) in the requested language.
615 ///
616 /// `format` must be one of the code-generation variants of
617 /// [`RetrieveFormat`] (e.g. [`RetrieveFormat::Java`],
618 /// [`RetrieveFormat::Rust`]). The generated code is returned as a string.
619 pub fn retrieve_expectations_as_code(
620 &self,
621 format: RetrieveFormat,
622 request: Option<&HttpRequest>,
623 ) -> Result<String> {
624 self.do_retrieve(request, RetrieveType::ActiveExpectations, format)
625 }
626
627 /// Retrieve the recorded (proxied) request/response pairs as MockServer SDK
628 /// setup code in the requested language.
629 ///
630 /// `format` must be one of the code-generation variants of
631 /// [`RetrieveFormat`]. The generated code is returned as a string.
632 pub fn retrieve_recorded_expectations_as_code(
633 &self,
634 format: RetrieveFormat,
635 request: Option<&HttpRequest>,
636 ) -> Result<String> {
637 self.do_retrieve(request, RetrieveType::RecordedExpectations, format)
638 }
639
640 /// Retrieve log messages matching the optional filter.
641 pub fn retrieve_log_messages(&self, request: Option<&HttpRequest>) -> Result<Vec<String>> {
642 let text = self.do_retrieve(request, RetrieveType::Logs, RetrieveFormat::LogEntries)?;
643 if text.is_empty() {
644 return Ok(vec![]);
645 }
646 // Log messages may be returned as a JSON array of strings or as a
647 // separator-delimited block. Try JSON first.
648 if let Ok(arr) = serde_json::from_str::<Vec<String>>(&text) {
649 return Ok(arr);
650 }
651 // Fall back to splitting on the separator used by MockServer.
652 Ok(text
653 .split("------------------------------------\n")
654 .map(|s| s.to_string())
655 .filter(|s| !s.is_empty())
656 .collect())
657 }
658
659 /// Retrieve recorded request/response pairs.
660 pub fn retrieve_request_responses(&self, request: Option<&HttpRequest>) -> Result<Vec<Value>> {
661 let text = self.do_retrieve(
662 request,
663 RetrieveType::RequestResponses,
664 RetrieveFormat::Json,
665 )?;
666 if text.is_empty() {
667 return Ok(vec![]);
668 }
669 Ok(serde_json::from_str(&text)?)
670 }
671
672 // ------------------------------------------------------------------
673 // Status / Bind
674 // ------------------------------------------------------------------
675
676 /// Query the server's listening ports.
677 pub fn status(&self) -> Result<Ports> {
678 let resp = self
679 .http
680 .put(self.url("/mockserver/status"))
681 .header("Content-Type", "application/json")
682 .body("")
683 .send()?;
684
685 let status = resp.status().as_u16();
686 match status {
687 200 => {
688 let text = resp.text()?;
689 Ok(serde_json::from_str(&text)?)
690 }
691 _ => Err(Error::UnexpectedStatus {
692 status,
693 body: resp.text().unwrap_or_default(),
694 }),
695 }
696 }
697
698 /// Bind additional listening ports.
699 pub fn bind(&self, ports: &[u16]) -> Result<Ports> {
700 let body = Ports {
701 ports: ports.to_vec(),
702 };
703 let resp = self
704 .http
705 .put(self.url("/mockserver/bind"))
706 .json(&body)
707 .send()?;
708
709 let status = resp.status().as_u16();
710 match status {
711 200 => {
712 let text = resp.text()?;
713 Ok(serde_json::from_str(&text)?)
714 }
715 400 => Err(Error::InvalidRequest(resp.text()?)),
716 406 => Err(Error::VerificationFailure(resp.text()?)),
717 _ => Err(Error::UnexpectedStatus {
718 status,
719 body: resp.text().unwrap_or_default(),
720 }),
721 }
722 }
723
724 /// Check if the MockServer has started (polls with retries).
725 pub fn has_started(&self, attempts: u32, timeout_ms: u64) -> bool {
726 for i in 0..attempts {
727 match self.status() {
728 Ok(_) => return true,
729 Err(_) => {
730 if i < attempts - 1 {
731 std::thread::sleep(std::time::Duration::from_millis(timeout_ms));
732 }
733 }
734 }
735 }
736 false
737 }
738
739 // ------------------------------------------------------------------
740 // Breakpoints
741 // ------------------------------------------------------------------
742
743 /// Ensure the breakpoint WebSocket client is connected and return the clientId.
744 /// If the existing connection's read loop has exited, it is replaced transparently.
745 fn ensure_breakpoint_ws(&self) -> Result<String> {
746 let mut guard = self.breakpoint_ws.lock().unwrap();
747 let needs_connect = match guard.as_ref() {
748 None => true,
749 Some(ws) => ws.is_dead(),
750 };
751 if needs_connect {
752 // Close the old dead connection if present
753 if let Some(old) = guard.take() {
754 old.close();
755 }
756 let ws = BreakpointWebSocketClient::connect(&self.base_url)?;
757 *guard = Some(ws);
758 }
759 Ok(guard.as_ref().unwrap().client_id.clone())
760 }
761
762 /// Register a breakpoint matcher with the given phases and handlers.
763 /// Returns the server-assigned breakpoint id.
764 pub fn add_breakpoint(
765 &self,
766 matcher: HttpRequest,
767 phases: &[&str],
768 request_handler: Option<BreakpointRequestHandler>,
769 response_handler: Option<BreakpointResponseHandler>,
770 stream_frame_handler: Option<BreakpointStreamFrameHandler>,
771 ) -> Result<String> {
772 if phases.is_empty() {
773 return Err(Error::InvalidRequest(
774 "At least one phase is required".into(),
775 ));
776 }
777
778 let client_id = self.ensure_breakpoint_ws()?;
779
780 let reg = BreakpointMatcherRegistration {
781 http_request: matcher,
782 phases: phases.iter().map(|s| s.to_string()).collect(),
783 client_id: Some(client_id),
784 };
785
786 let resp = self
787 .http
788 .put(self.url("/mockserver/breakpoint/matcher"))
789 .json(®)
790 .send()?;
791
792 let status = resp.status().as_u16();
793 let text = resp.text()?;
794 if status >= 400 {
795 return Err(Error::UnexpectedStatus { status, body: text });
796 }
797
798 let result: BreakpointMatcherResponse = serde_json::from_str(&text)?;
799 let id = result.id.clone();
800
801 // Register handlers
802 let guard = self.breakpoint_ws.lock().unwrap();
803 if let Some(ws) = guard.as_ref() {
804 if let Some(h) = request_handler {
805 ws.set_request_handler(&id, h);
806 }
807 if let Some(h) = response_handler {
808 ws.set_response_handler(&id, h);
809 }
810 if let Some(h) = stream_frame_handler {
811 ws.set_stream_frame_handler(&id, h);
812 }
813 }
814
815 Ok(id)
816 }
817
818 /// Convenience: register a REQUEST-only breakpoint.
819 pub fn add_request_breakpoint(
820 &self,
821 matcher: HttpRequest,
822 handler: BreakpointRequestHandler,
823 ) -> Result<String> {
824 self.add_breakpoint(
825 matcher,
826 &[crate::breakpoint::phase::REQUEST],
827 Some(handler),
828 None,
829 None,
830 )
831 }
832
833 /// Convenience: register a REQUEST + RESPONSE breakpoint.
834 pub fn add_request_response_breakpoint(
835 &self,
836 matcher: HttpRequest,
837 request_handler: BreakpointRequestHandler,
838 response_handler: BreakpointResponseHandler,
839 ) -> Result<String> {
840 self.add_breakpoint(
841 matcher,
842 &[
843 crate::breakpoint::phase::REQUEST,
844 crate::breakpoint::phase::RESPONSE,
845 ],
846 Some(request_handler),
847 Some(response_handler),
848 None,
849 )
850 }
851
852 /// Convenience: register a streaming-phase breakpoint.
853 pub fn add_stream_breakpoint(
854 &self,
855 matcher: HttpRequest,
856 phases: &[&str],
857 handler: BreakpointStreamFrameHandler,
858 ) -> Result<String> {
859 self.add_breakpoint(matcher, phases, None, None, Some(handler))
860 }
861
862 /// List all registered breakpoint matchers.
863 pub fn list_breakpoint_matchers(&self) -> Result<BreakpointMatcherList> {
864 let resp = self
865 .http
866 .get(self.url("/mockserver/breakpoint/matchers"))
867 .send()?;
868
869 let status = resp.status().as_u16();
870 let text = resp.text()?;
871 if status >= 400 {
872 return Err(Error::UnexpectedStatus { status, body: text });
873 }
874
875 Ok(serde_json::from_str(&text)?)
876 }
877
878 /// Remove a breakpoint matcher by id.
879 pub fn remove_breakpoint_matcher(&self, id: impl Into<String>) -> Result<()> {
880 let id = id.into();
881 let body = serde_json::json!({ "id": &id });
882 let resp = self
883 .http
884 .put(self.url("/mockserver/breakpoint/matcher/remove"))
885 .json(&body)
886 .send()?;
887
888 let status = resp.status().as_u16();
889 match status {
890 200 => {
891 let guard = self.breakpoint_ws.lock().unwrap();
892 if let Some(ws) = guard.as_ref() {
893 ws.remove_handlers(&id);
894 }
895 Ok(())
896 }
897 404 => Err(Error::InvalidRequest(format!(
898 "Breakpoint matcher not found: {id}"
899 ))),
900 _ => Err(Error::UnexpectedStatus {
901 status,
902 body: resp.text().unwrap_or_default(),
903 }),
904 }
905 }
906
907 /// Remove all registered breakpoint matchers.
908 pub fn clear_breakpoint_matchers(&self) -> Result<()> {
909 let resp = self
910 .http
911 .put(self.url("/mockserver/breakpoint/matcher/clear"))
912 .header("Content-Type", "application/json")
913 .body("")
914 .send()?;
915
916 let status = resp.status().as_u16();
917 if status >= 400 {
918 return Err(Error::UnexpectedStatus {
919 status,
920 body: resp.text().unwrap_or_default(),
921 });
922 }
923
924 let guard = self.breakpoint_ws.lock().unwrap();
925 if let Some(ws) = guard.as_ref() {
926 ws.clear_handlers();
927 }
928 Ok(())
929 }
930
931 /// Close the breakpoint callback WebSocket connection.
932 pub fn close_breakpoint_websocket(&self) {
933 let mut guard = self.breakpoint_ws.lock().unwrap();
934 if let Some(ws) = guard.take() {
935 ws.close();
936 }
937 }
938
939 // ------------------------------------------------------------------
940 // Object (closure) callbacks
941 // ------------------------------------------------------------------
942
943 /// Register an expectation whose response is produced by a Rust closure
944 /// invoked over the callback WebSocket (an `httpResponseObjectCallback`).
945 ///
946 /// When a request matches `matcher`, MockServer pushes it to this client over
947 /// the shared callback WebSocket; `handler` receives the [`HttpRequest`] and
948 /// returns the [`HttpResponse`] to send back. The closure runs on the client's
949 /// background WebSocket-read thread, so it must be `Send + 'static`.
950 ///
951 /// The callback WebSocket is shared with breakpoints — only one socket is
952 /// opened per client. There is a single object-response handler per client;
953 /// calling this again replaces it. Narrow which requests reach the closure
954 /// with the `matcher`.
955 ///
956 /// # Example
957 /// ```no_run
958 /// use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};
959 ///
960 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
961 /// client.mock_with_callback(
962 /// HttpRequest::new().method("GET").path("/echo"),
963 /// |req| {
964 /// HttpResponse::new()
965 /// .status_code(200)
966 /// .body(format!("you asked for {}", req.path.unwrap_or_default()))
967 /// },
968 /// ).unwrap();
969 /// ```
970 pub fn mock_with_callback<F>(
971 &self,
972 matcher: HttpRequest,
973 handler: F,
974 ) -> Result<Vec<Expectation>>
975 where
976 F: Fn(HttpRequest) -> HttpResponse + Send + 'static,
977 {
978 // Ensure the shared callback WebSocket is connected and learn its clientId.
979 let client_id = self.ensure_breakpoint_ws()?;
980
981 // Adapt the typed closure to the JSON-level ObjectResponseHandler the WS
982 // read loop drives. The reply must echo the WebSocketCorrelationId header,
983 // which route_object_callback re-applies after the closure returns.
984 let object_handler: crate::breakpoint::ObjectResponseHandler =
985 Box::new(move |request_json: Value| {
986 let request: HttpRequest =
987 serde_json::from_value(request_json).unwrap_or_default();
988 let response = handler(request);
989 serde_json::to_value(&response).unwrap_or_else(|_| serde_json::json!({}))
990 });
991
992 {
993 let guard = self.breakpoint_ws.lock().unwrap();
994 if let Some(ws) = guard.as_ref() {
995 ws.set_object_response_handler(object_handler);
996 }
997 }
998
999 let expectation = Expectation::new(matcher)
1000 .respond_object_callback(HttpObjectCallback::new(client_id));
1001 self.upsert(&[expectation])
1002 }
1003
1004 // ------------------------------------------------------------------
1005 // gRPC descriptor management
1006 // ------------------------------------------------------------------
1007
1008 /// Upload a compiled protobuf descriptor set so gRPC requests can be matched.
1009 ///
1010 /// `descriptor` must be the raw bytes of a `FileDescriptorSet` (e.g. the
1011 /// output of `protoc --descriptor_set_out=... --include_imports`). The bytes
1012 /// are sent verbatim as `application/octet-stream` — they are **not**
1013 /// base64-encoded. Sends a `PUT /mockserver/grpc/descriptors`; the server
1014 /// responds `201 Created` on success.
1015 ///
1016 /// # Example
1017 /// ```no_run
1018 /// use mockserver_client::ClientBuilder;
1019 ///
1020 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
1021 /// let descriptor_set: Vec<u8> = std::fs::read("greeter.desc").unwrap();
1022 /// client.upload_grpc_descriptor(&descriptor_set).unwrap();
1023 /// ```
1024 pub fn upload_grpc_descriptor(&self, descriptor: &[u8]) -> Result<()> {
1025 if descriptor.is_empty() {
1026 return Err(Error::InvalidRequest(
1027 "descriptor set bytes must not be empty".into(),
1028 ));
1029 }
1030 let resp = self
1031 .http
1032 .put(self.url("/mockserver/grpc/descriptors"))
1033 .header("Content-Type", "application/octet-stream")
1034 .body(descriptor.to_vec())
1035 .send()?;
1036
1037 let status = resp.status().as_u16();
1038 match status {
1039 200 | 201 => Ok(()),
1040 400 => Err(Error::InvalidRequest(resp.text()?)),
1041 _ => Err(Error::UnexpectedStatus {
1042 status,
1043 body: resp.text().unwrap_or_default(),
1044 }),
1045 }
1046 }
1047
1048 /// Retrieve the gRPC services registered from uploaded descriptor sets.
1049 ///
1050 /// Sends a `PUT /mockserver/grpc/services` and returns the parsed list of
1051 /// [`GrpcService`]s, each with its [`GrpcMethod`]s.
1052 pub fn retrieve_grpc_services(&self) -> Result<Vec<GrpcService>> {
1053 let resp = self
1054 .http
1055 .put(self.url("/mockserver/grpc/services"))
1056 .header("Content-Type", "application/json")
1057 .body("")
1058 .send()?;
1059
1060 let status = resp.status().as_u16();
1061 match status {
1062 200 => {
1063 let text = resp.text()?;
1064 if text.is_empty() {
1065 Ok(vec![])
1066 } else {
1067 Ok(serde_json::from_str(&text)?)
1068 }
1069 }
1070 400 => Err(Error::InvalidRequest(resp.text()?)),
1071 _ => Err(Error::UnexpectedStatus {
1072 status,
1073 body: resp.text().unwrap_or_default(),
1074 }),
1075 }
1076 }
1077
1078 /// Clear all uploaded gRPC descriptor sets and registered services.
1079 ///
1080 /// Sends a `PUT /mockserver/grpc/clear`; the server responds `200 OK`.
1081 pub fn clear_grpc_descriptors(&self) -> Result<()> {
1082 let resp = self
1083 .http
1084 .put(self.url("/mockserver/grpc/clear"))
1085 .header("Content-Type", "application/json")
1086 .body("")
1087 .send()?;
1088
1089 let status = resp.status().as_u16();
1090 match status {
1091 200 => Ok(()),
1092 400 => Err(Error::InvalidRequest(resp.text()?)),
1093 _ => Err(Error::UnexpectedStatus {
1094 status,
1095 body: resp.text().unwrap_or_default(),
1096 }),
1097 }
1098 }
1099
1100 // ------------------------------------------------------------------
1101 // Stateful scenarios
1102 // ------------------------------------------------------------------
1103
1104 /// Obtain a handle for inspecting and driving a named scenario state-machine.
1105 ///
1106 /// The returned [`Scenario`] borrows the client and issues control-plane
1107 /// requests against `/mockserver/scenario/{name}`.
1108 ///
1109 /// # Example
1110 /// ```no_run
1111 /// use mockserver_client::ClientBuilder;
1112 ///
1113 /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
1114 /// client.scenario("Deploy").set("Deploying").unwrap();
1115 /// client.scenario("Deploy").set_timed("Deploying", 5000, "Deployed").unwrap();
1116 /// client.scenario("Deploy").trigger("Failed").unwrap();
1117 /// let state = client.scenario("Deploy").state().unwrap();
1118 /// assert_eq!(state, "Failed");
1119 /// ```
1120 pub fn scenario(&self, name: &str) -> Scenario<'_> {
1121 Scenario {
1122 client: self,
1123 name: name.to_string(),
1124 }
1125 }
1126
1127 /// List every known scenario and its current state.
1128 ///
1129 /// Sends a `GET /mockserver/scenario` and returns the parsed list of
1130 /// [`ScenarioState`]s.
1131 pub fn scenarios(&self) -> Result<Vec<ScenarioState>> {
1132 let resp = self.http.get(self.url("/mockserver/scenario")).send()?;
1133 let status = resp.status().as_u16();
1134 match status {
1135 200 => {
1136 let text = resp.text()?;
1137 if text.is_empty() {
1138 Ok(vec![])
1139 } else {
1140 let list: ScenarioList = serde_json::from_str(&text)?;
1141 Ok(list.scenarios)
1142 }
1143 }
1144 400 => Err(Error::InvalidRequest(resp.text()?)),
1145 _ => Err(Error::UnexpectedStatus {
1146 status,
1147 body: resp.text().unwrap_or_default(),
1148 }),
1149 }
1150 }
1151
1152 /// Get the current state of a named scenario.
1153 fn scenario_state(&self, name: &str) -> Result<String> {
1154 let resp = self
1155 .http
1156 .get(self.url(&scenario_path(name)))
1157 .send()?;
1158 let status = resp.status().as_u16();
1159 match status {
1160 200 => {
1161 let text = resp.text()?;
1162 let state: ScenarioState = serde_json::from_str(&text)?;
1163 Ok(state.current_state)
1164 }
1165 400 => Err(Error::InvalidRequest(resp.text()?)),
1166 _ => Err(Error::UnexpectedStatus {
1167 status,
1168 body: resp.text().unwrap_or_default(),
1169 }),
1170 }
1171 }
1172
1173 /// Set a scenario's state, optionally scheduling a timed transition.
1174 fn scenario_set(
1175 &self,
1176 name: &str,
1177 state: &str,
1178 transition_after_ms: Option<u64>,
1179 next_state: Option<&str>,
1180 ) -> Result<()> {
1181 let mut body = serde_json::json!({ "state": state });
1182 if let Some(ms) = transition_after_ms {
1183 body["transitionAfterMs"] = serde_json::json!(ms);
1184 }
1185 if let Some(next) = next_state {
1186 body["nextState"] = serde_json::json!(next);
1187 }
1188 let resp = self
1189 .http
1190 .put(self.url(&scenario_path(name)))
1191 .json(&body)
1192 .send()?;
1193 self.scenario_ok(resp)
1194 }
1195
1196 /// Externally trigger a scenario state transition.
1197 fn scenario_trigger(&self, name: &str, new_state: &str) -> Result<()> {
1198 let body = serde_json::json!({ "newState": new_state });
1199 let resp = self
1200 .http
1201 .put(self.url(&format!("{}/trigger", scenario_path(name))))
1202 .json(&body)
1203 .send()?;
1204 self.scenario_ok(resp)
1205 }
1206
1207 /// Map a scenario REST response to `Ok(())` on `200`, surfacing the server's
1208 /// error body on `400` and any other status as [`Error::UnexpectedStatus`].
1209 fn scenario_ok(&self, resp: reqwest::blocking::Response) -> Result<()> {
1210 let status = resp.status().as_u16();
1211 match status {
1212 200 => Ok(()),
1213 400 => Err(Error::InvalidRequest(resp.text()?)),
1214 _ => Err(Error::UnexpectedStatus {
1215 status,
1216 body: resp.text().unwrap_or_default(),
1217 }),
1218 }
1219 }
1220
1221 // ------------------------------------------------------------------
1222 // SRE control plane — load scenario registry
1223 // ------------------------------------------------------------------
1224
1225 /// Register (load) a load scenario in the registry without running it.
1226 ///
1227 /// Sends a `PUT /mockserver/loadScenario` with the given [`LoadScenario`].
1228 /// The scenario's [`name`](LoadScenario::name) is the unique registry key
1229 /// used later by [`start_load_scenarios`](Self::start_load_scenarios) /
1230 /// [`stop_load_scenarios`](Self::stop_load_scenarios) and the per-scenario
1231 /// fetch/delete endpoints.
1232 ///
1233 /// Registering is always allowed — even when load generation is disabled on
1234 /// the server — so this does not surface [`Error::FeatureDisabled`]. Returns
1235 /// the raw JSON the server echoes (`{"name":..,"state":"LOADED"}`).
1236 pub fn load_scenario(&self, scenario: &LoadScenario) -> Result<Value> {
1237 let resp = self
1238 .http
1239 .put(self.url("/mockserver/loadScenario"))
1240 .json(scenario)
1241 .send()?;
1242 self.load_scenario_json(resp)
1243 }
1244
1245 /// List every registered load scenario.
1246 ///
1247 /// Sends a `GET /mockserver/loadScenario` and returns the raw JSON
1248 /// (`{"scenarios":[{"name":..,"state":..,"definition":..,"status":..?}]}`).
1249 pub fn load_scenarios(&self) -> Result<Value> {
1250 let resp = self.http.get(self.url("/mockserver/loadScenario")).send()?;
1251 self.load_scenario_json(resp)
1252 }
1253
1254 /// Fetch a single registered load scenario by name.
1255 ///
1256 /// Sends a `GET /mockserver/loadScenario/{name}`. Returns
1257 /// [`Error::NotFound`] when no scenario with that name is registered.
1258 pub fn get_load_scenario(&self, name: impl AsRef<str>) -> Result<Value> {
1259 let resp = self
1260 .http
1261 .get(self.url(&format!("/mockserver/loadScenario/{}", name.as_ref())))
1262 .send()?;
1263 self.load_scenario_json(resp)
1264 }
1265
1266 /// Remove a single registered load scenario by name.
1267 ///
1268 /// Sends a `DELETE /mockserver/loadScenario/{name}`. Returns
1269 /// [`Error::NotFound`] when no scenario with that name is registered.
1270 pub fn delete_load_scenario(&self, name: impl AsRef<str>) -> Result<Value> {
1271 let resp = self
1272 .http
1273 .delete(self.url(&format!("/mockserver/loadScenario/{}", name.as_ref())))
1274 .send()?;
1275 self.load_scenario_json(resp)
1276 }
1277
1278 /// Clear all registered load scenarios.
1279 ///
1280 /// Sends a `DELETE /mockserver/loadScenario`. Idempotent.
1281 pub fn clear_load_scenarios(&self) -> Result<Value> {
1282 let resp = self
1283 .http
1284 .delete(self.url("/mockserver/loadScenario"))
1285 .send()?;
1286 self.load_scenario_json(resp)
1287 }
1288
1289 /// Start one or more registered load scenarios by name.
1290 ///
1291 /// Sends a `PUT /mockserver/loadScenario/start` with `{"names":[...]}`.
1292 /// Requires load generation to be enabled on the server — returns
1293 /// [`Error::FeatureDisabled`] on `403` (`loadGenerationEnabled=false`) — and
1294 /// [`Error::NotFound`] when a name is not registered. Honours each
1295 /// scenario's `startDelayMillis`. Returns the raw JSON
1296 /// (`{"started":[{"name":..,"state":..}],"status":..}`).
1297 pub fn start_load_scenarios<S: AsRef<str>>(&self, names: &[S]) -> Result<Value> {
1298 let names: Vec<&str> = names.iter().map(|n| n.as_ref()).collect();
1299 let body = serde_json::json!({ "names": names });
1300 let resp = self
1301 .http
1302 .put(self.url("/mockserver/loadScenario/start"))
1303 .json(&body)
1304 .send()?;
1305 self.load_scenario_json(resp)
1306 }
1307
1308 /// Stop running load scenarios.
1309 ///
1310 /// Sends a `PUT /mockserver/loadScenario/stop`. When `names` is non-empty the
1311 /// body is `{"names":[...]}`; when it is empty (or `&[]`) the body is empty,
1312 /// which the server treats as "stop all". Returns the raw JSON
1313 /// (`{"stopped":[..],"status":..}`).
1314 pub fn stop_load_scenarios<S: AsRef<str>>(&self, names: &[S]) -> Result<Value> {
1315 let mut req = self.http.put(self.url("/mockserver/loadScenario/stop"));
1316 if names.is_empty() {
1317 req = req.header("Content-Type", "application/json").body("");
1318 } else {
1319 let names: Vec<&str> = names.iter().map(|n| n.as_ref()).collect();
1320 req = req.json(&serde_json::json!({ "names": names }));
1321 }
1322 let resp = req.send()?;
1323 self.load_scenario_json(resp)
1324 }
1325
1326 /// Convenience: register `scenario` then immediately start it by name.
1327 ///
1328 /// Equivalent to [`load_scenario`](Self::load_scenario) followed by
1329 /// [`start_load_scenarios`](Self::start_load_scenarios) with the scenario's
1330 /// own name. Returns the JSON from the `start` call. Surfaces
1331 /// [`Error::FeatureDisabled`] from `start` when load generation is disabled.
1332 pub fn run_load_scenario(&self, scenario: &LoadScenario) -> Result<Value> {
1333 self.load_scenario(scenario)?;
1334 self.start_load_scenarios(&[scenario.name.as_str()])
1335 }
1336
1337 // ------------------------------------------------------------------
1338 // SRE control plane — service chaos
1339 // ------------------------------------------------------------------
1340
1341 /// Register a service-scoped HTTP chaos profile for a downstream host.
1342 ///
1343 /// Sends a `PUT /mockserver/serviceChaos`. `ttl_millis`, when supplied, sets
1344 /// an optional time-to-live after which the registration auto-reverts.
1345 /// Returns the raw JSON the server echoes.
1346 pub fn set_service_chaos(
1347 &self,
1348 host: impl Into<String>,
1349 profile: &HttpChaosProfile,
1350 ttl_millis: Option<u64>,
1351 ) -> Result<Value> {
1352 let mut body = serde_json::json!({
1353 "host": host.into(),
1354 "chaos": profile,
1355 });
1356 if let Some(ttl) = ttl_millis {
1357 body["ttlMillis"] = serde_json::json!(ttl);
1358 }
1359 let resp = self
1360 .http
1361 .put(self.url("/mockserver/serviceChaos"))
1362 .json(&body)
1363 .send()?;
1364 self.json_or_feature_error(resp)
1365 }
1366
1367 /// Remove a single host's service-scoped chaos profile.
1368 ///
1369 /// Sends a `PUT /mockserver/serviceChaos` with `{"host":..,"remove":true}`.
1370 pub fn remove_service_chaos(&self, host: impl Into<String>) -> Result<Value> {
1371 let body = serde_json::json!({ "host": host.into(), "remove": true });
1372 let resp = self
1373 .http
1374 .put(self.url("/mockserver/serviceChaos"))
1375 .json(&body)
1376 .send()?;
1377 self.json_or_feature_error(resp)
1378 }
1379
1380 /// Clear all service-scoped chaos.
1381 ///
1382 /// Sends a `PUT /mockserver/serviceChaos` with `{"clear":true}`.
1383 pub fn clear_service_chaos(&self) -> Result<Value> {
1384 let body = serde_json::json!({ "clear": true });
1385 let resp = self
1386 .http
1387 .put(self.url("/mockserver/serviceChaos"))
1388 .json(&body)
1389 .send()?;
1390 self.json_or_feature_error(resp)
1391 }
1392
1393 // ------------------------------------------------------------------
1394 // SRE control plane — SLO verdicts
1395 // ------------------------------------------------------------------
1396
1397 /// Verify a set of service-level objectives over a window.
1398 ///
1399 /// Sends a `PUT /mockserver/verifySLO`. The HTTP status encodes the verdict:
1400 /// `200` for PASS or INCONCLUSIVE, `406` for FAIL — both deserialize into a
1401 /// [`SloVerdict`] (inspect [`SloVerdict::result`]). A `400` (malformed
1402 /// criteria, or SLO tracking disabled) surfaces as [`Error::FeatureDisabled`].
1403 ///
1404 /// Returns `Ok(SloVerdict)` for both PASS/INCONCLUSIVE (200) and FAIL (406)
1405 /// so callers can branch on the verdict; transport/parse failures and `400`
1406 /// are returned as `Err`.
1407 pub fn verify_slo(&self, criteria: &SloCriteria) -> Result<SloVerdict> {
1408 let resp = self
1409 .http
1410 .put(self.url("/mockserver/verifySLO"))
1411 .json(criteria)
1412 .send()?;
1413
1414 let status = resp.status().as_u16();
1415 match status {
1416 // PASS / INCONCLUSIVE (200) and FAIL (406) both carry a SloVerdict.
1417 200 | 406 => {
1418 let text = resp.text()?;
1419 Ok(serde_json::from_str(&text)?)
1420 }
1421 400 => Err(Error::FeatureDisabled(resp.text()?)),
1422 403 => Err(Error::FeatureDisabled(resp.text()?)),
1423 _ => Err(Error::UnexpectedStatus {
1424 status,
1425 body: resp.text().unwrap_or_default(),
1426 }),
1427 }
1428 }
1429
1430 // ------------------------------------------------------------------
1431 // SRE control plane — preemption
1432 // ------------------------------------------------------------------
1433
1434 /// Cordon and drain the server (preemption simulation).
1435 ///
1436 /// Sends a `PUT /mockserver/preemption` with the given [`PreemptionRequest`]
1437 /// and returns the resulting [`PreemptionStatus`].
1438 pub fn set_preemption(&self, request: &PreemptionRequest) -> Result<PreemptionStatus> {
1439 let resp = self
1440 .http
1441 .put(self.url("/mockserver/preemption"))
1442 .json(request)
1443 .send()?;
1444
1445 let status = resp.status().as_u16();
1446 match status {
1447 200 => {
1448 let text = resp.text()?;
1449 Ok(serde_json::from_str(&text)?)
1450 }
1451 400 => Err(Error::InvalidRequest(resp.text()?)),
1452 403 => Err(Error::FeatureDisabled(resp.text()?)),
1453 _ => Err(Error::UnexpectedStatus {
1454 status,
1455 body: resp.text().unwrap_or_default(),
1456 }),
1457 }
1458 }
1459
1460 /// Retrieve the current preemption status.
1461 ///
1462 /// Sends a `GET /mockserver/preemption` and returns the [`PreemptionStatus`].
1463 pub fn preemption_status(&self) -> Result<PreemptionStatus> {
1464 let resp = self
1465 .http
1466 .get(self.url("/mockserver/preemption"))
1467 .send()?;
1468
1469 let status = resp.status().as_u16();
1470 match status {
1471 200 => {
1472 let text = resp.text()?;
1473 Ok(serde_json::from_str(&text)?)
1474 }
1475 403 => Err(Error::FeatureDisabled(resp.text()?)),
1476 _ => Err(Error::UnexpectedStatus {
1477 status,
1478 body: resp.text().unwrap_or_default(),
1479 }),
1480 }
1481 }
1482
1483 /// Uncordon the server (clear any active preemption simulation).
1484 ///
1485 /// Sends a `DELETE /mockserver/preemption`. Idempotent — succeeds whether or
1486 /// not a simulation was active. Returns the raw JSON status.
1487 pub fn clear_preemption(&self) -> Result<Value> {
1488 let resp = self
1489 .http
1490 .delete(self.url("/mockserver/preemption"))
1491 .send()?;
1492 self.json_or_feature_error(resp)
1493 }
1494
1495 // ------------------------------------------------------------------
1496 // SRE control plane — chaos experiments
1497 // ------------------------------------------------------------------
1498
1499 /// Start a scheduled multi-stage chaos experiment.
1500 ///
1501 /// Sends a `PUT /mockserver/chaosExperiment` with the given
1502 /// [`ChaosExperiment`]. Only one experiment may be active at a time; starting
1503 /// a new one stops the previous one. Returns the raw JSON status
1504 /// (`{"status":"started","name":..}`).
1505 pub fn start_chaos_experiment(&self, experiment: &ChaosExperiment) -> Result<Value> {
1506 let resp = self
1507 .http
1508 .put(self.url("/mockserver/chaosExperiment"))
1509 .json(experiment)
1510 .send()?;
1511 self.json_or_feature_error(resp)
1512 }
1513
1514 // ------------------------------------------------------------------
1515 // Internal helpers
1516 // ------------------------------------------------------------------
1517
1518 /// Common handler for SRE endpoints returning JSON on `200`, mapping `403`
1519 /// to [`Error::FeatureDisabled`] (the feature is disabled on the server),
1520 /// `400` to [`Error::InvalidRequest`], and any other status to
1521 /// [`Error::UnexpectedStatus`]. An empty `200` body deserializes to JSON
1522 /// `null`.
1523 fn json_or_feature_error(&self, resp: reqwest::blocking::Response) -> Result<Value> {
1524 let status = resp.status().as_u16();
1525 match status {
1526 200 | 201 => {
1527 let text = resp.text()?;
1528 if text.is_empty() {
1529 Ok(Value::Null)
1530 } else {
1531 Ok(serde_json::from_str(&text)?)
1532 }
1533 }
1534 400 => Err(Error::InvalidRequest(resp.text()?)),
1535 403 => Err(Error::FeatureDisabled(resp.text()?)),
1536 _ => Err(Error::UnexpectedStatus {
1537 status,
1538 body: resp.text().unwrap_or_default(),
1539 }),
1540 }
1541 }
1542
1543 /// Handler for load-scenario registry endpoints. Like
1544 /// [`json_or_feature_error`](Self::json_or_feature_error) but additionally
1545 /// maps `404` to [`Error::NotFound`] (an unknown scenario name).
1546 fn load_scenario_json(&self, resp: reqwest::blocking::Response) -> Result<Value> {
1547 let status = resp.status().as_u16();
1548 match status {
1549 200 | 201 => {
1550 let text = resp.text()?;
1551 if text.is_empty() {
1552 Ok(Value::Null)
1553 } else {
1554 Ok(serde_json::from_str(&text)?)
1555 }
1556 }
1557 400 => Err(Error::InvalidRequest(resp.text()?)),
1558 403 => Err(Error::FeatureDisabled(resp.text()?)),
1559 404 => Err(Error::NotFound(resp.text()?)),
1560 _ => Err(Error::UnexpectedStatus {
1561 status,
1562 body: resp.text().unwrap_or_default(),
1563 }),
1564 }
1565 }
1566
1567 fn do_verify(&self, verification: &Verification) -> Result<()> {
1568 let resp = self
1569 .http
1570 .put(self.url("/mockserver/verify"))
1571 .json(verification)
1572 .send()?;
1573
1574 let status = resp.status().as_u16();
1575 match status {
1576 200 | 202 => Ok(()),
1577 406 => Err(Error::VerificationFailure(resp.text()?)),
1578 400 => Err(Error::InvalidRequest(resp.text()?)),
1579 _ => Err(Error::UnexpectedStatus {
1580 status,
1581 body: resp.text().unwrap_or_default(),
1582 }),
1583 }
1584 }
1585
1586 fn do_verify_sequence(&self, verification: &VerificationSequence) -> Result<()> {
1587 let resp = self
1588 .http
1589 .put(self.url("/mockserver/verifySequence"))
1590 .json(verification)
1591 .send()?;
1592
1593 let status = resp.status().as_u16();
1594 match status {
1595 200 | 202 => Ok(()),
1596 406 => Err(Error::VerificationFailure(resp.text()?)),
1597 400 => Err(Error::InvalidRequest(resp.text()?)),
1598 _ => Err(Error::UnexpectedStatus {
1599 status,
1600 body: resp.text().unwrap_or_default(),
1601 }),
1602 }
1603 }
1604
1605 fn url(&self, path: &str) -> String {
1606 format!("{}{path}", self.base_url)
1607 }
1608
1609 fn do_retrieve(
1610 &self,
1611 request: Option<&HttpRequest>,
1612 retrieve_type: RetrieveType,
1613 format: RetrieveFormat,
1614 ) -> Result<String> {
1615 let url = format!(
1616 "{}?type={}&format={}",
1617 self.url("/mockserver/retrieve"),
1618 retrieve_type.as_str(),
1619 format.as_str(),
1620 );
1621
1622 let mut builder = self.http.put(&url);
1623 builder = builder.header("Content-Type", "application/json");
1624 if let Some(req) = request {
1625 builder = builder.json(req);
1626 } else {
1627 builder = builder.body("");
1628 }
1629
1630 let resp = builder.send()?;
1631 let status = resp.status().as_u16();
1632 match status {
1633 200 => Ok(resp.text()?),
1634 400 => Err(Error::InvalidRequest(resp.text()?)),
1635 _ => Err(Error::UnexpectedStatus {
1636 status,
1637 body: resp.text().unwrap_or_default(),
1638 }),
1639 }
1640 }
1641}
1642
1643// ---------------------------------------------------------------------------
1644// ForwardChainExpectation (fluent builder)
1645// ---------------------------------------------------------------------------
1646
1647/// Fluent builder for creating an expectation via `client.when(...).respond(...)`.
1648pub struct ForwardChainExpectation<'a> {
1649 client: &'a MockServerClient,
1650 request: HttpRequest,
1651 times: Option<Times>,
1652 time_to_live: Option<TimeToLive>,
1653 priority: Option<i32>,
1654 id: Option<String>,
1655}
1656
1657impl<'a> ForwardChainExpectation<'a> {
1658 /// Set how many times this expectation should match.
1659 pub fn times(mut self, times: Times) -> Self {
1660 self.times = Some(times);
1661 self
1662 }
1663
1664 /// Set the time-to-live for this expectation.
1665 pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
1666 self.time_to_live = Some(ttl);
1667 self
1668 }
1669
1670 /// Set the priority for this expectation.
1671 pub fn priority(mut self, priority: i32) -> Self {
1672 self.priority = Some(priority);
1673 self
1674 }
1675
1676 /// Set the expectation ID (for upsert semantics).
1677 pub fn with_id(mut self, id: impl Into<String>) -> Self {
1678 self.id = Some(id.into());
1679 self
1680 }
1681
1682 /// Complete the expectation with a response action.
1683 pub fn respond(self, response: HttpResponse) -> Result<Vec<Expectation>> {
1684 let (client, expectation) = self.into_parts();
1685 let expectation = expectation.respond(response);
1686 client.upsert(&[expectation])
1687 }
1688
1689 /// Complete the expectation with a forward action.
1690 pub fn forward(self, forward: HttpForward) -> Result<Vec<Expectation>> {
1691 let (client, expectation) = self.into_parts();
1692 let expectation = expectation.forward(forward);
1693 client.upsert(&[expectation])
1694 }
1695
1696 /// Complete the expectation with an error action.
1697 pub fn error(self, error: HttpError) -> Result<Vec<Expectation>> {
1698 let (client, expectation) = self.into_parts();
1699 let expectation = expectation.error(error);
1700 client.upsert(&[expectation])
1701 }
1702
1703 /// Complete the expectation with a Server-Sent Events (SSE) response action.
1704 pub fn respond_sse(self, sse: HttpSseResponse) -> Result<Vec<Expectation>> {
1705 let (client, expectation) = self.into_parts();
1706 let expectation = expectation.respond_sse(sse);
1707 client.upsert(&[expectation])
1708 }
1709
1710 /// Complete the expectation with a WebSocket response action.
1711 pub fn respond_web_socket(self, ws: HttpWebSocketResponse) -> Result<Vec<Expectation>> {
1712 let (client, expectation) = self.into_parts();
1713 let expectation = expectation.respond_web_socket(ws);
1714 client.upsert(&[expectation])
1715 }
1716
1717 /// Complete the expectation with a DNS response action.
1718 pub fn respond_dns(self, dns: DnsResponse) -> Result<Vec<Expectation>> {
1719 let (client, expectation) = self.into_parts();
1720 let expectation = expectation.respond_dns(dns);
1721 client.upsert(&[expectation])
1722 }
1723
1724 /// Complete the expectation with a raw binary response action.
1725 pub fn respond_binary(self, binary: BinaryResponse) -> Result<Vec<Expectation>> {
1726 let (client, expectation) = self.into_parts();
1727 let expectation = expectation.respond_binary(binary);
1728 client.upsert(&[expectation])
1729 }
1730
1731 /// Complete the expectation with a gRPC streaming response action.
1732 pub fn respond_grpc_stream(self, grpc: GrpcStreamResponse) -> Result<Vec<Expectation>> {
1733 let (client, expectation) = self.into_parts();
1734 let expectation = expectation.respond_grpc_stream(grpc);
1735 client.upsert(&[expectation])
1736 }
1737
1738 // ------------------------------------------------------------------
1739 // `respond_with_*` aliases (cross-client naming parity)
1740 //
1741 // The Python, PHP, and .NET clients expose these advanced response
1742 // builders under `respond_with_*` names. These aliases give the Rust
1743 // fluent chain the same surface so examples translate verbatim across
1744 // clients; each simply delegates to its idiomatic Rust counterpart.
1745 // ------------------------------------------------------------------
1746
1747 /// Alias of [`respond_sse`](Self::respond_sse) for cross-client naming parity.
1748 pub fn respond_with_sse(self, sse: HttpSseResponse) -> Result<Vec<Expectation>> {
1749 self.respond_sse(sse)
1750 }
1751
1752 /// Alias of [`respond_web_socket`](Self::respond_web_socket) for cross-client naming parity.
1753 pub fn respond_with_web_socket(self, ws: HttpWebSocketResponse) -> Result<Vec<Expectation>> {
1754 self.respond_web_socket(ws)
1755 }
1756
1757 /// Alias of [`respond_dns`](Self::respond_dns) for cross-client naming parity.
1758 pub fn respond_with_dns(self, dns: DnsResponse) -> Result<Vec<Expectation>> {
1759 self.respond_dns(dns)
1760 }
1761
1762 /// Alias of [`respond_binary`](Self::respond_binary) for cross-client naming parity.
1763 pub fn respond_with_binary(self, binary: BinaryResponse) -> Result<Vec<Expectation>> {
1764 self.respond_binary(binary)
1765 }
1766
1767 /// Alias of [`respond_grpc_stream`](Self::respond_grpc_stream) for cross-client naming parity.
1768 pub fn respond_with_grpc_stream(self, grpc: GrpcStreamResponse) -> Result<Vec<Expectation>> {
1769 self.respond_grpc_stream(grpc)
1770 }
1771
1772 fn into_parts(self) -> (&'a MockServerClient, Expectation) {
1773 let ForwardChainExpectation {
1774 client,
1775 request,
1776 times,
1777 time_to_live,
1778 priority,
1779 id,
1780 } = self;
1781 let mut exp = Expectation::new(request);
1782 exp.times = times;
1783 exp.time_to_live = time_to_live;
1784 exp.priority = priority;
1785 exp.id = id;
1786 (client, exp)
1787 }
1788}
1789
1790// ---------------------------------------------------------------------------
1791// Scenario (control-plane handle)
1792// ---------------------------------------------------------------------------
1793
1794/// A handle for inspecting and driving a named scenario state-machine.
1795///
1796/// Obtained via [`MockServerClient::scenario`]. Each method issues a single
1797/// control-plane request against `/mockserver/scenario/{name}`.
1798pub struct Scenario<'a> {
1799 client: &'a MockServerClient,
1800 name: String,
1801}
1802
1803impl Scenario<'_> {
1804 /// Get the scenario's current state.
1805 ///
1806 /// Sends `GET /mockserver/scenario/{name}`.
1807 pub fn state(&self) -> Result<String> {
1808 self.client.scenario_state(&self.name)
1809 }
1810
1811 /// Set the scenario's state.
1812 ///
1813 /// Sends `PUT /mockserver/scenario/{name}` with `{"state": state}`.
1814 pub fn set(&self, state: &str) -> Result<()> {
1815 self.client.scenario_set(&self.name, state, None, None)
1816 }
1817
1818 /// Set the scenario's state and schedule an automatic transition to
1819 /// `next_state` after `transition_after_ms` milliseconds.
1820 ///
1821 /// Sends `PUT /mockserver/scenario/{name}` with
1822 /// `{"state", "transitionAfterMs", "nextState"}`.
1823 pub fn set_timed(
1824 &self,
1825 state: &str,
1826 transition_after_ms: u64,
1827 next_state: &str,
1828 ) -> Result<()> {
1829 self.client.scenario_set(
1830 &self.name,
1831 state,
1832 Some(transition_after_ms),
1833 Some(next_state),
1834 )
1835 }
1836
1837 /// Externally trigger a transition to `new_state`.
1838 ///
1839 /// Sends `PUT /mockserver/scenario/{name}/trigger` with `{"newState": new_state}`.
1840 pub fn trigger(&self, new_state: &str) -> Result<()> {
1841 self.client.scenario_trigger(&self.name, new_state)
1842 }
1843}