Skip to content

Commit b6bb399

Browse files
authored
promote aws-smithy-mocks out of experimental (#4098)
## Motivation and Context * #4074 * #3926 ## Description * Rename `aws-smithy-mocks-experimental` to `aws-smithy-mocks` and reset version to `0.1.0` * Fix [#3926](#3926) by having `mock_client!` macro always set an HTTP client. By default it returns a dummy I'm a teapot response to aid in debugging and be easy to tell where that response is coming from. Also when a rule sets an HTTP response it is not set as an extension in the request and returned by the HTTP client. This allows for more thorough and accurate integration testing as the client/runtime will see the response coming from the HTTP client instead of being ignored and then replaced as it was before * Added a new sequence builder API for allowing a rule to generate more than one response. This allows for testing retries for example. * Also deleted a hyper 1.x test that is no longer needed and was ignored anyway ## Questions * In this PR I've simply renamed/moved `aws-smithy-mocks-experimental` to `aws-smithy-mocks`. Do we want or need to publish one last version of `aws-smithy-mocks-experimental` with a deprecated API that instructs users it has moved? Alternatively we could publish an [informational advisory notice](https://github.com/RustSec/advisory-db#advisory-format) about the rename. We don't currently promote the crate anywhere and the dev guide will get updated so I'm on the fence for what we want to do. ## Testing * Added new unit tests to exercise the API without needing an SDK client by leveraging the manual `Operation` APIs. * Added new integration tests for S3 to cover the macros and test the library e2e. ## Checklist <!--- If a checkbox below is not applicable, then please DELETE it rather than leaving it unchecked --> - [x] For changes to the smithy-rs codegen or runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "client," "server," or both in the `applies_to` key. - [x] For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "aws-sdk-rust" in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent db90ed1 commit b6bb399

File tree

25 files changed

+2011
-78
lines changed

25 files changed

+2011
-78
lines changed

.changelog/1745330307.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
applies_to:
3+
- client
4+
- aws-sdk-rust
5+
authors:
6+
- aajtodd
7+
references:
8+
- smithy-rs#4074
9+
- smithy-rs#3926
10+
breaking: false
11+
new_feature: true
12+
bug_fix: true
13+
---
14+
Promote `aws-smithy-mocks-experimental` to `aws-smithy-mocks`. This crate is now a recommended tool for testing
15+
generated SDK clients. This release includes several fixes as well as a new sequence builder API that can be
16+
used to test more complex scenarios such as retries.
17+
18+
```rust
19+
use aws_sdk_s3::operation::get_object::GetObjectOutput;
20+
use aws_sdk_s3::config::retry::RetryConfig;
21+
use aws_smithy_types::byte_stream::ByteStream;
22+
use aws_smithy_mocks::{mock, mock_client, RuleMode};
23+
24+
#[tokio::test]
25+
async fn test_retry_behavior() {
26+
// Create a rule that returns 503 twice, then succeeds
27+
let retry_rule = mock!(aws_sdk_s3::Client::get_object)
28+
.sequence()
29+
.http_status(503, None)
30+
.times(2) // Return 503 HTTP status twice
31+
.output(|| GetObjectOutput::builder() // Finally return a successful output
32+
.body(ByteStream::from_static(b"success"))
33+
.build())
34+
.build();
35+
36+
// Create a mocked client with the rule
37+
let s3 = mock_client!(
38+
aws_sdk_s3,
39+
RuleMode::Sequential,
40+
[&retry_rule],
41+
|client_builder| {
42+
client_builder.retry_config(RetryConfig::standard().with_max_attempts(3))
43+
}
44+
);
45+
46+
// This should succeed after two retries
47+
let result = s3
48+
.get_object()
49+
.bucket("test-bucket")
50+
.key("test-key")
51+
.send()
52+
.await
53+
.expect("success after retries");
54+
55+
// Verify the response
56+
let data = result.body.collect().await.expect("successful read").to_vec();
57+
assert_eq!(data, b"success");
58+
59+
// Verify all responses were used
60+
assert_eq!(retry_rule.num_calls(), 3);
61+
}
62+
```

aws/rust-runtime/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/rust-runtime/aws-config/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/IntegrationTestDependencies.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Compani
2929
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.TracingSubscriber
3030
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.TracingTest
3131
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.smithyHttpClient
32+
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.smithyMocks
3233
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.smithyProtocolTestHelpers
3334
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.smithyRuntime
3435
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency.Companion.smithyRuntimeApiTestUtil
@@ -170,5 +171,6 @@ class S3TestDependencies(private val runtimeConfig: RuntimeConfig) : LibRsCustom
170171
addDependency(TempFile)
171172
addDependency(TracingAppender)
172173
addDependency(TracingTest)
174+
addDependency(smithyMocks(runtimeConfig))
173175
}
174176
}

