aws_smithy_http_client/test_util/
wire.rs1#![allow(missing_docs)]
41
42pub mod connection;
43
44use aws_smithy_async::future::never::Never;
45use aws_smithy_async::future::BoxFuture;
46use aws_smithy_runtime_api::client::http::SharedHttpClient;
47use bytes::Bytes;
48use http_body_util::Full;
49use hyper::service::service_fn;
50use hyper_util::client::legacy::connect::dns::Name;
51use hyper_util::rt::{TokioExecutor, TokioIo};
52use hyper_util::server::graceful::{GracefulConnection, GracefulShutdown};
53use std::collections::HashSet;
54use std::convert::Infallible;
55use std::error::Error;
56use std::future::Future;
57use std::iter::Once;
58use std::net::SocketAddr;
59use std::sync::{Arc, Mutex};
60use std::task::{Context, Poll};
61use tokio::net::TcpListener;
62use tokio::sync::oneshot;
63
64#[non_exhaustive]
66#[derive(Debug, Clone)]
67pub enum RecordedEvent {
68 DnsLookup(String),
69 NewConnection,
70 Response(ReplayedEvent),
71}
72
73type Matcher = (
74 Box<dyn Fn(&RecordedEvent) -> Result<(), Box<dyn Error>>>,
75 &'static str,
76);
77
78pub fn check_matches(events: &[RecordedEvent], matchers: &[Matcher]) {
80 let mut events_iter = events.iter();
81 let mut matcher_iter = matchers.iter();
82 let mut idx = -1;
83 loop {
84 idx += 1;
85 let bail = |err: Box<dyn Error>| {
86 panic!("failed on event {idx}:\n {err}\n actual recorded events: {events:?}")
87 };
88 match (events_iter.next(), matcher_iter.next()) {
89 (Some(event), Some((matcher, _msg))) => matcher(event).unwrap_or_else(bail),
90 (None, None) => return,
91 (Some(event), None) => {
92 bail(format!("got {event:?} but no more events were expected").into())
93 }
94 (None, Some((_expect, msg))) => {
95 bail(format!("expected {msg:?} but no more events were expected").into())
96 }
97 }
98 }
99}
100
101#[macro_export]
102macro_rules! matcher {
103 ($expect:tt) => {
104 (
105 Box::new(|event: &$crate::test_util::wire::RecordedEvent| {
106 if !matches!(event, $expect) {
107 return Err(
108 format!("expected `{}` but got {:?}", stringify!($expect), event).into(),
109 );
110 }
111 Ok(())
112 }),
113 stringify!($expect),
114 )
115 };
116}
117
118#[macro_export]
120macro_rules! match_events {
121 ($( $expect:pat),*) => {
122 |events| {
123 $crate::test_util::wire::check_matches(events, &[$( $crate::matcher!($expect) ),*]);
124 }
125 };
126 }
127
128#[macro_export]
130macro_rules! ev {
131 (http($status:expr)) => {
132 $crate::test_util::wire::RecordedEvent::Response(
133 $crate::test_util::wire::ReplayedEvent::HttpResponse {
134 status: $status,
135 ..
136 },
137 )
138 };
139 (dns) => {
140 $crate::test_util::wire::RecordedEvent::DnsLookup(_)
141 };
142 (connect) => {
143 $crate::test_util::wire::RecordedEvent::NewConnection
144 };
145 (timeout) => {
146 $crate::test_util::wire::RecordedEvent::Response(
147 $crate::test_util::wire::ReplayedEvent::Timeout,
148 )
149 };
150}
151
152pub use {ev, match_events, matcher};
153
154#[non_exhaustive]
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub enum ReplayedEvent {
157 Timeout,
158 HttpResponse { status: u16, body: Bytes },
159}
160
161impl ReplayedEvent {
162 pub fn ok() -> Self {
163 Self::HttpResponse {
164 status: 200,
165 body: Bytes::new(),
166 }
167 }
168
169 pub fn with_body(body: impl AsRef<[u8]>) -> Self {
170 Self::HttpResponse {
171 status: 200,
172 body: Bytes::copy_from_slice(body.as_ref()),
173 }
174 }
175
176 pub fn status(status: u16) -> Self {
177 Self::HttpResponse {
178 status,
179 body: Bytes::new(),
180 }
181 }
182}
183
184#[derive(Debug)]
194pub struct WireMockServer {
195 event_log: Arc<Mutex<Vec<RecordedEvent>>>,
196 bind_addr: SocketAddr,
197 shutdown_hook: oneshot::Sender<()>,
199}
200
201#[derive(Debug, Clone)]
202struct SharedGraceful {
203 graceful: Arc<Mutex<Option<hyper_util::server::graceful::GracefulShutdown>>>,
204}
205
206impl SharedGraceful {
207 fn new() -> Self {
208 Self {
209 graceful: Arc::new(Mutex::new(Some(GracefulShutdown::new()))),
210 }
211 }
212
213 fn watch<C: GracefulConnection>(&self, conn: C) -> impl Future<Output = C::Output> {
214 let graceful = self.graceful.lock().unwrap();
215 graceful
216 .as_ref()
217 .expect("graceful not shutdown")
218 .watch(conn)
219 }
220
221 async fn shutdown(&self) {
222 let graceful = { self.graceful.lock().unwrap().take() };
223
224 if let Some(graceful) = graceful {
225 graceful.shutdown().await;
226 }
227 }
228}
229
230impl WireMockServer {
231 pub async fn start(mut response_events: Vec<ReplayedEvent>) -> Self {
233 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
234 let (tx, mut rx) = oneshot::channel();
235 let listener_addr = listener.local_addr().unwrap();
236 response_events.reverse();
237 let response_events = Arc::new(Mutex::new(response_events));
238 let handler_events = response_events;
239 let wire_events = Arc::new(Mutex::new(vec![]));
240 let wire_log_for_service = wire_events.clone();
241 let poisoned_conns: Arc<Mutex<HashSet<SocketAddr>>> = Default::default();
242 let graceful = SharedGraceful::new();
243 let conn_builder = Arc::new(hyper_util::server::conn::auto::Builder::new(
244 TokioExecutor::new(),
245 ));
246
247 let server = async move {
248 let poisoned_conns = poisoned_conns.clone();
249 let events = handler_events.clone();
250 let wire_log = wire_log_for_service.clone();
251 loop {
252 tokio::select! {
253 Ok((stream, remote_addr)) = listener.accept() => {
254 tracing::info!("established connection: {:?}", remote_addr);
255 let poisoned_conns = poisoned_conns.clone();
256 let events = events.clone();
257 let wire_log = wire_log.clone();
258 wire_log.lock().unwrap().push(RecordedEvent::NewConnection);
259 let io = TokioIo::new(stream);
260
261 let svc = service_fn(move |_req| {
262 let poisoned_conns = poisoned_conns.clone();
263 let events = events.clone();
264 let wire_log = wire_log.clone();
265 if poisoned_conns.lock().unwrap().contains(&remote_addr) {
266 tracing::error!("poisoned connection {:?} was reused!", &remote_addr);
267 panic!("poisoned connection was reused!");
268 }
269 let next_event = events.clone().lock().unwrap().pop();
270 async move {
271 let next_event = next_event
272 .unwrap_or_else(|| panic!("no more events! Log: {wire_log:?}"));
273
274 wire_log
275 .lock()
276 .unwrap()
277 .push(RecordedEvent::Response(next_event.clone()));
278
279 if next_event == ReplayedEvent::Timeout {
280 tracing::info!("{} is poisoned", remote_addr);
281 poisoned_conns.lock().unwrap().insert(remote_addr);
282 }
283 tracing::debug!("replying with {:?}", next_event);
284 let event = generate_response_event(next_event).await;
285 dbg!(event)
286 }
287 });
288
289 let conn_builder = conn_builder.clone();
290 let graceful = graceful.clone();
291 tokio::spawn(async move {
292 let conn = conn_builder.serve_connection(io, svc);
293 let fut = graceful.watch(conn);
294 if let Err(e) = fut.await {
295 panic!("Error serving connection: {e:?}");
296 }
297 });
298 },
299 _ = &mut rx => {
300 tracing::info!("wire server: shutdown signalled");
301 graceful.shutdown().await;
302 tracing::info!("wire server: shutdown complete!");
303 break;
304 }
305 }
306 }
307 };
308
309 tokio::spawn(server);
310 Self {
311 event_log: wire_events,
312 bind_addr: listener_addr,
313 shutdown_hook: tx,
314 }
315 }
316
317 pub fn events(&self) -> Vec<RecordedEvent> {
319 self.event_log.lock().unwrap().clone()
320 }
321
322 fn bind_addr(&self) -> SocketAddr {
323 self.bind_addr
324 }
325
326 pub fn dns_resolver(&self) -> LoggingDnsResolver {
327 let event_log = self.event_log.clone();
328 let bind_addr = self.bind_addr;
329 LoggingDnsResolver(InnerDnsResolver {
330 log: event_log,
331 socket_addr: bind_addr,
332 })
333 }
334
335 pub fn http_client(&self) -> SharedHttpClient {
339 let resolver = self.dns_resolver();
340 crate::client::build_with_tcp_conn_fn(None, None, None, move || {
341 hyper_util::client::legacy::connect::HttpConnector::new_with_resolver(
342 resolver.clone().0,
343 )
344 })
345 }
346
347 pub fn endpoint_url(&self) -> String {
351 format!(
352 "http://this-url-is-converted-to-localhost.com:{}",
353 self.bind_addr().port()
354 )
355 }
356
357 pub fn shutdown(self) {
359 let _ = self.shutdown_hook.send(());
360 }
361}
362
363async fn generate_response_event(
364 event: ReplayedEvent,
365) -> Result<http_1x::Response<Full<Bytes>>, Infallible> {
366 let resp = match event {
367 ReplayedEvent::HttpResponse { status, body } => http_1x::Response::builder()
368 .status(status)
369 .body(Full::new(body))
370 .unwrap(),
371 ReplayedEvent::Timeout => {
372 Never::new().await;
373 unreachable!()
374 }
375 };
376 Ok::<_, Infallible>(resp)
377}
378
379#[derive(Clone, Debug)]
383pub struct LoggingDnsResolver(InnerDnsResolver);
384
385#[derive(Clone, Debug)]
387struct InnerDnsResolver {
388 log: Arc<Mutex<Vec<RecordedEvent>>>,
389 socket_addr: SocketAddr,
390}
391
392impl tower::Service<Name> for InnerDnsResolver {
393 type Response = Once<SocketAddr>;
394 type Error = Infallible;
395 type Future = BoxFuture<'static, Self::Response, Self::Error>;
396
397 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
398 Poll::Ready(Ok(()))
399 }
400
401 fn call(&mut self, req: Name) -> Self::Future {
402 let socket_addr = self.socket_addr;
403 let log = self.log.clone();
404 Box::pin(async move {
405 println!("looking up {req:?}, replying with {socket_addr:?}");
406 log.lock()
407 .unwrap()
408 .push(RecordedEvent::DnsLookup(req.to_string()));
409 Ok(std::iter::once(socket_addr))
410 })
411 }
412}
413
414#[cfg(all(feature = "legacy-test-util", feature = "hyper-014"))]
415impl hyper_0_14::service::Service<hyper_0_14::client::connect::dns::Name> for LoggingDnsResolver {
416 type Response = Once<SocketAddr>;
417 type Error = Infallible;
418 type Future = BoxFuture<'static, Self::Response, Self::Error>;
419
420 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
421 self.0.poll_ready(cx)
422 }
423
424 fn call(&mut self, req: hyper_0_14::client::connect::dns::Name) -> Self::Future {
425 use std::str::FromStr;
426 let adapter = Name::from_str(req.as_str()).expect("valid conversion");
427 self.0.call(adapter)
428 }
429}