1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! # aws-smithy-mocks
|
7 + | //!
|
8 + | //! A flexible mocking framework for testing clients generated by smithy-rs, including all packages of the AWS SDK for Rust.
|
9 + | //!
|
10 + | //! This crate provides a simple yet powerful way to mock SDK client responses for testing purposes.
|
11 + | //! It uses interceptors to return stub responses, allowing you to test both happy-path and error scenarios
|
12 + | //! without mocking the entire client or using traits.
|
13 + | //!
|
14 + | //! ## Key Features
|
15 + | //!
|
16 + | //! - **Simple API**: Create mock rules with a fluent API using the [`mock!`] macro
|
17 + | //! - **Flexible Response Types**: Return modeled outputs, errors, or raw HTTP responses
|
18 + | //! - **Request Matching**: Match requests based on their properties
|
19 + | //! - **Response Sequencing**: Define sequences of responses for testing retry behavior
|
20 + | //! - **Rule Modes**: Control how rules are matched and applied
|
21 + | //!
|
22 + | //! ## Basic Usage
|
23 + | //!
|
24 + | //! ```rust,ignore
|
25 + | //! use aws_sdk_s3::operation::get_object::GetObjectOutput;
|
26 + | //! use aws_sdk_s3::Client;
|
27 + | //! use aws_smithy_types::byte_stream::ByteStream;
|
28 + | //! use aws_smithy_mocks::{mock, mock_client};
|
29 + | //!
|
30 + | //! #[tokio::test]
|
31 + | //! async fn test_s3_get_object() {
|
32 + | //! // Create a rule that returns a successful response
|
33 + | //! let get_object_rule = mock!(Client::get_object)
|
34 + | //! .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
|
35 + | //! .then_output(|| GetObjectOutput::builder()
|
36 + | //! .body(ByteStream::from_static(b"test-content"))
|
37 + | //! .build()
|
38 + | //! );
|
39 + | //!
|
40 + | //! // Create a mocked client with the rule
|
41 + | //! let s3 = mock_client!(aws_sdk_s3, [&get_object_rule]);
|
42 + | //!
|
43 + | //! // Use the client as you would normally
|
44 + | //! let result = s3.get_object()
|
45 + | //! .bucket("test-bucket")
|
46 + | //! .key("test-key")
|
47 + | //! .send()
|
48 + | //! .await
|
49 + | //! .expect("success response");
|
50 + | //!
|
51 + | //! // Verify the response
|
52 + | //! let data = result.body.collect().await.expect("successful read").to_vec();
|
53 + | //! assert_eq!(data, b"test-content");
|
54 + | //!
|
55 + | //! // Verify the rule was used
|
56 + | //! assert_eq!(get_object_rule.num_calls(), 1);
|
57 + | //! }
|
58 + | //! ```
|
59 + | //!
|
60 + | //! ## Creating Rules
|
61 + | //!
|
62 + | //! Rules are created using the [`mock!`] macro, which takes a client operation as an argument:
|
63 + | //!
|
64 + | //! ```rust,ignore
|
65 + | //! let rule = mock!(Client::get_object)
|
66 + | //! // Optional: Add a matcher to filter requests
|
67 + | //! .match_requests(|req| req.bucket() == Some("test-bucket"))
|
68 + | //! // Add a response
|
69 + | //! .then_output(|| GetObjectOutput::builder().build());
|
70 + | //! ```
|
71 + | //!
|
72 + | //! ### Response Types
|
73 + | //!
|
74 + | //! You can return different types of responses:
|
75 + | //!
|
76 + | //! ```rust,ignore
|
77 + | //! // Return a modeled output
|
78 + | //! let success_rule = mock!(Client::get_object)
|
79 + | //! .then_output(|| GetObjectOutput::builder().build());
|
80 + | //!
|
81 + | //! // Return a modeled error
|
82 + | //! let error_rule = mock!(Client::get_object)
|
83 + | //! .then_error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
|
84 + | //!
|
85 + | //! // Return an HTTP response
|
86 + | //! let http_rule = mock!(Client::get_object)
|
87 + | //! .then_http_response(|| HttpResponse::new(
|
88 + | //! StatusCode::try_from(503).unwrap(),
|
89 + | //! SdkBody::from("service unavailable")
|
90 + | //! ));
|
91 + | //! ```
|
92 + | //!
|
93 + | //! ### Response Sequences
|
94 + | //!
|
95 + | //! For testing retry behavior or complex scenarios, you can define sequences of responses using the sequence builder API:
|
96 + | //!
|
97 + | //! ```rust,ignore
|
98 + | //! let retry_rule = mock!(Client::get_object)
|
99 + | //! .sequence()
|
100 + | //! .http_status(503, None) // First call returns 503
|
101 + | //! .http_status(503, None) // Second call returns 503
|
102 + | //! .output(|| GetObjectOutput::builder().build()) // Third call succeeds
|
103 + | //! .build();
|
104 + | //!
|
105 + | //! // With repetition using `times()`
|
106 + | //! let retry_rule = mock!(Client::get_object)
|
107 + | //! .sequence()
|
108 + | //! .http_status(503, None)
|
109 + | //! .times(2) // First two calls return 503
|
110 + | //! .output(|| GetObjectOutput::builder().build()) // Third call succeeds
|
111 + | //! .build();
|
112 + | //! ```
|
113 + | //!
|
114 + | //! The sequence builder API provides a fluent interface for defining sequences of responses.
|
115 + | //! After providing all responses in the sequence, the rule is considered exhausted.
|
116 + | //!
|
117 + | //! ## Creating Mocked Clients
|
118 + | //!
|
119 + | //! Use the [`mock_client!`] macro to create a client with your rules:
|
120 + | //!
|
121 + | //! ```rust,ignore
|
122 + | //! // Create a client with a single rule
|
123 + | //! let client = mock_client!(aws_sdk_s3, [&rule]);
|
124 + | //!
|
125 + | //! // Create a client with multiple rules and a specific rule mode
|
126 + | //! let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, [&rule1, &rule2]);
|
127 + | //!
|
128 + | //! // Create a client with additional configuration
|
129 + | //! let client = mock_client!(
|
130 + | //! aws_sdk_s3,
|
131 + | //! RuleMode::Sequential,
|
132 + | //! [&rule],
|
133 + | //! |config| config.force_path_style(true)
|
134 + | //! );
|
135 + | //! ```
|
136 + | //!
|
137 + | //! ## Rule Modes
|
138 + | //!
|
139 + | //! The [`RuleMode`] enum controls how rules are matched and applied:
|
140 + | //!
|
141 + | //! - `RuleMode::Sequential`: Rules are tried in order. When a rule is exhausted, the next rule is used.
|
142 + | //! - `RuleMode::MatchAny`: The first matching rule is used, regardless of order.
|
143 + | //!
|
144 + | //! ```rust,ignore
|
145 + | //! let interceptor = MockResponseInterceptor::new()
|
146 + | //! .rule_mode(RuleMode::Sequential)
|
147 + | //! .with_rule(&rule1)
|
148 + | //! .with_rule(&rule2);
|
149 + | //! ```
|
150 + | //!
|
151 + | //! ## Testing Retry Behavior
|
152 + | //!
|
153 + | //! The mocking framework supports testing retry behavior by allowing you to define sequences of responses:
|
154 + | //!
|
155 + | //! ```rust,ignore
|
156 + | //! #[tokio::test]
|
157 + | //! async fn test_retry() {
|
158 + | //! // Create a rule that returns errors for the first two attempts, then succeeds
|
159 + | //! let rule = mock!(Client::get_object)
|
160 + | //! .sequence()
|
161 + | //! .http_status(503, None)
|
162 + | //! .times(2) // Service unavailable for first two calls
|
163 + | //! .output(|| GetObjectOutput::builder().build()) // Success on third call
|
164 + | //! .build();
|
165 + | //!
|
166 + | //! // Create a client with retry enabled
|
167 + | //! let client = mock_client!(aws_sdk_s3, [&rule]);
|
168 + | //!
|
169 + | //! // The operation should succeed after retries
|
170 + | //! let result = client.get_object()
|
171 + | //! .bucket("test-bucket")
|
172 + | //! .key("test-key")
|
173 + | //! .send()
|
174 + | //! .await;
|
175 + | //!
|
176 + | //! assert!(result.is_ok());
|
177 + | //! assert_eq!(rule.num_calls(), 3); // Called 3 times (2 failures + 1 success)
|
178 + | //! }
|
179 + | //! ```
|
180 + | //!
|
181 + |
|
182 + | /* Automatically managed default lints */
|
183 + | #![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
184 + | /* End of automatically managed default lints */
|
185 + | #![warn(
|
186 + | missing_docs,
|
187 + | rustdoc::missing_crate_level_docs,
|
188 + | unreachable_pub,
|
189 + | rust_2018_idioms
|
190 + | )]
|
191 + |
|
192 + | mod interceptor;
|
193 + | mod rule;
|
194 + |
|
195 + | pub use interceptor::{create_mock_http_client, MockResponseInterceptor};
|
196 + | pub(crate) use rule::MockResponse;
|
197 + | pub use rule::{Rule, RuleBuilder, RuleMode};
|
198 + |
|
199 + | // why do we need a macro for this?
|
200 + | // We want customers to be able to provide an ergonomic way to say the method they're looking for,
|
201 + | // `Client::list_buckets`, e.g. But there isn't enough information on that type to recover everything.
|
202 + | // This macro commits a small amount of crimes to recover that type information so we can construct
|
203 + | // a rule that can intercept these operations.
|
204 + |
|
205 + | /// `mock!` macro that produces a [`RuleBuilder`] from a client invocation
|
206 + | ///
|
207 + | /// See the `examples` folder of this crate for fully worked examples.
|
208 + | ///
|
209 + | /// # Examples
|
210 + | ///
|
211 + | /// **Mock and return a success response**:
|
212 + | ///
|
213 + | /// ```rust,ignore
|
214 + | /// use aws_sdk_s3::operation::get_object::GetObjectOutput;
|
215 + | /// use aws_sdk_s3::Client;
|
216 + | /// use aws_smithy_types::byte_stream::ByteStream;
|
217 + | /// use aws_smithy_mocks::mock;
|
218 + | /// let get_object_happy_path = mock!(Client::get_object)
|
219 + | /// .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
|
220 + | /// .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
|
221 + | /// ```
|
222 + | ///
|
223 + | /// **Mock and return an error**:
|
224 + | /// ```rust,ignore
|
225 + | /// use aws_sdk_s3::operation::get_object::GetObjectError;
|
226 + | /// use aws_sdk_s3::types::error::NoSuchKey;
|
227 + | /// use aws_sdk_s3::Client;
|
228 + | /// use aws_smithy_mocks::mock;
|
229 + | /// let get_object_error_path = mock!(Client::get_object)
|
230 + | /// .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
|
231 + | /// ```
|
232 + | ///
|
233 + | #[macro_export]
|
234 + | macro_rules! mock {
|
235 + | ($operation: expr) => {
|
236 + | #[allow(unreachable_code)]
|
237 + | {
|
238 + | $crate::RuleBuilder::new_from_mock(
|
239 + | // We don't actually want to run this code, so we put it in a closure. The closure
|
240 + | // has the types we want which makes this whole thing type-safe (and the IDE can even
|
241 + | // figure out the right input/output types in inference!)
|
242 + | // The code generated here is:
|
243 + | // `Client::list_buckets(todo!())`
|
244 + | || $operation(todo!()).as_input().clone().build().unwrap(),
|
245 + | || $operation(todo!()).send(),
|
246 + | )
|
247 + | }
|
248 + | };
|
249 + | }
|
250 + |
|
251 + | // This could be obviated by a reasonable trait, since you can express it with SdkConfig if clients implement From<&SdkConfig>.
|
252 + |
|
253 + | /// `mock_client!` macro produces a Client configured with a number of Rules and appropriate test default configuration.
|
254 + | ///
|
255 + | /// # Examples
|
256 + | ///
|
257 + | /// **Create a client that uses a mock failure and then a success**:
|
258 + | ///
|
259 + | /// ```rust,ignore
|
260 + | /// use aws_sdk_s3::operation::get_object::{GetObjectOutput, GetObjectError};
|
261 + | /// use aws_sdk_s3::types::error::NoSuchKey;
|
262 + | /// use aws_sdk_s3::Client;
|
263 + | /// use aws_smithy_types::byte_stream::ByteStream;
|
264 + | /// use aws_smithy_mocks::{mock_client, mock, RuleMode};
|
265 + | /// let get_object_error_path = mock!(Client::get_object)
|
266 + | /// .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
|
267 + | /// .build();
|
268 + | /// let get_object_happy_path = mock!(Client::get_object)
|
269 + | /// .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
|
270 + | /// .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build())
|
271 + | /// .build();
|
272 + | /// let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, &[&get_object_error_path, &get_object_happy_path]);
|
273 + | ///
|
274 + | ///
|
275 + | /// **Create a client but customize a specific setting**:
|
276 + | /// rust,ignore
|
277 + | /// use aws_sdk_s3::operation::get_object::GetObjectOutput;
|
278 + | /// use aws_sdk_s3::Client;
|
279 + | /// use aws_smithy_types::byte_stream::ByteStream;
|
280 + | /// use aws_smithy_mocks::{mock_client, mock, RuleMode};
|
281 + | /// let get_object_happy_path = mock!(Client::get_object)
|
282 + | /// .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
|
283 + | /// .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build())
|
284 + | /// .build();
|
285 + | /// let client = mock_client!(
|
286 + | /// aws_sdk_s3,
|
287 + | /// RuleMode::Sequential,
|
288 + | /// &[&get_object_happy_path],
|
289 + | /// // Perhaps you need to force path style
|
290 + | /// |client_builder|client_builder.force_path_style(true)
|
291 + | /// );
|
292 + | /// ```
|
293 + | ///
|
294 + | #[macro_export]
|
295 + | macro_rules! mock_client {
|
296 + | ($aws_crate: ident, $rules: expr) => {
|
297 + | $crate::mock_client!($aws_crate, $crate::RuleMode::Sequential, $rules)
|
298 + | };
|
299 + | ($aws_crate: ident, $rule_mode: expr, $rules: expr) => {{
|
300 + | $crate::mock_client!($aws_crate, $rule_mode, $rules, |conf| conf)
|
301 + | }};
|
302 + | ($aws_crate: ident, $rule_mode: expr, $rules: expr, $additional_configuration: expr) => {{
|
303 + | let mut mock_response_interceptor =
|
304 + | $crate::MockResponseInterceptor::new().rule_mode($rule_mode);
|
305 + | for rule in $rules {
|
306 + | mock_response_interceptor = mock_response_interceptor.with_rule(rule)
|
307 + | }
|
308 + |
|
309 + | // Create a mock HTTP client
|
310 + | let mock_http_client = $crate::create_mock_http_client();
|
311 + |
|
312 + | // Allow callers to avoid explicitly specifying the type
|
313 + | fn coerce<T: Fn($aws_crate::config::Builder) -> $aws_crate::config::Builder>(f: T) -> T {
|
314 + | f
|
315 + | }
|
316 + |
|
317 + | $aws_crate::client::Client::from_conf(
|
318 + | coerce($additional_configuration)(
|
319 + | $aws_crate::config::Config::builder()
|
320 + | .with_test_defaults()
|
321 + | .region($aws_crate::config::Region::from_static("us-east-1"))
|
322 + | .http_client(mock_http_client)
|
323 + | .interceptor(mock_response_interceptor),
|
324 + | )
|
325 + | .build(),
|
326 + | )
|
327 + | }};
|
328 + | }
|