yup_hyper_mock/
lib.rs

1//! Contains various utility types and macros useful for testing hyper clients.
2//!
3//! # Macros
4//! The `mock_connector!` and `mock_connector_in_order!` macros can be used to
5//! feed a client with preset data. That way you can control exactly what it will
6//! see, confining the test-case to its own sandbox that way.
7//!
8//! All types they define are public to allow them to be used in other unit-tests.
9//! Please note that integration tests cannot share their mock types anyway, as each
10//! integration test goes into its own binary.
11//!
12//! # Usage
13//!
14//! Set it up for use in tests in `Cargo.toml`
15//!
16//! ```toml
17//! [dev-dependencies]
18//! yup-hyper-mock = "*"
19//! log = "*"  # log macros are used within yup-hyper-mock
20//! ```
21//!
22//! Link it into your `src/(lib.rs|main.rs)`
23//!
24//! ```Rust
25//! #[cfg(test)] #[macro_use]
26//! extern crate "yup-hyper-mock" as hyper_mock
27//! ```
28
29use log::debug;
30
31use std::collections::HashMap;
32use std::pin::Pin;
33use std::sync::Arc;
34use std::task::{Context, Poll};
35
36use futures;
37use futures::lock::Mutex;
38use futures::Future;
39use hyper::Uri;
40use tower_service::Service;
41
42mod streams;
43pub use crate::streams::MockPollStream;
44
45/// This macro maps host URLs to a respective reply, which is given in plain-text.
46/// It ignores everything that is written to it.
47#[macro_export]
48macro_rules! mock_connector (
49    ($name:ident {
50        $($url:expr => $res:expr)*
51    }) => (
52        #[derive(Clone)]
53        pub struct $name($crate::HostToReplyConnector);
54
55        impl Default for $name {
56            fn default() -> Self {
57                let mut c = Self(Default::default());
58                $(c.0.m.insert($url.to_string(), $res.to_string());)*
59                c
60            }
61        }
62
63        impl tower_service::Service<hyper::Uri> for $name {
64            type Response = $crate::MockPollStream;
65            type Error = std::io::Error;
66            type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
67
68            fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
69                self.0.poll_ready(cx)
70            }
71
72            fn call(&mut self, req: hyper::Uri) -> Self::Future {
73                self.0.call(req)
74            }
75        }
76    )
77);
78
79/// A `Connect` which provides a single reply stream per host.
80///
81/// The mapping is done from full host url (e.g. `http://host.name.com`) to the
82/// singular reply the host is supposed to make.
83#[derive(Default, Clone)]
84pub struct HostToReplyConnector {
85    pub m: HashMap<String, String>,
86}
87
88impl Service<Uri> for HostToReplyConnector {
89    type Response = crate::MockPollStream;
90    type Error = std::io::Error;
91    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
92
93    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
94        Poll::Ready(Ok(()))
95    }
96
97    fn call(&mut self, req: Uri) -> Self::Future {
98        debug!("HostToReplyConnector::connect({:?})", req);
99
100        match (|| {
101            // ignore port for now
102            self.m.get(&format!("{}://{}", req.scheme()?, req.host()?))
103        })() {
104            Some(res) => Box::pin(futures::future::ok(MockPollStream::new(
105                res.clone().into_bytes(),
106            ))),
107            None => panic!("HostToReplyConnector doesn't know url {}", req),
108        }
109    }
110}
111
112/// This macro yields all given server replies in the order they are given.
113/// The destination host URL doesn't matter at all.
114#[macro_export]
115macro_rules! mock_connector_in_order (
116    ($name:ident {
117        $( $res:expr )*
118    }) => (
119        #[derive(Clone)]
120        pub struct $name($crate::SequentialConnector);
121
122        impl Default for $name {
123            fn default() -> $name {
124                Self($crate::SequentialConnector::new(vec![
125                    $($res.to_string(),)*
126                ]))
127            }
128        }
129
130        impl tower_service::Service<hyper::Uri> for $name {
131            type Response = $crate::MockPollStream;
132            type Error = std::io::Error;
133            type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
134
135            fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
136                self.0.poll_ready(cx)
137            }
138
139            fn call(&mut self, req: hyper::Uri) -> Self::Future {
140                self.0.call(req)
141            }
142        }
143    )
144);
145
146/// A connector which requires you to implement the `Default` trait, allowing you
147/// to determine the data it should be initialized with
148#[derive(Clone)]
149pub struct SequentialConnector {
150    pub content: Arc<[String]>,
151    pub current: Arc<Mutex<usize>>,
152}
153
154impl SequentialConnector {
155    pub fn new(content: impl Into<Box<[String]>>) -> Self {
156        let content = content.into();
157        assert!(
158            content.len() != 0,
159            "Not a single streamer return value specified"
160        );
161
162        SequentialConnector {
163            content: content.into(),
164            current: Arc::new(Mutex::new(0)),
165        }
166    }
167}
168
169impl Service<Uri> for SequentialConnector {
170    type Response = crate::MockPollStream;
171    type Error = std::io::Error;
172    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
173
174    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
175        Poll::Ready(Ok(()))
176    }
177
178    fn call(&mut self, req: Uri) -> Self::Future {
179        debug!("SequentialConnector::connect({:?})", req);
180
181        let content = self.content.clone();
182        let current = self.current.clone();
183        Box::pin(async move {
184            let mut current = current.lock().await;
185            let data = content[*current].clone().into_bytes();
186            *current = *current + 1;
187            Ok(MockPollStream::new(data))
188        })
189    }
190}