Skip to main content

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(&reg)
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}