diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 8f940cf89a..a34e8565f0 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -11,6 +11,11 @@ # meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"} # author = "rcoh" +[[aws-smithy-mocks-experimental]] +message = "Replace `enforce_order(bool)` with `enum RuleMode`" +references = ["smithy-rs#3502", "awsdocs/aws-doc-sdk-examples#6264"] +meta = { "breaking" = true, "tada" = false, "bug" = false } + [[smithy-rs]] message = "Increased minimum version of wasi crate dependency in aws-smithy-wasm to 0.12.1." references = ["smithy-rs#3476"] diff --git a/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml b/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml index 13fcb7b337..10164da620 100644 --- a/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml +++ b/rust-runtime/aws-smithy-mocks-experimental/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-mocks-experimental" -version = "0.1.1" +version = "0.2.0" authors = ["AWS Rust SDK Team "] description = "Experimental testing utilities for smithy-rs generated clients" edition = "2021" diff --git a/rust-runtime/aws-smithy-mocks-experimental/src/lib.rs b/rust-runtime/aws-smithy-mocks-experimental/src/lib.rs index 2cedc429b7..8443aeed43 100644 --- a/rust-runtime/aws-smithy-mocks-experimental/src/lib.rs +++ b/rust-runtime/aws-smithy-mocks-experimental/src/lib.rs @@ -75,6 +75,46 @@ macro_rules! mock { }; } +// 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 +/// 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_experimental::{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()); +/// let get_object_error_path = mock!(Client::get_object) +/// .then_error(||GetObjectError::NoSuchKey(NoSuchKey::builder().build())); +/// let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, &[&get_object_error_path, &get_object_happy_path]); +/// ``` +#[macro_export] +macro_rules! mock_client { + ($aws_crate: ident, $rules: expr) => { + mock_client!($aws_crate, $crate::RuleMode::Sequential, $rules) + }; + ($aws_crate: ident, $rule_mode: expr, $rules: 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) + } + $aws_crate::client::Client::from_conf( + $aws_crate::config::Config::builder() + .with_test_defaults() + .region($aws_crate::config::Region::from_static("us-east-1")) + .interceptor(mock_response_interceptor) + .build(), + ) + }}; +} + type MatchFn = Arc bool + Send + Sync>; type OutputFn = Arc Result> + Send + Sync>; @@ -90,10 +130,19 @@ enum MockOutput { ModeledResponse(OutputFn), } +/// RuleMode describes how rules will be interpreted. +/// - In RuleMode::MatchAny, the first matching rule will be applied, and the rules will remain unchanged. +/// - In RuleMode::Sequential, the first matching rule will be applied, and that rule will be removed from the list of rules. +#[derive()] +pub enum RuleMode { + MatchAny, + Sequential, +} + /// Interceptor which produces mock responses based on a list of rules pub struct MockResponseInterceptor { rules: Arc>>, - enforce_order: bool, + rule_mode: RuleMode, must_match: bool, } @@ -213,7 +262,7 @@ impl MockResponseInterceptor { pub fn new() -> Self { Self { rules: Default::default(), - enforce_order: false, + rule_mode: RuleMode::MatchAny, must_match: true, } } @@ -225,11 +274,11 @@ impl MockResponseInterceptor { self } - /// Require that rules are matched in order. + /// Set the RuleMode to use when evaluating rules. /// - /// If a rule matches out of order, the interceptor will panic. - pub fn enforce_order(mut self) -> Self { - self.enforce_order = true; + /// See `RuleMode` enum for modes and how they are applied. + pub fn rule_mode(mut self, rule_mode: RuleMode) -> Self { + self.rule_mode = rule_mode; self } @@ -251,8 +300,8 @@ impl Intercept for MockResponseInterceptor { cfg: &mut ConfigBag, ) -> Result<(), BoxError> { let mut rules = self.rules.lock().unwrap(); - let rule = match self.enforce_order { - true => { + let rule = match self.rule_mode { + RuleMode::Sequential => { let rule = rules .pop_front() .expect("no more rules but a new request was received"); @@ -264,7 +313,7 @@ impl Intercept for MockResponseInterceptor { } Some(rule) } - false => rules + RuleMode::MatchAny => rules .iter() .find(|rule| (rule.matcher)(context.input())) .cloned(), diff --git a/rust-runtime/aws-smithy-mocks-experimental/tests/get-object-mocks.rs b/rust-runtime/aws-smithy-mocks-experimental/tests/get-object-mocks.rs index 2448df010b..784d495d43 100644 --- a/rust-runtime/aws-smithy-mocks-experimental/tests/get-object-mocks.rs +++ b/rust-runtime/aws-smithy-mocks-experimental/tests/get-object-mocks.rs @@ -14,7 +14,7 @@ use aws_smithy_types::byte_stream::ByteStream; use aws_smithy_types::error::metadata::ProvideErrorMetadata; use aws_smithy_types::error::ErrorMetadata; -use aws_smithy_mocks_experimental::{mock, MockResponseInterceptor}; +use aws_smithy_mocks_experimental::{mock, mock_client, MockResponseInterceptor, RuleMode}; const S3_NO_SUCH_KEY: &str = r#" @@ -52,10 +52,10 @@ async fn create_mock_s3_get_object() { }); let get_object_mocks = MockResponseInterceptor::new() + .rule_mode(RuleMode::Sequential) .with_rule(&s3_404) .with_rule(&s3_real_object) - .with_rule(&modeled_error) - .enforce_order(); + .with_rule(&modeled_error); let s3 = aws_sdk_s3::Client::from_conf( Config::builder() @@ -96,3 +96,49 @@ async fn create_mock_s3_get_object() { let err = s3.list_buckets().send().await.expect_err("bad access key"); assert_eq!(err.code(), Some("InvalidAccessKey")); } + +#[tokio::test] +async fn mock_client() { + let s3_404 = mock!(Client::get_object).then_http_response(|| { + HttpResponse::new( + StatusCode::try_from(400).unwrap(), + SdkBody::from(S3_NO_SUCH_KEY), + ) + }); + + let s3_real_object = mock!(Client::get_object).then_output(|| { + GetObjectOutput::builder() + .body(ByteStream::from_static(b"test-test-test")) + .build() + }); + + let s3 = mock_client!(aws_sdk_s3, [&s3_404, &s3_real_object]); + + let error = s3 + .get_object() + .bucket("test-bucket") + .key("foo") + .send() + .await + .expect_err("404"); + assert!(matches!( + error.into_service_error(), + GetObjectError::NoSuchKey(_) + )); + assert_eq!(s3_404.num_calls(), 1); + + let data = s3 + .get_object() + .bucket("test-bucket") + .key("correct-key") + .send() + .await + .expect("success response") + .body + .collect() + .await + .expect("successful read") + .to_vec(); + assert_eq!(data, b"test-test-test"); + assert_eq!(s3_real_object.num_calls(), 1); +}