aws_smithy_http_client/test_util/
replay.rs1use aws_smithy_protocol_test::{assert_ok, validate_body, MediaType};
7use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
8use aws_smithy_runtime_api::client::http::{
9 HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
10};
11use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
12use aws_smithy_runtime_api::client::result::ConnectorError;
13use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
14use aws_smithy_runtime_api::shared::IntoShared;
15use http_1x::header::CONTENT_TYPE;
16use std::ops::Deref;
17use std::sync::{Arc, Mutex, MutexGuard};
18
19type ReplayEvents = Vec<ReplayEvent>;
20
21pub(crate) const DEFAULT_RELAXED_HEADERS: &[&str] =
22 &["x-amz-user-agent", "user-agent", "authorization"];
23
24#[derive(Debug)]
29pub struct ReplayEvent {
30 request: HttpRequest,
31 response: HttpResponse,
32}
33
34impl ReplayEvent {
35 pub fn new(request: impl TryInto<HttpRequest>, response: impl TryInto<HttpResponse>) -> Self {
37 Self {
38 request: request.try_into().ok().expect("invalid request"),
39 response: response.try_into().ok().expect("invalid response"),
40 }
41 }
42
43 pub fn request(&self) -> &HttpRequest {
45 &self.request
46 }
47
48 pub fn response(&self) -> &HttpResponse {
50 &self.response
51 }
52}
53
54impl From<(HttpRequest, HttpResponse)> for ReplayEvent {
55 fn from((request, response): (HttpRequest, HttpResponse)) -> Self {
56 Self::new(request, response)
57 }
58}
59
60#[derive(Debug)]
61struct ValidateRequest {
62 expected: HttpRequest,
63 actual: HttpRequest,
64}
65
66impl ValidateRequest {
67 fn assert_matches(&self, index: usize, ignore_headers: &[&str]) {
68 let (actual, expected) = (&self.actual, &self.expected);
69 assert_eq!(
70 expected.uri(),
71 actual.uri(),
72 "request[{index}] - URI doesn't match expected value"
73 );
74 for (name, value) in expected.headers() {
75 if !ignore_headers.contains(&name) {
76 let actual_header = actual
77 .headers()
78 .get(name)
79 .unwrap_or_else(|| panic!("Request #{index} - Header {name:?} is missing"));
80 assert_eq!(
81 value, actual_header,
82 "request[{index}] - Header {name:?} doesn't match expected value",
83 );
84 }
85 }
86 let actual_str = std::str::from_utf8(actual.body().bytes().unwrap_or(&[]));
87 let expected_str = std::str::from_utf8(expected.body().bytes().unwrap_or(&[]));
88 let media_type = if actual
89 .headers()
90 .get(CONTENT_TYPE)
91 .map(|v| v.contains("json"))
92 .unwrap_or(false)
93 {
94 MediaType::Json
95 } else {
96 MediaType::Other("unknown".to_string())
97 };
98 match (actual_str, expected_str) {
99 (Ok(actual), Ok(expected)) => assert_ok(validate_body(actual, expected, media_type)),
100 _ => assert_eq!(
101 expected.body().bytes(),
102 actual.body().bytes(),
103 "request[{index}] - Body contents didn't match expected value"
104 ),
105 };
106 }
107}
108
109#[derive(Clone, Debug)]
155pub struct StaticReplayClient {
156 data: Arc<Mutex<ReplayEvents>>,
157 requests: Arc<Mutex<Vec<ValidateRequest>>>,
158}
159
160impl StaticReplayClient {
161 pub fn new(mut data: ReplayEvents) -> Self {
163 data.reverse();
164 StaticReplayClient {
165 data: Arc::new(Mutex::new(data)),
166 requests: Default::default(),
167 }
168 }
169
170 pub fn actual_requests(&self) -> impl Iterator<Item = &HttpRequest> + '_ {
172 struct Iter<'a> {
176 _guard: MutexGuard<'a, Vec<ValidateRequest>>,
178 values: *const ValidateRequest,
180 len: usize,
181 next_index: usize,
182 }
183 impl<'a> Iterator for Iter<'a> {
184 type Item = &'a HttpRequest;
185
186 fn next(&mut self) -> Option<Self::Item> {
187 if self.next_index >= self.len {
189 None
190 } else {
191 let next = unsafe {
194 let offset = self.values.add(self.next_index);
195 &*offset
196 };
197 self.next_index += 1;
198 Some(&next.actual)
199 }
200 }
201 }
202
203 let guard = self.requests.lock().unwrap();
204 Iter {
205 values: guard.as_ptr(),
206 len: guard.len(),
207 _guard: guard,
208 next_index: 0,
209 }
210 }
211
212 fn requests(&self) -> impl Deref<Target = Vec<ValidateRequest>> + '_ {
213 self.requests.lock().unwrap()
214 }
215
216 #[track_caller]
225 pub fn assert_requests_match(&self, ignore_headers: &[&str]) {
226 for (i, req) in self.requests().iter().enumerate() {
227 req.assert_matches(i, ignore_headers)
228 }
229 let remaining_requests = self.data.lock().unwrap();
230 assert!(
231 remaining_requests.is_empty(),
232 "Expected {} additional requests (only {} sent)",
233 remaining_requests.len(),
234 self.requests().len()
235 );
236 }
237
238 #[track_caller]
245 pub fn relaxed_requests_match(&self) {
246 self.assert_requests_match(DEFAULT_RELAXED_HEADERS)
247 }
248}
249
250impl HttpConnector for StaticReplayClient {
251 fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
252 let res = if let Some(event) = self.data.lock().unwrap().pop() {
253 self.requests.lock().unwrap().push(ValidateRequest {
254 expected: event.request,
255 actual: request,
256 });
257
258 Ok(event.response)
259 } else {
260 Err(ConnectorError::other(
261 "StaticReplayClient: no more test data available to respond with".into(),
262 None,
263 ))
264 };
265
266 HttpConnectorFuture::new(async move { res })
267 }
268}
269
270impl HttpClient for StaticReplayClient {
271 fn http_connector(
272 &self,
273 _: &HttpConnectorSettings,
274 _: &RuntimeComponents,
275 ) -> SharedHttpConnector {
276 self.clone().into_shared()
277 }
278
279 fn connector_metadata(&self) -> Option<ConnectorMetadata> {
280 Some(ConnectorMetadata::new("static-replay-client", None))
281 }
282}
283
284#[cfg(test)]
285mod test {
286 use crate::test_util::{ReplayEvent, StaticReplayClient};
287 use aws_smithy_types::body::SdkBody;
288
289 #[test]
290 fn create_from_either_http_type() {
291 let _client = StaticReplayClient::new(vec![ReplayEvent::new(
292 http_1x::Request::builder()
293 .uri("test")
294 .body(SdkBody::from("hello"))
295 .unwrap(),
296 http_1x::Response::builder()
297 .status(200)
298 .body(SdkBody::from("hello"))
299 .unwrap(),
300 )]);
301 }
302}