1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

//! # aws-smithy-mocks
//!
//! A flexible mocking framework for testing clients generated by smithy-rs, including all packages of the AWS SDK for Rust.
//!
//! This crate provides a simple yet powerful way to mock SDK client responses for testing purposes.
//! It uses interceptors to return stub responses, allowing you to test both happy-path and error scenarios
//! without mocking the entire client or using traits.
//!
//! ## Key Features
//!
//! - **Simple API**: Create mock rules with a fluent API using the [`mock!`] macro
//! - **Flexible Response Types**: Return modeled outputs, errors, or raw HTTP responses
//! - **Request Matching**: Match requests based on their properties
//! - **Response Sequencing**: Define sequences of responses for testing retry behavior
//! - **Rule Modes**: Control how rules are matched and applied
//!
//! ## Basic Usage
//!
//! ```rust,ignore
//! use aws_sdk_s3::operation::get_object::GetObjectOutput;
//! use aws_sdk_s3::Client;
//! use aws_smithy_types::byte_stream::ByteStream;
//! use aws_smithy_mocks::{mock, mock_client};
//!
//! #[tokio::test]
//! async fn test_s3_get_object() {
//!     // Create a rule that returns a successful response
//!     let get_object_rule = mock!(Client::get_object)
//!         .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
//!         .then_output(|| GetObjectOutput::builder()
//!             .body(ByteStream::from_static(b"test-content"))
//!             .build()
//!          );
//!
//!     // Create a mocked client with the rule
//!     let s3 = mock_client!(aws_sdk_s3, [&get_object_rule]);
//!
//!     // Use the client as you would normally
//!     let result = s3.get_object()
//!         .bucket("test-bucket")
//!         .key("test-key")
//!         .send()
//!         .await
//!         .expect("success response");
//!
//!     // Verify the response
//!     let data = result.body.collect().await.expect("successful read").to_vec();
//!     assert_eq!(data, b"test-content");
//!
//!     // Verify the rule was used
//!     assert_eq!(get_object_rule.num_calls(), 1);
//! }
//! ```
//!
//! ## Creating Rules
//!
//! Rules are created using the [`mock!`] macro, which takes a client operation as an argument:
//!
//! ```rust,ignore
//! let rule = mock!(Client::get_object)
//!     // Optional: Add a matcher to filter requests
//!     .match_requests(|req| req.bucket() == Some("test-bucket"))
//!     // Add a response
//!     .then_output(|| GetObjectOutput::builder().build());
//! ```
//!
//! ### Response Types
//!
//! You can return different types of responses:
//!
//! ```rust,ignore
//! // Return a modeled output
//! let success_rule = mock!(Client::get_object)
//!     .then_output(|| GetObjectOutput::builder().build());
//!
//! // Return a modeled error
//! let error_rule = mock!(Client::get_object)
//!     .then_error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
//!
//! // Return an HTTP response
//! let http_rule = mock!(Client::get_object)
//!     .then_http_response(|| HttpResponse::new(
//!         StatusCode::try_from(503).unwrap(),
//!         SdkBody::from("service unavailable")
//!     ));
//! ```
//!
//! ### Response Sequences
//!
//! For testing retry behavior or complex scenarios, you can define sequences of responses using the sequence builder API:
//!
//! ```rust,ignore
//! let retry_rule = mock!(Client::get_object)
//!     .sequence()
//!     .http_status(503, None)                          // First call returns 503
//!     .http_status(503, None)                          // Second call returns 503
//!     .output(|| GetObjectOutput::builder().build())   // Third call succeeds
//!     .build();
//!
//! // With repetition using `times()`
//! let retry_rule = mock!(Client::get_object)
//!     .sequence()
//!     .http_status(503, None)
//!     .times(2)                                        // First two calls return 503
//!     .output(|| GetObjectOutput::builder().build())   // Third call succeeds
//!     .build();
//! ```
//!
//! The sequence builder API provides a fluent interface for defining sequences of responses.
//! After providing all responses in the sequence, the rule is considered exhausted.
//!
//! ## Creating Mocked Clients
//!
//! Use the [`mock_client!`] macro to create a client with your rules:
//!
//! ```rust,ignore
//! // Create a client with a single rule
//! let client = mock_client!(aws_sdk_s3, [&rule]);
//!
//! // Create a client with multiple rules and a specific rule mode
//! let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, [&rule1, &rule2]);
//!
//! // Create a client with additional configuration
//! let client = mock_client!(
//!     aws_sdk_s3,
//!     RuleMode::Sequential,
//!     [&rule],
//!     |config| config.force_path_style(true)
//! );
//! ```
//!
//! ## Rule Modes
//!
//! The [`RuleMode`] enum controls how rules are matched and applied:
//!
//! - `RuleMode::Sequential`: Rules are tried in order. When a rule is exhausted, the next rule is used.
//! - `RuleMode::MatchAny`: The first matching rule is used, regardless of order.
//!
//! ```rust,ignore
//! let interceptor = MockResponseInterceptor::new()
//!     .rule_mode(RuleMode::Sequential)
//!     .with_rule(&rule1)
//!     .with_rule(&rule2);
//! ```
//!
//! ## Testing Retry Behavior
//!
//! The mocking framework supports testing retry behavior by allowing you to define sequences of responses:
//!
//! ```rust,ignore
//! #[tokio::test]
//! async fn test_retry() {
//!     // Create a rule that returns errors for the first two attempts, then succeeds
//!     let rule = mock!(Client::get_object)
//!         .sequence()
//!         .http_status(503, None)
//!         .times(2)                                       // Service unavailable for first two calls
//!         .output(|| GetObjectOutput::builder().build())  // Success on third call
//!         .build();
//!
//!     // Create a client with retry enabled
//!     let client = mock_client!(aws_sdk_s3, [&rule]);
//!
//!     // The operation should succeed after retries
//!     let result = client.get_object()
//!         .bucket("test-bucket")
//!         .key("test-key")
//!         .send()
//!         .await;
//!
//!     assert!(result.is_ok());
//!     assert_eq!(rule.num_calls(), 3);  // Called 3 times (2 failures + 1 success)
//! }
//! ```
//!

