aws_smithy_http_client/test_util/
capture_request.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
7use aws_smithy_runtime_api::client::http::{
8    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
9};
10use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
11use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
12use aws_smithy_runtime_api::http::HttpError;
13use aws_smithy_runtime_api::shared::IntoShared;
14use aws_smithy_types::body::SdkBody;
15use std::fmt::Debug;
16use std::sync::{Arc, Mutex};
17use tokio::sync::oneshot;
18
19#[derive(Debug)]
20struct Inner {
21    response: Option<HttpResponse>,
22    sender: Option<oneshot::Sender<HttpRequest>>,
23}
24
25/// Test Connection to capture a single request
26#[derive(Debug, Clone)]
27pub struct CaptureRequestHandler(Arc<Mutex<Inner>>);
28
29impl HttpConnector for CaptureRequestHandler {
30    fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
31        let mut inner = self.0.lock().unwrap();
32        if let Err(_e) = inner.sender.take().expect("already sent").send(request) {
33            tracing::trace!("The receiver was already dropped");
34        }
35        HttpConnectorFuture::ready(Ok(inner
36            .response
37            .take()
38            .expect("could not handle second request")))
39    }
40}
41
42impl HttpClient for CaptureRequestHandler {
43    fn http_connector(
44        &self,
45        _: &HttpConnectorSettings,
46        _: &RuntimeComponents,
47    ) -> SharedHttpConnector {
48        self.clone().into_shared()
49    }
50
51    fn connector_metadata(&self) -> Option<ConnectorMetadata> {
52        Some(ConnectorMetadata::new("capture-request-handler", None))
53    }
54}
55
56/// Receiver for [`CaptureRequestHandler`].
57#[derive(Debug)]
58pub struct CaptureRequestReceiver {
59    receiver: oneshot::Receiver<HttpRequest>,
60}
61
62impl CaptureRequestReceiver {
63    /// Expect that a request was sent. Returns the captured request.
64    ///
65    /// # Panics
66    /// If no request was received
67    #[track_caller]
68    pub fn expect_request(mut self) -> HttpRequest {
69        self.receiver.try_recv().expect("no request was received")
70    }
71
72    /// Expect that no request was captured. Panics if a request was received.
73    ///
74    /// # Panics
75    /// If a request was received
76    #[track_caller]
77    pub fn expect_no_request(mut self) {
78        self.receiver
79            .try_recv()
80            .expect_err("expected no request to be received!");
81    }
82}
83
84/// Test connection used to capture a single request
85///
86/// If response is `None`, it will reply with a 200 response with an empty body
87///
88/// Example:
89/// ```compile_fail
90/// let (capture_client, request) = capture_request(None);
91/// let conf = aws_sdk_sts::Config::builder()
92///     .http_client(capture_client)
93///     .build();
94/// let client = aws_sdk_sts::Client::from_conf(conf);
95/// let _ = client.assume_role_with_saml().send().await;
96/// // web identity should be unsigned
97/// assert_eq!(
98///     request.expect_request().headers().get("AUTHORIZATION"),
99///     None
100/// );
101/// ```
102pub fn capture_request(
103    response: Option<http_1x::Response<SdkBody>>,
104) -> (CaptureRequestHandler, CaptureRequestReceiver) {
105    capture_request_inner(response)
106}
107
108fn capture_request_inner(
109    response: Option<impl TryInto<HttpResponse, Error = HttpError>>,
110) -> (CaptureRequestHandler, CaptureRequestReceiver) {
111    let (tx, rx) = oneshot::channel();
112    let http_resp: HttpResponse = match response {
113        Some(resp) => resp.try_into().expect("valid HttpResponse"),
114        None => http_1x::Response::builder()
115            .status(200)
116            .body(SdkBody::empty())
117            .expect("unreachable")
118            .try_into()
119            .expect("unreachable"),
120    };
121    (
122        CaptureRequestHandler(Arc::new(Mutex::new(Inner {
123            response: Some(http_resp),
124            sender: Some(tx),
125        }))),
126        CaptureRequestReceiver { receiver: rx },
127    )
128}
129
130#[allow(missing_docs)]
131#[cfg(feature = "legacy-test-util")]
132pub fn legacy_capture_request(
133    response: Option<http_02x::Response<SdkBody>>,
134) -> (CaptureRequestHandler, CaptureRequestReceiver) {
135    capture_request_inner(response)
136}
137
138#[cfg(test)]
139mod test {
140    use aws_smithy_runtime_api::client::http::HttpConnector;
141    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
142    use aws_smithy_types::body::SdkBody;
143
144    #[cfg(feature = "legacy-test-util")]
145    #[tokio::test]
146    async fn test_can_plug_in_http_02x() {
147        use super::legacy_capture_request;
148        let (capture_client, _request) = legacy_capture_request(Some(
149            http_02x::Response::builder()
150                .status(202)
151                .body(SdkBody::empty())
152                .expect("unreachable"),
153        ));
154
155        let resp = capture_client.call(HttpRequest::empty()).await.unwrap();
156        assert_eq!(202, resp.status().as_u16());
157    }
158
159    #[tokio::test]
160    async fn test_can_plug_in_http_1x() {
161        use super::capture_request;
162        let (capture_client, _request) = capture_request(Some(
163            http_1x::Response::builder()
164                .status(202)
165                .body(SdkBody::empty())
166                .expect("unreachable"),
167        ));
168
169        let resp = capture_client.call(HttpRequest::empty()).await.unwrap();
170        assert_eq!(202, resp.status().as_u16());
171    }
172}