Skip to main content

mockserver_client/
client.rs

1//! The MockServer client and its builder.
2
3use reqwest::blocking::Client;
4use serde_json::Value;
5
6use crate::breakpoint::{
7    BreakpointMatcherList, BreakpointMatcherRegistration, BreakpointMatcherResponse,
8    BreakpointRequestHandler, BreakpointResponseHandler, BreakpointStreamFrameHandler,
9    BreakpointWebSocketClient,
10};
11use crate::error::{Error, Result};
12use crate::model::*;
13
14// ---------------------------------------------------------------------------
15// ClientBuilder
16// ---------------------------------------------------------------------------
17
18/// Builder for constructing a [`MockServerClient`].
19///
20/// # Example
21/// ```no_run
22/// use mockserver_client::ClientBuilder;
23///
24/// let client = ClientBuilder::new("localhost", 1080)
25///     .context_path("/api")
26///     .secure(true)
27///     .build()
28///     .unwrap();
29/// ```
30pub struct ClientBuilder {
31    host: String,
32    port: u16,
33    context_path: String,
34    secure: bool,
35    tls_verify: bool,
36}
37
38impl ClientBuilder {
39    /// Create a new builder targeting the given host and port.
40    pub fn new(host: impl Into<String>, port: u16) -> Self {
41        Self {
42            host: host.into(),
43            port,
44            context_path: String::new(),
45            secure: false,
46            tls_verify: true,
47        }
48    }
49
50    /// Set a context path prefix (e.g., "/mockserver" if deployed behind a reverse proxy).
51    pub fn context_path(mut self, path: impl Into<String>) -> Self {
52        self.context_path = path.into();
53        self
54    }
55
56    /// Use HTTPS instead of HTTP.
57    pub fn secure(mut self, secure: bool) -> Self {
58        self.secure = secure;
59        self
60    }
61
62    /// Whether to verify TLS certificates (default: true).
63    pub fn tls_verify(mut self, verify: bool) -> Self {
64        self.tls_verify = verify;
65        self
66    }
67
68    /// Build the client.
69    pub fn build(self) -> Result<MockServerClient> {
70        let scheme = if self.secure { "https" } else { "http" };
71        let ctx = if self.context_path.is_empty() {
72            String::new()
73        } else if self.context_path.starts_with('/') {
74            self.context_path
75        } else {
76            format!("/{}", self.context_path)
77        };
78        let base_url = format!("{scheme}://{}:{}{ctx}", self.host, self.port);
79
80        let http_client = Client::builder()
81            .danger_accept_invalid_certs(!self.tls_verify)
82            .build()?;
83
84        Ok(MockServerClient {
85            base_url,
86            http: http_client,
87            breakpoint_ws: std::sync::Mutex::new(None),
88        })
89    }
90}
91
92// ---------------------------------------------------------------------------
93// MockServerClient
94// ---------------------------------------------------------------------------
95
96/// A blocking client for the MockServer control-plane REST API.
97///
98/// Created via [`ClientBuilder`]. All methods are synchronous.
99pub struct MockServerClient {
100    pub(crate) base_url: String,
101    http: Client,
102    breakpoint_ws: std::sync::Mutex<Option<BreakpointWebSocketClient>>,
103}
104
105impl MockServerClient {
106    // ------------------------------------------------------------------
107    // Expectation creation
108    // ------------------------------------------------------------------
109
110    /// Create one or more expectations on the server.
111    ///
112    /// Returns the created expectations as echoed by the server.
113    pub fn upsert(&self, expectations: &[Expectation]) -> Result<Vec<Expectation>> {
114        let body = serde_json::to_value(expectations)?;
115        let resp = self
116            .http
117            .put(self.url("/mockserver/expectation"))
118            .json(&body)
119            .send()?;
120
121        let status = resp.status().as_u16();
122        match status {
123            200 | 201 => {
124                let text = resp.text()?;
125                if text.is_empty() {
126                    Ok(expectations.to_vec())
127                } else {
128                    Ok(serde_json::from_str(&text)?)
129                }
130            }
131            400 => Err(Error::InvalidRequest(resp.text()?)),
132            _ => Err(Error::UnexpectedStatus {
133                status,
134                body: resp.text().unwrap_or_default(),
135            }),
136        }
137    }
138
139    // ------------------------------------------------------------------
140    // Fluent API entry point
141    // ------------------------------------------------------------------
142
143    /// Begin building an expectation with the fluent `when(...).respond(...)` API.
144    ///
145    /// # Example
146    /// ```no_run
147    /// use mockserver_client::{ClientBuilder, HttpRequest, HttpResponse};
148    ///
149    /// let client = ClientBuilder::new("localhost", 1080).build().unwrap();
150    /// client.when(HttpRequest::new().method("GET").path("/foo"))
151    ///     .respond(HttpResponse::new().status_code(200).body("bar"))
152    ///     .unwrap();
153    /// ```
154    pub fn when(&self, request: HttpRequest) -> ForwardChainExpectation<'_> {
155        ForwardChainExpectation {
156            client: self,
157            request,
158            times: None,
159            time_to_live: None,
160            priority: None,
161            id: None,
162        }
163    }
164
165    // ------------------------------------------------------------------
166    // Verify
167    // ------------------------------------------------------------------
168
169    /// Verify that a request was received the specified number of times.
170    ///
171    /// Returns `Ok(())` if verification passes, or
172    /// `Err(Error::VerificationFailure)` with the server's failure message.
173    pub fn verify(&self, request: HttpRequest, times: VerificationTimes) -> Result<()> {
174        let verification = Verification {
175            http_request: request,
176            times: Some(times),
177        };
178        let resp = self
179            .http
180            .put(self.url("/mockserver/verify"))
181            .json(&verification)
182            .send()?;
183
184        let status = resp.status().as_u16();
185        match status {
186            200 | 202 => Ok(()),
187            406 => Err(Error::VerificationFailure(resp.text()?)),
188            400 => Err(Error::InvalidRequest(resp.text()?)),
189            _ => Err(Error::UnexpectedStatus {
190                status,
191                body: resp.text().unwrap_or_default(),
192            }),
193        }
194    }
195
196    /// Verify that requests were received in the given order.
197    pub fn verify_sequence(&self, requests: Vec<HttpRequest>) -> Result<()> {
198        let verification = VerificationSequence {
199            http_requests: requests,
200        };
201        let resp = self
202            .http
203            .put(self.url("/mockserver/verifySequence"))
204            .json(&verification)
205            .send()?;
206
207        let status = resp.status().as_u16();
208        match status {
209            200 | 202 => Ok(()),
210            406 => Err(Error::VerificationFailure(resp.text()?)),
211            400 => Err(Error::InvalidRequest(resp.text()?)),
212            _ => Err(Error::UnexpectedStatus {
213                status,
214                body: resp.text().unwrap_or_default(),
215            }),
216        }
217    }
218
219    // ------------------------------------------------------------------
220    // Clear / Reset
221    // ------------------------------------------------------------------
222
223    /// Clear expectations and/or logs matching the given request.
224    ///
225    /// If `request` is `None`, clears everything of the specified type.
226    pub fn clear(
227        &self,
228        request: Option<&HttpRequest>,
229        clear_type: Option<ClearType>,
230    ) -> Result<()> {
231        let mut url = self.url("/mockserver/clear");
232        if let Some(ct) = clear_type {
233            url = format!("{url}?type={}", ct.as_str());
234        }
235
236        let mut builder = self.http.put(&url);
237        builder = builder.header("Content-Type", "application/json");
238        if let Some(req) = request {
239            builder = builder.json(req);
240        } else {
241            builder = builder.body("");
242        }
243
244        let resp = builder.send()?;
245        let status = resp.status().as_u16();
246        match status {
247            200 => Ok(()),
248            400 => Err(Error::InvalidRequest(resp.text()?)),
249            _ => Err(Error::UnexpectedStatus {
250                status,
251                body: resp.text().unwrap_or_default(),
252            }),
253        }
254    }
255
256    /// Clear expectations by expectation ID.
257    pub fn clear_by_id(
258        &self,
259        expectation_id: impl Into<String>,
260        clear_type: Option<ClearType>,
261    ) -> Result<()> {
262        let mut url = self.url("/mockserver/clear");
263        if let Some(ct) = clear_type {
264            url = format!("{url}?type={}", ct.as_str());
265        }
266
267        let body = serde_json::json!({ "id": expectation_id.into() });
268        let resp = self.http.put(&url).json(&body).send()?;
269
270        let status = resp.status().as_u16();
271        match status {
272            200 => Ok(()),
273            400 => Err(Error::InvalidRequest(resp.text()?)),
274            _ => Err(Error::UnexpectedStatus {
275                status,
276                body: resp.text().unwrap_or_default(),
277            }),
278        }
279    }
280
281    /// Reset all expectations and recorded requests.
282    pub fn reset(&self) -> Result<()> {
283        let resp = self
284            .http
285            .put(self.url("/mockserver/reset"))
286            .header("Content-Type", "application/json")
287            .body("")
288            .send()?;
289
290        let status = resp.status().as_u16();
291        match status {
292            200 => Ok(()),
293            _ => Err(Error::UnexpectedStatus {
294                status,
295                body: resp.text().unwrap_or_default(),
296            }),
297        }
298    }
299
300    // ------------------------------------------------------------------
301    // Retrieve
302    // ------------------------------------------------------------------
303
304    /// Retrieve recorded requests matching the optional filter.
305    pub fn retrieve_recorded_requests(
306        &self,
307        request: Option<&HttpRequest>,
308    ) -> Result<Vec<HttpRequest>> {
309        let text =
310            self.do_retrieve(request, RetrieveType::Requests, RetrieveFormat::Json)?;
311        if text.is_empty() {
312            return Ok(vec![]);
313        }
314        Ok(serde_json::from_str(&text)?)
315    }
316
317    /// Retrieve active expectations matching the optional filter.
318    pub fn retrieve_active_expectations(
319        &self,
320        request: Option<&HttpRequest>,
321    ) -> Result<Vec<Expectation>> {
322        let text = self.do_retrieve(
323            request,
324            RetrieveType::ActiveExpectations,
325            RetrieveFormat::Json,
326        )?;
327        if text.is_empty() {
328            return Ok(vec![]);
329        }
330        Ok(serde_json::from_str(&text)?)
331    }
332
333    /// Retrieve recorded expectations matching the optional filter.
334    pub fn retrieve_recorded_expectations(
335        &self,
336        request: Option<&HttpRequest>,
337    ) -> Result<Vec<Expectation>> {
338        let text = self.do_retrieve(
339            request,
340            RetrieveType::RecordedExpectations,
341            RetrieveFormat::Json,
342        )?;
343        if text.is_empty() {
344            return Ok(vec![]);
345        }
346        Ok(serde_json::from_str(&text)?)
347    }
348
349    /// Retrieve log messages matching the optional filter.
350    pub fn retrieve_log_messages(
351        &self,
352        request: Option<&HttpRequest>,
353    ) -> Result<Vec<String>> {
354        let text =
355            self.do_retrieve(request, RetrieveType::Logs, RetrieveFormat::LogEntries)?;
356        if text.is_empty() {
357            return Ok(vec![]);
358        }
359        // Log messages may be returned as a JSON array of strings or as a
360        // separator-delimited block. Try JSON first.
361        if let Ok(arr) = serde_json::from_str::<Vec<String>>(&text) {
362            return Ok(arr);
363        }
364        // Fall back to splitting on the separator used by MockServer.
365        Ok(text
366            .split("------------------------------------\n")
367            .map(|s| s.to_string())
368            .filter(|s| !s.is_empty())
369            .collect())
370    }
371
372    /// Retrieve recorded request/response pairs.
373    pub fn retrieve_request_responses(
374        &self,
375        request: Option<&HttpRequest>,
376    ) -> Result<Vec<Value>> {
377        let text = self.do_retrieve(
378            request,
379            RetrieveType::RequestResponses,
380            RetrieveFormat::Json,
381        )?;
382        if text.is_empty() {
383            return Ok(vec![]);
384        }
385        Ok(serde_json::from_str(&text)?)
386    }
387
388    // ------------------------------------------------------------------
389    // Status / Bind
390    // ------------------------------------------------------------------
391
392    /// Query the server's listening ports.
393    pub fn status(&self) -> Result<Ports> {
394        let resp = self
395            .http
396            .put(self.url("/mockserver/status"))
397            .header("Content-Type", "application/json")
398            .body("")
399            .send()?;
400
401        let status = resp.status().as_u16();
402        match status {
403            200 => {
404                let text = resp.text()?;
405                Ok(serde_json::from_str(&text)?)
406            }
407            _ => Err(Error::UnexpectedStatus {
408                status,
409                body: resp.text().unwrap_or_default(),
410            }),
411        }
412    }
413
414    /// Bind additional listening ports.
415    pub fn bind(&self, ports: &[u16]) -> Result<Ports> {
416        let body = Ports {
417            ports: ports.to_vec(),
418        };
419        let resp = self
420            .http
421            .put(self.url("/mockserver/bind"))
422            .json(&body)
423            .send()?;
424
425        let status = resp.status().as_u16();
426        match status {
427            200 => {
428                let text = resp.text()?;
429                Ok(serde_json::from_str(&text)?)
430            }
431            400 => Err(Error::InvalidRequest(resp.text()?)),
432            406 => Err(Error::VerificationFailure(resp.text()?)),
433            _ => Err(Error::UnexpectedStatus {
434                status,
435                body: resp.text().unwrap_or_default(),
436            }),
437        }
438    }
439
440    /// Check if the MockServer has started (polls with retries).
441    pub fn has_started(&self, attempts: u32, timeout_ms: u64) -> bool {
442        for i in 0..attempts {
443            match self.status() {
444                Ok(_) => return true,
445                Err(_) => {
446                    if i < attempts - 1 {
447                        std::thread::sleep(std::time::Duration::from_millis(timeout_ms));
448                    }
449                }
450            }
451        }
452        false
453    }
454
455    // ------------------------------------------------------------------
456    // Breakpoints
457    // ------------------------------------------------------------------
458
459    /// Ensure the breakpoint WebSocket client is connected and return the clientId.
460    /// If the existing connection's read loop has exited, it is replaced transparently.
461    fn ensure_breakpoint_ws(&self) -> Result<String> {
462        let mut guard = self.breakpoint_ws.lock().unwrap();
463        let needs_connect = match guard.as_ref() {
464            None => true,
465            Some(ws) => ws.is_dead(),
466        };
467        if needs_connect {
468            // Close the old dead connection if present
469            if let Some(old) = guard.take() {
470                old.close();
471            }
472            let ws = BreakpointWebSocketClient::connect(&self.base_url)?;
473            *guard = Some(ws);
474        }
475        Ok(guard.as_ref().unwrap().client_id.clone())
476    }
477
478    /// Register a breakpoint matcher with the given phases and handlers.
479    /// Returns the server-assigned breakpoint id.
480    pub fn add_breakpoint(
481        &self,
482        matcher: HttpRequest,
483        phases: &[&str],
484        request_handler: Option<BreakpointRequestHandler>,
485        response_handler: Option<BreakpointResponseHandler>,
486        stream_frame_handler: Option<BreakpointStreamFrameHandler>,
487    ) -> Result<String> {
488        if phases.is_empty() {
489            return Err(Error::InvalidRequest(
490                "At least one phase is required".into(),
491            ));
492        }
493
494        let client_id = self.ensure_breakpoint_ws()?;
495
496        let reg = BreakpointMatcherRegistration {
497            http_request: matcher,
498            phases: phases.iter().map(|s| s.to_string()).collect(),
499            client_id: Some(client_id),
500        };
501
502        let resp = self
503            .http
504            .put(self.url("/mockserver/breakpoint/matcher"))
505            .json(&reg)
506            .send()?;
507
508        let status = resp.status().as_u16();
509        let text = resp.text()?;
510        if status >= 400 {
511            return Err(Error::UnexpectedStatus {
512                status,
513                body: text,
514            });
515        }
516
517        let result: BreakpointMatcherResponse = serde_json::from_str(&text)?;
518        let id = result.id.clone();
519
520        // Register handlers
521        let guard = self.breakpoint_ws.lock().unwrap();
522        if let Some(ws) = guard.as_ref() {
523            if let Some(h) = request_handler {
524                ws.set_request_handler(&id, h);
525            }
526            if let Some(h) = response_handler {
527                ws.set_response_handler(&id, h);
528            }
529            if let Some(h) = stream_frame_handler {
530                ws.set_stream_frame_handler(&id, h);
531            }
532        }
533
534        Ok(id)
535    }
536
537    /// Convenience: register a REQUEST-only breakpoint.
538    pub fn add_request_breakpoint(
539        &self,
540        matcher: HttpRequest,
541        handler: BreakpointRequestHandler,
542    ) -> Result<String> {
543        self.add_breakpoint(
544            matcher,
545            &[crate::breakpoint::phase::REQUEST],
546            Some(handler),
547            None,
548            None,
549        )
550    }
551
552    /// Convenience: register a REQUEST + RESPONSE breakpoint.
553    pub fn add_request_response_breakpoint(
554        &self,
555        matcher: HttpRequest,
556        request_handler: BreakpointRequestHandler,
557        response_handler: BreakpointResponseHandler,
558    ) -> Result<String> {
559        self.add_breakpoint(
560            matcher,
561            &[
562                crate::breakpoint::phase::REQUEST,
563                crate::breakpoint::phase::RESPONSE,
564            ],
565            Some(request_handler),
566            Some(response_handler),
567            None,
568        )
569    }
570
571    /// Convenience: register a streaming-phase breakpoint.
572    pub fn add_stream_breakpoint(
573        &self,
574        matcher: HttpRequest,
575        phases: &[&str],
576        handler: BreakpointStreamFrameHandler,
577    ) -> Result<String> {
578        self.add_breakpoint(matcher, phases, None, None, Some(handler))
579    }
580
581    /// List all registered breakpoint matchers.
582    pub fn list_breakpoint_matchers(&self) -> Result<BreakpointMatcherList> {
583        let resp = self
584            .http
585            .get(self.url("/mockserver/breakpoint/matchers"))
586            .send()?;
587
588        let status = resp.status().as_u16();
589        let text = resp.text()?;
590        if status >= 400 {
591            return Err(Error::UnexpectedStatus {
592                status,
593                body: text,
594            });
595        }
596
597        Ok(serde_json::from_str(&text)?)
598    }
599
600    /// Remove a breakpoint matcher by id.
601    pub fn remove_breakpoint_matcher(&self, id: impl Into<String>) -> Result<()> {
602        let id = id.into();
603        let body = serde_json::json!({ "id": &id });
604        let resp = self
605            .http
606            .put(self.url("/mockserver/breakpoint/matcher/remove"))
607            .json(&body)
608            .send()?;
609
610        let status = resp.status().as_u16();
611        match status {
612            200 => {
613                let guard = self.breakpoint_ws.lock().unwrap();
614                if let Some(ws) = guard.as_ref() {
615                    ws.remove_handlers(&id);
616                }
617                Ok(())
618            }
619            404 => Err(Error::InvalidRequest(format!(
620                "Breakpoint matcher not found: {id}"
621            ))),
622            _ => Err(Error::UnexpectedStatus {
623                status,
624                body: resp.text().unwrap_or_default(),
625            }),
626        }
627    }
628
629    /// Remove all registered breakpoint matchers.
630    pub fn clear_breakpoint_matchers(&self) -> Result<()> {
631        let resp = self
632            .http
633            .put(self.url("/mockserver/breakpoint/matcher/clear"))
634            .header("Content-Type", "application/json")
635            .body("")
636            .send()?;
637
638        let status = resp.status().as_u16();
639        if status >= 400 {
640            return Err(Error::UnexpectedStatus {
641                status,
642                body: resp.text().unwrap_or_default(),
643            });
644        }
645
646        let guard = self.breakpoint_ws.lock().unwrap();
647        if let Some(ws) = guard.as_ref() {
648            ws.clear_handlers();
649        }
650        Ok(())
651    }
652
653    /// Close the breakpoint callback WebSocket connection.
654    pub fn close_breakpoint_websocket(&self) {
655        let mut guard = self.breakpoint_ws.lock().unwrap();
656        if let Some(ws) = guard.take() {
657            ws.close();
658        }
659    }
660
661    // ------------------------------------------------------------------
662    // Internal helpers
663    // ------------------------------------------------------------------
664
665    fn url(&self, path: &str) -> String {
666        format!("{}{path}", self.base_url)
667    }
668
669    fn do_retrieve(
670        &self,
671        request: Option<&HttpRequest>,
672        retrieve_type: RetrieveType,
673        format: RetrieveFormat,
674    ) -> Result<String> {
675        let url = format!(
676            "{}?type={}&format={}",
677            self.url("/mockserver/retrieve"),
678            retrieve_type.as_str(),
679            format.as_str(),
680        );
681
682        let mut builder = self.http.put(&url);
683        builder = builder.header("Content-Type", "application/json");
684        if let Some(req) = request {
685            builder = builder.json(req);
686        } else {
687            builder = builder.body("");
688        }
689
690        let resp = builder.send()?;
691        let status = resp.status().as_u16();
692        match status {
693            200 => Ok(resp.text()?),
694            400 => Err(Error::InvalidRequest(resp.text()?)),
695            _ => Err(Error::UnexpectedStatus {
696                status,
697                body: resp.text().unwrap_or_default(),
698            }),
699        }
700    }
701}
702
703// ---------------------------------------------------------------------------
704// ForwardChainExpectation (fluent builder)
705// ---------------------------------------------------------------------------
706
707/// Fluent builder for creating an expectation via `client.when(...).respond(...)`.
708pub struct ForwardChainExpectation<'a> {
709    client: &'a MockServerClient,
710    request: HttpRequest,
711    times: Option<Times>,
712    time_to_live: Option<TimeToLive>,
713    priority: Option<i32>,
714    id: Option<String>,
715}
716
717impl<'a> ForwardChainExpectation<'a> {
718    /// Set how many times this expectation should match.
719    pub fn times(mut self, times: Times) -> Self {
720        self.times = Some(times);
721        self
722    }
723
724    /// Set the time-to-live for this expectation.
725    pub fn time_to_live(mut self, ttl: TimeToLive) -> Self {
726        self.time_to_live = Some(ttl);
727        self
728    }
729
730    /// Set the priority for this expectation.
731    pub fn priority(mut self, priority: i32) -> Self {
732        self.priority = Some(priority);
733        self
734    }
735
736    /// Set the expectation ID (for upsert semantics).
737    pub fn with_id(mut self, id: impl Into<String>) -> Self {
738        self.id = Some(id.into());
739        self
740    }
741
742    /// Complete the expectation with a response action.
743    pub fn respond(self, response: HttpResponse) -> Result<Vec<Expectation>> {
744        let (client, expectation) = self.into_parts();
745        let expectation = expectation.respond(response);
746        client.upsert(&[expectation])
747    }
748
749    /// Complete the expectation with a forward action.
750    pub fn forward(self, forward: HttpForward) -> Result<Vec<Expectation>> {
751        let (client, expectation) = self.into_parts();
752        let expectation = expectation.forward(forward);
753        client.upsert(&[expectation])
754    }
755
756    /// Complete the expectation with an error action.
757    pub fn error(self, error: HttpError) -> Result<Vec<Expectation>> {
758        let (client, expectation) = self.into_parts();
759        let expectation = expectation.error(error);
760        client.upsert(&[expectation])
761    }
762
763    fn into_parts(self) -> (&'a MockServerClient, Expectation) {
764        let ForwardChainExpectation {
765            client,
766            request,
767            times,
768            time_to_live,
769            priority,
770            id,
771        } = self;
772        let mut exp = Expectation::new(request);
773        exp.times = times;
774        exp.time_to_live = time_to_live;
775        exp.priority = priority;
776        exp.id = id;
777        (client, exp)
778    }
779}