httpmock/api/
proxy.rs

1use crate::{
2    api::server::MockServer,
3    common::{
4        data::RecordingRuleConfig,
5        data::RequestRequirements,
6        util::{write_file, Join},
7    },
8    When,
9};
10use std::{
11    cell::Cell,
12    path::{Path, PathBuf},
13    rc::Rc,
14};
15
16/// Represents a forwarding rule on a [MockServer](struct.MockServer.html), allowing HTTP requests
17/// that meet specific criteria to be redirected to a designated destination. Each rule is
18/// uniquely identified by an ID within the server context.
19pub struct ForwardingRule<'a> {
20    pub id: usize,
21    pub(crate) server: &'a MockServer,
22}
23
24impl<'a> ForwardingRule<'a> {
25    pub fn new(id: usize, server: &'a MockServer) -> Self {
26        Self { id, server }
27    }
28
29    /// Synchronously deletes the forwarding rule from the mock server.
30    /// This method blocks the current thread until the deletion has been completed, ensuring that the rule is no longer active and will not affect any further requests.
31    ///
32    /// # Panics
33    /// Panics if the deletion fails, typically due to issues such as the rule not existing or server connectivity problems.
34    pub fn delete(&mut self) {
35        self.delete_async().join();
36    }
37
38    /// Asynchronously deletes the forwarding rule from the mock server.
39    /// This method performs the deletion without blocking the current thread,
40    /// making it suitable for use in asynchronous applications where maintaining responsiveness
41    /// or concurrent execution is necessary.
42    ///
43    /// # Panics
44    /// Panics if the deletion fails, typically due to issues such as the rule not existing or server connectivity problems.
45    /// This method will raise an immediate panic on such failures, signaling that the operation could not be completed as expected.
46    pub async fn delete_async(&self) {
47        self.server
48            .server_adapter
49            .as_ref()
50            .unwrap()
51            .delete_forwarding_rule(self.id)
52            .await
53            .expect("could not delete mock from server");
54    }
55}
56/// Provides methods for managing a proxy rule from the server.
57pub struct ProxyRule<'a> {
58    pub id: usize,
59    pub(crate) server: &'a MockServer,
60}
61
62impl<'a> ProxyRule<'a> {
63    pub fn new(id: usize, server: &'a MockServer) -> Self {
64        Self { id, server }
65    }
66
67    /// Synchronously deletes the proxy rule from the server.
68    /// This method blocks the current thread until the deletion is complete, ensuring that
69    /// the rule is removed and will no longer redirect any requests.
70    ///
71    /// # Usage
72    /// This method is typically used in synchronous environments where immediate removal of the
73    /// rule is necessary and can afford a blocking operation.
74    ///
75    /// # Panics
76    /// Panics if the deletion fails due to server-related issues such as connectivity problems,
77    /// or if the rule does not exist on the server.
78    pub fn delete(&mut self) {
79        self.delete_async().join();
80    }
81
82    /// Asynchronously deletes the proxy rule from the server.
83    /// This method allows for non-blocking operations, suitable for asynchronous environments
84    /// where tasks are performed concurrently without interrupting the main workflow.
85    ///
86    /// # Usage
87    /// Ideal for use in modern async/await patterns in Rust, providing a way to handle resource
88    /// cleanup without stalling other operations.
89    ///
90    /// # Panics
91    /// Panics if the deletion fails due to server-related issues such as connectivity problems,
92    /// or if the rule does not exist on the server. This method raises an immediate panic to
93    /// indicate that the operation could not be completed as expected.
94    pub async fn delete_async(&self) {
95        self.server
96            .server_adapter
97            .as_ref()
98            .unwrap()
99            .delete_proxy_rule(self.id)
100            .await
101            .expect("could not delete mock from server");
102    }
103}
104
105/// Represents a recording of interactions (requests and responses) on a mock server.
106/// This structure is used to capture and store detailed information about the HTTP
107/// requests received by the server and the corresponding responses sent back.
108///
109/// The `Recording` structure can be especially useful in testing scenarios where
110/// monitoring and verifying the exact behavior of HTTP interactions is necessary,
111/// such as ensuring that a server is responding with the correct headers, body content,
112/// and status codes in response to various requests.
113pub struct Recording<'a> {
114    pub id: usize,
115    pub(crate) server: &'a MockServer,
116}
117
118/// Represents a reference to a recording of HTTP interactions on a mock server.
119/// This struct allows for management and retrieval of recorded data, such as viewing,
120/// exporting, and deleting the recording.
121impl<'a> Recording<'a> {
122    pub fn new(id: usize, server: &'a MockServer) -> Self {
123        Self { id, server }
124    }
125
126    /// Synchronously deletes the recording from the mock server.
127    /// This method blocks the current thread until the deletion is completed,
128    /// ensuring that the recording is fully removed before proceeding.
129    ///
130    /// # Panics
131    /// Panics if the deletion fails, which can occur if the recording does not exist,
132    /// or there are server connectivity issues.
133    pub fn delete(&mut self) {
134        self.delete_async().join();
135    }
136
137    /// Asynchronously deletes the recording from the mock server.
138    /// This method allows for non-blocking operations, suitable for asynchronous environments
139    /// where tasks are performed concurrently without waiting for the deletion to complete.
140    ///
141    /// # Panics
142    /// Panics if the deletion fails, typically due to the recording not existing on the server
143    /// or connectivity issues with the server. This method provides immediate feedback by
144    /// raising a panic on such failures.
145    pub async fn delete_async(&self) {
146        self.server
147            .server_adapter
148            .as_ref()
149            .unwrap()
150            .delete_recording(self.id)
151            .await
152            .expect("could not delete mock from server");
153    }
154
155    /// Synchronously saves the recording to a specified directory with a timestamped filename.
156    /// The file is named using a combination of the provided scenario name and a UNIX timestamp, formatted as YAML.
157    ///
158    /// # Parameters
159    /// - `dir`: The directory path where the file will be saved.
160    /// - `scenario_name`: A descriptive name for the scenario, used as part of the filename.
161    ///
162    /// # Returns
163    /// Returns a `Result` containing the `PathBuf` of the created file, or an error if the save operation fails.
164    ///
165    /// # Errors
166    /// Errors if the file cannot be written due to issues like directory permissions, unavailable disk space, or other I/O errors.
167    #[cfg(feature = "record")]
168    pub fn save_to<PathRef: AsRef<Path>, IntoString: Into<String>>(
169        &self,
170        dir: PathRef,
171        scenario_name: IntoString,
172    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
173        self.save_to_async(dir, scenario_name).join()
174    }
175
176    /// Asynchronously saves the recording to the specified directory with a scenario-specific and timestamped filename.
177    ///
178    /// # Parameters
179    /// - `dir`: The directory path where the file will be saved.
180    /// - `scenario`: A string representing the scenario name, used as part of the filename.
181    ///
182    /// # Returns
183    /// Returns an `async` `Result` with the `PathBuf` of the saved file or an error if unable to save.
184    #[cfg(feature = "record")]
185    pub async fn save_to_async<PathRef: AsRef<Path>, IntoString: Into<String>>(
186        &self,
187        dir: PathRef,
188        scenario: IntoString,
189    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
190        let rec = self
191            .server
192            .server_adapter
193            .as_ref()
194            .unwrap()
195            .export_recording(self.id)
196            .await?;
197
198        let scenario = scenario.into();
199        let dir = dir.as_ref();
200        let timestamp = std::time::SystemTime::now()
201            .duration_since(std::time::UNIX_EPOCH)?
202            .as_secs();
203        let filename = format!("{}_{}.yaml", scenario, timestamp);
204        let filepath = dir.join(filename);
205
206        if let Some(bytes) = rec {
207            return Ok(write_file(&filepath, &bytes, true).await?);
208        }
209
210        Err("No recording data available".into())
211    }
212
213    /// Synchronously saves the recording to the default directory (`target/httpmock/recordings`) with the scenario name.
214    ///
215    /// # Parameters
216    /// - `scenario_name`: A descriptive name for the scenario, which helps identify the recording file.
217    ///
218    /// # Returns
219    /// Returns a `Result` with the `PathBuf` to the saved file or an error.
220    #[cfg(feature = "record")]
221    pub fn save<IntoString: Into<String>>(
222        &self,
223        scenario_name: IntoString,
224    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
225        self.save_async(scenario_name).join()
226    }
227
228    /// Asynchronously saves the recording to the default directory structured under `target/httpmock/recordings`.
229    ///
230    /// # Parameters
231    /// - `scenario`: A descriptive name for the test scenario, used in naming the saved file.
232    ///
233    /// # Returns
234    /// Returns an `async` `Result` with the `PathBuf` of the saved file or an error.
235    #[cfg(feature = "record")]
236    pub async fn save_async<IntoString: Into<String>>(
237        &self,
238        scenario: IntoString,
239    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
240        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
241            .join("target")
242            .join("httpmock")
243            .join("recordings");
244        self.save_to_async(path, scenario).await
245    }
246}
247
248pub struct ForwardingRuleBuilder {
249    pub(crate) request_requirements: Rc<Cell<RequestRequirements>>,
250    pub(crate) headers: Rc<Cell<Vec<(String, String)>>>,
251}
252
253impl ForwardingRuleBuilder {
254    pub fn add_request_header<Key: Into<String>, Value: Into<String>>(
255        mut self,
256        key: Key,
257        value: Value,
258    ) -> Self {
259        let mut headers = self.headers.take();
260        headers.push((key.into(), value.into()));
261        self.headers.set(headers);
262        self
263    }
264
265    pub fn filter<WhenSpecFn>(mut self, when: WhenSpecFn) -> Self
266    where
267        WhenSpecFn: FnOnce(When),
268    {
269        when(When {
270            expectations: self.request_requirements.clone(),
271        });
272        self
273    }
274}
275
276pub struct ProxyRuleBuilder {
277    // TODO: These fields are visible to the user, make them not public
278    pub(crate) request_requirements: Rc<Cell<RequestRequirements>>,
279    pub(crate) headers: Rc<Cell<Vec<(String, String)>>>,
280}
281
282impl ProxyRuleBuilder {
283    pub fn add_request_header<Key: Into<String>, Value: Into<String>>(
284        mut self,
285        key: Key,
286        value: Value,
287    ) -> Self {
288        let mut headers = self.headers.take();
289        headers.push((key.into(), value.into()));
290        self.headers.set(headers);
291        self
292    }
293
294    pub fn filter<WhenSpecFn>(mut self, when: WhenSpecFn) -> Self
295    where
296        WhenSpecFn: FnOnce(When),
297    {
298        when(When {
299            expectations: self.request_requirements.clone(),
300        });
301
302        self
303    }
304}
305
306pub struct RecordingRuleBuilder {
307    pub config: Rc<Cell<RecordingRuleConfig>>,
308}
309
310impl RecordingRuleBuilder {
311    pub fn record_request_header<IntoString: Into<String>>(mut self, header: IntoString) -> Self {
312        let mut config = self.config.take();
313        config.record_headers.push(header.into());
314        self.config.set(config);
315        self
316    }
317
318    pub fn record_request_headers<IntoString: Into<String>>(
319        mut self,
320        headers: Vec<IntoString>,
321    ) -> Self {
322        let mut config = self.config.take();
323        config
324            .record_headers
325            .extend(headers.into_iter().map(Into::into));
326        self.config.set(config);
327        self
328    }
329
330    pub fn filter<WhenSpecFn>(mut self, when: WhenSpecFn) -> Self
331    where
332        WhenSpecFn: FnOnce(When),
333    {
334        let mut config = self.config.take();
335
336        let mut request_requirements = Rc::new(Cell::new(config.request_requirements));
337
338        when(When {
339            expectations: request_requirements.clone(),
340        });
341
342        config.request_requirements = request_requirements.take();
343
344        self.config.set(config);
345
346        self
347    }
348
349    pub fn record_response_delays(mut self, record: bool) -> Self {
350        let mut config = self.config.take();
351        config.record_response_delays = record;
352        self.config.set(config);
353
354        self
355    }
356}