tokiotest_httpserver/
lib.rs

1#![doc = include_str!("../README.md")]
2pub mod handler;
3
4use std::collections::BinaryHeap;
5use std::future::Future;
6use std::net::{SocketAddr};
7use test_context::AsyncTestContext;
8use tokio::sync::oneshot::{Receiver, Sender};
9use tokio::task::JoinHandle;
10use hyper::{Server, StatusCode, Uri};
11use hyper::service::{make_service_fn, service_fn};
12use lazy_static::lazy_static;
13use std::sync::Mutex;
14use std::sync::Arc;
15use queues::{Queue, IsQueue, queue};
16use crate::handler::{default_handle, HandlerCallback};
17use std::env;
18
19pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
20pub static TOKIOTEST_HTTP_PORT_ENV: &str = "TOKIOTEST_HTTP_PORT";
21
22lazy_static! {
23    static ref PORTS: Mutex<BinaryHeap<u16>> = Mutex::new(BinaryHeap::from((12300u16..12400u16).collect::<Vec<u16>>()));
24}
25
26pub fn take_port() -> u16 {
27    PORTS.lock().unwrap().pop().unwrap()
28}
29pub fn release_port(port: u16) {
30    PORTS.lock().unwrap().push(port)
31}
32
33#[allow(dead_code)]
34pub struct HttpTestContext {
35    pub port: u16,
36    pub handlers: Arc<Mutex<Queue<HandlerCallback>>>,
37    server_handler: JoinHandle<Result<(), hyper::Error>>,
38    sender: Sender<()>,
39}
40
41impl HttpTestContext {
42    pub fn add(&mut self, handler: HandlerCallback) {
43        self.handlers.lock().unwrap().add(handler).unwrap();
44    }
45
46    pub fn uri(&self, path: &str) -> Uri {
47        format!("http://{}:{}{}", "localhost", self.port, path).parse::<Uri>().unwrap()
48    }
49}
50
51pub async fn run_service(addr: SocketAddr, rx: Receiver<()>,
52    handlers: Arc<Mutex<Queue<HandlerCallback>>>) -> impl Future<Output = Result<(), hyper::Error>> {
53
54    let new_service = make_service_fn(move |_| {
55        let cloned_handlers = handlers.clone();
56        async {
57            Ok::<_, Error>(service_fn(move |req| {
58                match cloned_handlers.lock() {
59                    Ok(mut handlers_rw) => {
60                        match handlers_rw.remove() {
61                            Ok(handler) => { handler(req) }
62                            Err(_err) => { Box::pin(default_handle(req)) }
63                        }
64                    }
65                    Err(_err_lock) => Box::pin(default_handle(req))
66                }
67            }))
68        }
69    });
70    Server::bind(&addr).serve(new_service).with_graceful_shutdown(async { rx.await.ok(); })
71}
72
73#[async_trait::async_trait]
74impl AsyncTestContext for HttpTestContext {
75    async fn setup() -> HttpTestContext {
76        let port: u16 = match env::var(TOKIOTEST_HTTP_PORT_ENV) {
77            Ok(port_str) => port_str.parse::<u16>().unwrap(),
78            Err(_e) => take_port()
79        };
80        let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), port);
81        let (sender, receiver) = tokio::sync::oneshot::channel::<()>();
82        let handlers: Arc<Mutex<Queue<HandlerCallback>>> = Arc::new(Mutex::new(queue![]));
83        let server_handler = tokio::spawn(run_service(addr, receiver, handlers.clone()).await);
84        HttpTestContext {
85            server_handler,
86            sender,
87            port,
88            handlers
89        }
90    }
91
92    async fn teardown(self) {
93        let _ = self.sender.send(()).unwrap();
94        let _ = tokio::join!(self.server_handler);
95        release_port(self.port);
96    }
97}
98
99#[cfg(test)]
100mod test {
101    use hyper::{StatusCode, Method, Request, Body, HeaderMap, Client};
102    use crate::{HttpTestContext};
103    use test_context::test_context;
104    use crate::handler::HandlerBuilder;
105
106    #[test_context(HttpTestContext)]
107    #[tokio::test]
108    async fn test_get_without_expect_should_send_500(ctx: &mut HttpTestContext) {
109        let resp = Client::new().get(ctx.uri("/whatever")).await.unwrap();
110        assert_eq!(500, resp.status());
111    }
112
113    #[test_context(HttpTestContext)]
114    #[tokio::test]
115    async fn test_get_respond_404(ctx: &mut HttpTestContext) {
116        ctx.add(HandlerBuilder::new("/unknown").status_code(StatusCode::NOT_FOUND).build());
117
118        let resp = Client::new().get(ctx.uri("/unknown")).await.unwrap();
119
120        assert_eq!(404, resp.status());
121    }
122
123    #[test_context(HttpTestContext)]
124    #[tokio::test]
125    async fn test_get_endpoint(ctx: &mut HttpTestContext) {
126        ctx.add(HandlerBuilder::new("/foo").status_code(StatusCode::OK).build());
127
128        let resp = Client::new().get(ctx.uri("/foo")).await.unwrap();
129        assert_eq!(200, resp.status());
130
131        let resp = Client::new().get(ctx.uri("/foo")).await.unwrap();
132        assert_eq!(500, resp.status());
133    }
134
135    #[test_context(HttpTestContext)]
136    #[tokio::test]
137    async fn test_get_with_headers(ctx: &mut HttpTestContext) {
138        let mut headers = HeaderMap::new();
139        headers.append("foo", "bar".parse().unwrap());
140        ctx.add(HandlerBuilder::new("/headers").status_code(StatusCode::OK).headers(headers.clone()).build());
141        ctx.add(HandlerBuilder::new("/headers").status_code(StatusCode::OK).headers(headers).build());
142
143        let resp = Client::new().get(ctx.uri("/headers")).await.unwrap();
144        assert_eq!(500, resp.status());
145
146        let req = Request::builder().method(Method::GET).uri(ctx.uri("/headers")).header("foo", "bar").body(Body::empty()).unwrap();
147        let resp = Client::new().request(req).await.unwrap();
148        assert_eq!(200, resp.status());
149    }
150
151    #[test_context(HttpTestContext)]
152    #[tokio::test]
153    async fn test_post_endpoint(ctx: &mut HttpTestContext) {
154        ctx.add(HandlerBuilder::new("/bar")
155            .status_code(StatusCode::OK)
156            .method(Method::POST).build());
157
158        let req = Request::builder()
159            .method(Method::POST)
160            .uri(ctx.uri("/bar"))
161            .body(Body::from("foo=bar"))
162            .expect("request builder");
163
164        let resp = Client::new().request(req).await.unwrap();
165
166        assert_eq!(200, resp.status());
167    }
168}