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}