aws/sdk/Cargo.lock

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/sdk/integration-tests/s3/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", feat
2727
aws-smithy-runtime-api = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime-api", features = ["test-util", "http-1x"] }
2828
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }
2929
aws-smithy-http-client = { path = "../../build/aws-sdk/sdk/aws-smithy-http-client", features = ["default-client", "rustls-ring", "test-util", "wire-mock"] }
30+
aws-smithy-mocks = { path = "../../build/aws-sdk/sdk/aws-smithy-mocks" }
3031
aws-types = { path = "../../build/aws-sdk/sdk/aws-types" }
3132
bytes = "1"
3233
bytes-utils = "0.1.2"

aws/sdk/integration-tests/s3/tests/hyper-10.rs

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
//! Integration tests for `aws-smithy-mocks`. These tests are not necessarily specific to S3 but
7+
//! we need to test the macros against an actual SDK.
8+
9+
use aws_sdk_s3::config::retry::RetryConfig;
10+
use aws_sdk_s3::operation::get_object::{GetObjectError, GetObjectOutput};
11+
use aws_sdk_s3::operation::list_buckets::ListBucketsError;
12+
use aws_smithy_mocks::{mock, mock_client, RuleMode};
13+
use aws_smithy_runtime_api::client::orchestrator::HttpResponse;
14+
use aws_smithy_runtime_api::http::StatusCode;
15+
use aws_smithy_types::body::SdkBody;
16+
use aws_smithy_types::byte_stream::ByteStream;
17+
use aws_smithy_types::error::metadata::ProvideErrorMetadata;
18+
use aws_smithy_types::error::ErrorMetadata;
19+
20+
const S3_NO_SUCH_KEY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
21+
<Error>
22+
<Code>NoSuchKey</Code>
23+
<Message>The resource you requested does not exist</Message>
24+
<Resource>/mybucket/myfoto.jpg</Resource>
25+
<RequestId>4442587FB7D0A2F9</RequestId>
26+
</Error>"#;
27+
28+
#[tokio::test]
29+
async fn test_mock_client() {
30+
let s3_404 = mock!(aws_sdk_s3::Client::get_object)
31+
.match_requests(|inp| {
32+
inp.bucket() == Some("test-bucket") && inp.key() != Some("correct-key")
33+
})
34+
.then_http_response(|| {
35+
HttpResponse::new(
36+
StatusCode::try_from(400).unwrap(),
37+
SdkBody::from(S3_NO_SUCH_KEY),
38+
)
39+
});
40+
41+
let s3_real_object = mock!(aws_sdk_s3::Client::get_object)
42+
.match_requests(|inp| {
43+
inp.bucket() == Some("test-bucket") && inp.key() == Some("correct-key")
44+
})
45+
.then_output(|| {
46+
GetObjectOutput::builder()
47+
.body(ByteStream::from_static(b"test-test-test"))
48+
.build()
49+
});
50+
51+
let modeled_error = mock!(aws_sdk_s3::Client::list_buckets).then_error(|| {
52+
ListBucketsError::generic(ErrorMetadata::builder().code("InvalidAccessKey").build())
53+
});
54+
55+
let s3 = mock_client!(aws_sdk_s3, &[&s3_404, &s3_real_object, &modeled_error]);
56+
57+
let error = s3
58+
.get_object()
59+
.bucket("test-bucket")
60+
.key("foo")
61+
.send()
62+
.await
63+
.expect_err("404");
64+
65+
assert!(matches!(
66+
error.into_service_error(),
67+
GetObjectError::NoSuchKey(_)
68+
));
69+
70+
assert_eq!(s3_404.num_calls(), 1);
71+
72+
let data = s3
73+
.get_object()
74+
.bucket("test-bucket")
75+
.key("correct-key")
76+
.send()
77+
.await
78+
.expect("success response")
79+
.body
80+
.collect()
81+
.await
82+
.expect("successful read")
83+
.to_vec();
84+
85+
assert_eq!(data, b"test-test-test");
86+
assert_eq!(s3_real_object.num_calls(), 1);
87+
88+
let err = s3.list_buckets().send().await.expect_err("bad access key");
89+
assert_eq!(err.code(), Some("InvalidAccessKey"));
90+
}
91+
92+
#[tokio::test]
93+
async fn test_mock_client_sequence() {
94+
let rule = mock!(aws_sdk_s3::Client::get_object)
95+
.sequence()
96+
.http_status(400, Some(S3_NO_SUCH_KEY.to_string()))
97+
.output(|| {
98+
GetObjectOutput::builder()
99+
.body(ByteStream::from_static(b"test-test-test"))
100+
.build()
101+
})
102+
.build();
103+
104+
// test client builder override
105+
let s3 = mock_client!(
106+
aws_sdk_s3,
107+
RuleMode::Sequential,
108+
[&rule],
109+
|client_builder| { client_builder.endpoint_url("http://localhost:9000") }
110+
);
111+
112+
let error = s3
113+
.get_object()
114+
.bucket("test-bucket")
115+
.key("foo")
116+
.send()
117+
.await
118+
.expect_err("404");
119+
120+
assert!(matches!(
121+
error.into_service_error(),
122+
GetObjectError::NoSuchKey(_)
123+
));
124+
assert_eq!(1, rule.num_calls());
125+
let data = s3
126+
.get_object()
127+
.bucket("test-bucket")
128+
.key("correct-key")
129+
.send()
130+
.await
131+
.expect("success response")
132+
.body
133+
.collect()
134+
.await
135+
.expect("successful read")
136+
.to_vec();
137+
138+
assert_eq!(data, b"test-test-test");
139+
assert_eq!(2, rule.num_calls());
140+
}
141+
142+
#[tokio::test]
143+
async fn test_mock_client_retries() {
144+
let rule = mock!(aws_sdk_s3::Client::get_object)
145+
.sequence()
146+
.http_status(503, None)
147+
.times(2)
148+
.output(|| {
149+
GetObjectOutput::builder()
150+
.body(ByteStream::from_static(b"test-test-test"))
151+
.build()
152+
})
153+
.build();
154+
155+
// test client builder override
156+
let s3 = mock_client!(
157+
aws_sdk_s3,
158+
RuleMode::Sequential,
159+
[&rule],
160+
|client_builder| {
161+
client_builder.retry_config(RetryConfig::standard().with_max_attempts(3))
162+
}
163+
);
164+
165+
let data = s3
166+
.get_object()
167+
.bucket("test-bucket")
168+
.key("correct-key")
169+
.send()
170+
.await
171+
.expect("success response")
172+
.body
173+
.collect()
174+
.await
175+
.expect("successful read")
176+
.to_vec();
177+
178+
assert_eq!(data, b"test-test-test");
179+
assert_eq!(3, rule.num_calls());
180+
}

buildSrc/src/main/kotlin/CrateSet.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ object CrateSet {
7070
"aws-smithy-http-auth",
7171
"aws-smithy-http-tower",
7272
"aws-smithy-json",
73+
"aws-smithy-mocks",
7374
"aws-smithy-mocks-experimental",
7475
"aws-smithy-observability",
7576
"aws-smithy-observability-otel",

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ data class CargoDependency(
394394

395395
fun smithyXml(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-xml")
396396

397+
fun smithyMocks(runtimeConfig: RuntimeConfig) =
398+
runtimeConfig.smithyRuntimeCrate("smithy-mocks", scope = DependencyScope.Dev)
399+
397400
// behind feature-gate
398401
val Serde =
399402
CargoDependency("serde", CratesIo("1.0"), features = setOf("derive"), scope = DependencyScope.CfgUnstable)

0 commit comments

Comments
 (0)