mirror of https://github.com/smithy-lang/smithy-rs
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._
This commit is contained in:
parent
3e81645f1f
commit
e858d3e262
|
@ -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,6 +1,6 @@
|
|||
[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"
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue