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(),
)
}};
}