tokiotest-httpserver 0.2.1

A simple http server to make unit/integration tests
Documentation
#![doc = include_str!("../README.md")]
pub mod handler;

use std::collections::BinaryHeap;
use std::future::Future;
use std::net::{SocketAddr};
use test_context::AsyncTestContext;
use tokio::sync::oneshot::{Receiver, Sender};
use tokio::task::JoinHandle;
use hyper::{Server, StatusCode, Uri};
use hyper::service::{make_service_fn, service_fn};
use lazy_static::lazy_static;
use std::sync::Mutex;
use std::sync::Arc;
use queues::{Queue, IsQueue, queue};
use crate::handler::{default_handle, HandlerCallback};
use std::env;

pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
pub static TOKIOTEST_HTTP_PORT_ENV: &str = "TOKIOTEST_HTTP_PORT";

lazy_static! {
    static ref PORTS: Mutex<BinaryHeap<u16>> = Mutex::new(BinaryHeap::from((12300u16..12400u16).collect::<Vec<u16>>()));
}

pub fn take_port() -> u16 {
    PORTS.lock().unwrap().pop().unwrap()
}
pub fn release_port(port: u16) {
    PORTS.lock().unwrap().push(port)
}

#[allow(dead_code)]
pub struct HttpTestContext {
    pub port: u16,
    pub handlers: Arc<Mutex<Queue<HandlerCallback>>>,
    server_handler: JoinHandle<Result<(), hyper::Error>>,
    sender: Sender<()>,
}

impl HttpTestContext {
    pub fn add(&mut self, handler: HandlerCallback) {
        self.handlers.lock().unwrap().add(handler).unwrap();
    }

    pub fn uri(&self, path: &str) -> Uri {
        format!("http://{}:{}{}", "localhost", self.port, path).parse::<Uri>().unwrap()
    }
}

pub async fn run_service(addr: SocketAddr, rx: Receiver<()>,
    handlers: Arc<Mutex<Queue<HandlerCallback>>>) -> impl Future<Output = Result<(), hyper::Error>> {

    let new_service = make_service_fn(move |_| {
        let cloned_handlers = handlers.clone();
        async {
            Ok::<_, Error>(service_fn(move |req| {
                match cloned_handlers.lock() {
                    Ok(mut handlers_rw) => {
                        match handlers_rw.remove() {
                            Ok(handler) => { handler(req) }
                            Err(_err) => { Box::pin(default_handle(req)) }
                        }
                    }
                    Err(_err_lock) => Box::pin(default_handle(req))
                }
            }))
        }
    });
    Server::bind(&addr).serve(new_service).with_graceful_shutdown(async { rx.await.ok(); })
}

#[async_trait::async_trait]
impl AsyncTestContext for HttpTestContext {
    async fn setup() -> HttpTestContext {
        let port: u16 = match env::var(TOKIOTEST_HTTP_PORT_ENV) {
            Ok(port_str) => port_str.parse::<u16>().unwrap(),
            Err(_e) => take_port()
        };
        let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), port);
        let (sender, receiver) = tokio::sync::oneshot::channel::<()>();
        let handlers: Arc<Mutex<Queue<HandlerCallback>>> = Arc::new(Mutex::new(queue![]));
        let server_handler = tokio::spawn(run_service(addr, receiver, handlers.clone()).await);
        HttpTestContext {
            server_handler,
            sender,
            port,
            handlers
        }
    }

    async fn teardown(self) {
        let _ = self.sender.send(()).unwrap();
        let _ = tokio::join!(self.server_handler);
        release_port(self.port);
    }
}

#[cfg(test)]
mod test {
    use hyper::{StatusCode, Method, Request, Body, HeaderMap, Client};
    use crate::{HttpTestContext};
    use test_context::test_context;
    use crate::handler::HandlerBuilder;

    #[test_context(HttpTestContext)]
    #[tokio::test]
    async fn test_get_without_expect_should_send_500(ctx: &mut HttpTestContext) {
        let resp = Client::new().get(ctx.uri("/whatever")).await.unwrap();
        assert_eq!(500, resp.status());
    }

    #[test_context(HttpTestContext)]
    #[tokio::test]
    async fn test_get_respond_404(ctx: &mut HttpTestContext) {
        ctx.add(HandlerBuilder::new("/unknown").status_code(StatusCode::NOT_FOUND).build());

        let resp = Client::new().get(ctx.uri("/unknown")).await.unwrap();

        assert_eq!(404, resp.status());
    }

    #[test_context(HttpTestContext)]
    #[tokio::test]
    async fn test_get_endpoint(ctx: &mut HttpTestContext) {
        ctx.add(HandlerBuilder::new("/foo").status_code(StatusCode::OK).build());

        let resp = Client::new().get(ctx.uri("/foo")).await.unwrap();
        assert_eq!(200, resp.status());

        let resp = Client::new().get(ctx.uri("/foo")).await.unwrap();
        assert_eq!(500, resp.status());
    }

    #[test_context(HttpTestContext)]
    #[tokio::test]
    async fn test_get_with_headers(ctx: &mut HttpTestContext) {
        let mut headers = HeaderMap::new();
        headers.append("foo", "bar".parse().unwrap());
        ctx.add(HandlerBuilder::new("/headers").status_code(StatusCode::OK).headers(headers.clone()).build());
        ctx.add(HandlerBuilder::new("/headers").status_code(StatusCode::OK).headers(headers).build());

        let resp = Client::new().get(ctx.uri("/headers")).await.unwrap();
        assert_eq!(500, resp.status());

        let req = Request::builder().method(Method::GET).uri(ctx.uri("/headers")).header("foo", "bar").body(Body::empty()).unwrap();
        let resp = Client::new().request(req).await.unwrap();
        assert_eq!(200, resp.status());
    }

    #[test_context(HttpTestContext)]
    #[tokio::test]
    async fn test_post_endpoint(ctx: &mut HttpTestContext) {
        ctx.add(HandlerBuilder::new("/bar")
            .status_code(StatusCode::OK)
            .method(Method::POST).build());

        let req = Request::builder()
            .method(Method::POST)
            .uri(ctx.uri("/bar"))
            .body(Body::from("foo=bar"))
            .expect("request builder");

        let resp = Client::new().request(req).await.unwrap();

        assert_eq!(200, resp.status());
    }
}