/* Automatically managed default lints */
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
/* End of automatically managed default lints */
#![warn(
    missing_docs,
    rustdoc::missing_crate_level_docs,
    unreachable_pub,
    rust_2018_idioms
)]

mod interceptor;
mod rule;

pub use interceptor::{create_mock_http_client, MockResponseInterceptor};
pub(crate) use rule::MockResponse;
pub use rule::{Rule, RuleBuilder, RuleMode};

// why do we need a macro for this?
// We want customers to be able to provide an ergonomic way to say the method they're looking for,
// `Client::list_buckets`, e.g. But there isn't enough information on that type to recover everything.
// This macro commits a small amount of crimes to recover that type information so we can construct
// a rule that can intercept these operations.

/// `mock!` macro that produces a [`RuleBuilder`] from a client invocation
///
/// See the `examples` folder of this crate for fully worked examples.
///
/// # Examples
///
/// **Mock and return a success response**:
///
/// ```rust,ignore
/// use aws_sdk_s3::operation::get_object::GetObjectOutput;
/// use aws_sdk_s3::Client;
/// use aws_smithy_types::byte_stream::ByteStream;
/// use aws_smithy_mocks::mock;
/// let get_object_happy_path = mock!(Client::get_object)
///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build());
/// ```
///
/// **Mock and return an error**:
/// ```rust,ignore
/// use aws_sdk_s3::operation::get_object::GetObjectError;
/// use aws_sdk_s3::types::error::NoSuchKey;
/// use aws_sdk_s3::Client;
/// use aws_smithy_mocks::mock;
/// let get_object_error_path = mock!(Client::get_object)
///   .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()));
/// ```
///
#[macro_export]
macro_rules! mock {
    ($operation: expr) => {
        #[allow(unreachable_code)]
        {
            $crate::RuleBuilder::new_from_mock(
                // We don't actually want to run this code, so we put it in a closure. The closure
                // has the types we want which makes this whole thing type-safe (and the IDE can even
                // figure out the right input/output types in inference!)
                // The code generated here is:
                // `Client::list_buckets(todo!())`
                || $operation(todo!()).as_input().clone().build().unwrap(),
                || $operation(todo!()).send(),
            )
        }
    };
}

// This could be obviated by a reasonable trait, since you can express it with SdkConfig if clients implement From<&SdkConfig>.

/// `mock_client!` macro produces a Client configured with a number of Rules and appropriate test default configuration.
///
/// # Examples
///
/// **Create a client that uses a mock failure and then a success**:
///
/// ```rust,ignore
/// use aws_sdk_s3::operation::get_object::{GetObjectOutput, GetObjectError};
/// use aws_sdk_s3::types::error::NoSuchKey;
/// use aws_sdk_s3::Client;
/// use aws_smithy_types::byte_stream::ByteStream;
/// use aws_smithy_mocks::{mock_client, mock, RuleMode};
/// let get_object_error_path = mock!(Client::get_object)
///   .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build()))
///   .build();
/// let get_object_happy_path = mock!(Client::get_object)
///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build())
///   .build();
/// let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, &[&get_object_error_path, &get_object_happy_path]);
///
///
/// **Create a client but customize a specific setting**:
/// rust,ignore
/// use aws_sdk_s3::operation::get_object::GetObjectOutput;
/// use aws_sdk_s3::Client;
/// use aws_smithy_types::byte_stream::ByteStream;
/// use aws_smithy_mocks::{mock_client, mock, RuleMode};
/// let get_object_happy_path = mock!(Client::get_object)
///   .match_requests(|req|req.bucket() == Some("test-bucket") && req.key() == Some("test-key"))
///   .then_output(||GetObjectOutput::builder().body(ByteStream::from_static(b"12345-abcde")).build())
///   .build();
/// let client = mock_client!(
///     aws_sdk_s3,
///     RuleMode::Sequential,
///     &[&get_object_happy_path],
///     // Perhaps you need to force path style
///     |client_builder|client_builder.force_path_style(true)
/// );
/// ```
///
#[macro_export]
macro_rules! mock_client {
    ($aws_crate: ident, $rules: expr) => {
        $crate::mock_client!($aws_crate, $crate::RuleMode::Sequential, $rules)
    };
    ($aws_crate: ident, $rule_mode: expr, $rules: expr) => {{
        $crate::mock_client!($aws_crate, $rule_mode, $rules, |conf| conf)
    }};
    ($aws_crate: ident, $rule_mode: expr, $rules: expr, $additional_configuration: expr) => {{
        let mut mock_response_interceptor =
            $crate::MockResponseInterceptor::new().rule_mode($rule_mode);
        for rule in $rules {
            mock_response_interceptor = mock_response_interceptor.with_rule(rule)
        }

        // Create a mock HTTP client
        let mock_http_client = $crate::create_mock_http_client();

        // Allow callers to avoid explicitly specifying the type
        fn coerce<T: Fn($aws_crate::config::Builder) -> $aws_crate::config::Builder>(f: T) -> T {
            f
        }

        $aws_crate::client::Client::from_conf(
            coerce($additional_configuration)(
                $aws_crate::config::Config::builder()
                    .with_test_defaults()
                    .region($aws_crate::config::Region::from_static("us-east-1"))
                    .http_client(mock_http_client)
                    .interceptor(mock_response_interceptor),
            )
            .build(),
        )
    }};
}