Skip to main content

mockserver_client/
client.rs

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