Unverified Commit e858d3e2 authored by David Souther's avatar David Souther Committed by GitHub
Browse files

Replace `enforce_order(bool)` with `enum RuleMode` (#3502)

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.

Also adds a `make_client!` macro produces a Client configured with a
number of Rules and appropriate test default configuration.

## Motivation and Context
Working through improvements on experimental mocks after implementing
them in the Cloudwatch Logs example.

## Testing
Unit tests, doctests, 

## Checklist
- [x] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [ ] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent 3e81645f
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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"]
+1 −1
Original line number Diff line number Diff line
[package]
name = "aws-smithy-mocks-experimental"
version = "0.1.1"
version = "0.2.0"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
description = "Experimental testing utilities for smithy-rs generated clients"
edition = "2021"
+58 −9
Original line number Diff line number Diff line
@@ -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<dyn Fn(&Input) -> bool + Send + Sync>;
type OutputFn = Arc<dyn Fn() -> Result<Output, OrchestratorError<Error>> + 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<Mutex<VecDeque<Rule>>>,
    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(),
+49 −3
Original line number Diff line number Diff line
@@ -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#"<?xml version="1.0" encoding="UTF-8"?>
<Error>
@@ -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);
}