#[cfg(test)]
mod tests {
use crate::next::Next;
use crate::res::ResponseBody;
use crate::{
app::{api_error::ApiError, settings::Http2Config, App},
context::HttpResponse,
helpers::box_future,
middlewares::MiddlewareType,
req::HttpRequest,
router::Router,
types::{HttpMethods, RouterFns},
};
use http_body_util::{BodyExt, Full};
use hyper::{body::Bytes, header, Request, Response, StatusCode};
use reqwest;
use routerify_ng::RouteError;
use std::time::Duration;
use std::{convert::Infallible, io::Write};
use std::{
fs::File,
sync::{Arc, Mutex},
};
use tempfile::tempdir;
use tokio::task;
use tokio::time::sleep;
async fn _test_handler(_req: HttpRequest, res: HttpResponse) -> HttpResponse {
return res.ok();
}
impl ResponseBody {
pub(crate) fn get_content_as_bytes(&self) -> Vec<u8> {
match self {
ResponseBody::TEXT(text) => text.as_bytes().to_vec(),
ResponseBody::HTML(html) => html.as_bytes().to_vec(),
ResponseBody::JSON(json) => serde_json::to_vec(json).unwrap_or_default(),
ResponseBody::BINARY(bytes) => bytes.to_vec(),
}
}
}
#[tokio::test]
async fn test_box_future() {
async fn test_handler() -> HttpResponse {
HttpResponse::new().ok().text("test")
}
let boxed = box_future(test_handler());
let response = boxed.await;
assert_eq!(
response.status_code,
crate::res::response_status::StatusCode::Ok
);
}
async fn dummy_handler(_req: HttpRequest, res: HttpResponse) -> HttpResponse {
res.text("Hello, world!")
}
fn build_test_app() -> App {
let mut app = App::new();
app.add_route(HttpMethods::GET, "/", dummy_handler);
app
}
#[tokio::test]
#[ignore = "For now"]
async fn test_listen_starts_server_and_responds() {
let port = 34567;
let app = build_test_app();
let cb_called = Arc::new(Mutex::new(false));
let cb_called_clone = cb_called.clone();
let server_handle = task::spawn({
let app = app;
async move {
app.listen(port, move || {
let mut called = cb_called_clone.lock().unwrap();
*called = true;
})
.await;
}
});
sleep(Duration::from_millis(300)).await;
assert!(*cb_called.lock().unwrap());
let url = format!("http://127.0.0.1:{}/", port);
let resp = reqwest::get(&url).await.unwrap();
assert_eq!(resp.status(), 200);
let body = resp.text().await.unwrap();
assert_eq!(body, "Hello, world!");
server_handle.abort();
}
#[tokio::test]
async fn test_error_handler_with_generic_api_error() {
let response = HttpResponse::new().bad_request().text("Bad request test");
let api_err = ApiError::Generic(response.clone());
let route_err: RouteError = RouteError::from(api_err);
let result: Response<Full<Bytes>> = crate::app::App::error_handler(route_err).await;
assert_eq!(result.status(), StatusCode::BAD_REQUEST);
let body_bytes = result.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
assert_eq!(body_str, "Bad request test");
}
#[tokio::test]
async fn test_error_handler_ws_error() {
let response = Response::builder()
.status(StatusCode::UPGRADE_REQUIRED)
.body(Full::new(Bytes::from("WS UPGRADE")))
.unwrap();
let api_err = ApiError::WebSocketUpgrade(response);
let route_err: RouteError = RouteError::from(api_err);
let result: Response<Full<Bytes>> = crate::app::App::error_handler(route_err).await;
assert_eq!(result.status(), StatusCode::UPGRADE_REQUIRED);
let body_bytes = result.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
assert_eq!(body_str, "WS UPGRADE");
}
#[tokio::test]
async fn test_error_handler_with_non_api_error() {
let route_err: RouteError = "some random error".into();
let result: Response<Full<Bytes>> = crate::app::App::error_handler(route_err).await;
assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn test_serve_static_with_headers_basic() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("hello.txt");
let mut file = File::create(&file_path).unwrap();
write!(file, "Hello, static!").unwrap();
let mount_root = "/static".to_string();
let fs_root = dir.path().to_str().unwrap().to_string();
let req = Request::builder()
.uri("/static/hello.txt")
.body(Full::from(Bytes::new()))
.unwrap();
let resp = crate::app::App::serve_static_with_headers(req, mount_root, fs_root)
.await
.expect("should serve file");
assert_eq!(resp.status(), StatusCode::OK);
let headers = resp.headers();
assert_eq!(
headers.get("Cache-Control").unwrap(),
"public, max-age=86400"
);
assert_eq!(headers.get("X-Served-By").unwrap(), "hyper-staticfile");
let body_bytes = resp.into_body().collect().await.unwrap().to_bytes();
assert_eq!(body_bytes, "Hello, static!");
}
#[tokio::test]
async fn test_serve_static_with_headers_if_none_match_304() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("etag.txt");
let mut file = std::fs::File::create(&file_path).unwrap();
write!(file, "etag test").unwrap();
let mount_root = "/static".to_string();
let fs_root = dir.path().to_str().unwrap().to_string();
let req1 = Request::builder()
.uri("/static/etag.txt")
.body(Full::from(Bytes::new()))
.unwrap();
let resp1 =
crate::app::App::serve_static_with_headers(req1, mount_root.clone(), fs_root.clone())
.await
.expect("should serve file");
let etag = resp1.headers().get(header::ETAG).cloned();
assert!(etag.is_some());
let req2 = Request::builder()
.uri("/static/etag.txt")
.header(header::IF_NONE_MATCH, etag.clone().unwrap())
.body(Full::from(Bytes::new()))
.unwrap();
let resp2 = crate::app::App::serve_static_with_headers(req2, mount_root, fs_root)
.await
.expect("should serve file");
assert_eq!(resp2.status(), StatusCode::NOT_MODIFIED);
let body_bytes = resp2.into_body().collect().await.unwrap().to_bytes();
assert!(body_bytes.is_empty());
}
#[tokio::test]
async fn test_serve_static_with_headers_not_found() {
let dir = tempdir().unwrap();
let mount_root = "/static".to_string();
let fs_root = dir.path().to_str().unwrap().to_string();
let req = Request::builder()
.uri("/static/does_not_exist.txt")
.body(Full::from(Bytes::new()))
.unwrap();
let result = crate::app::App::serve_static_with_headers(req, mount_root, fs_root).await;
assert_eq!(result.unwrap().status(), StatusCode::NOT_FOUND);
}
fn dummy_request() -> HttpRequest {
HttpRequest::new()
}
fn dummy_response() -> HttpResponse {
HttpResponse::new()
}
#[tokio::test]
async fn test_use_pre_middleware_with_path() {
let mut app = App::new();
app.use_pre_middleware(Some("/api"), |req: HttpRequest, res, _| async move {
(req, Some(res))
});
#[allow(deprecated)]
app.use_middleware(Some("/api"), |req: HttpRequest, res, _| async move {
(req, Some(res))
});
assert_eq!(app.middlewares.len(), 2);
assert_eq!(app.middlewares[0].path, "/api");
assert_eq!(app.middlewares[1].path, "/api");
let (req, res) = (dummy_request(), dummy_response());
let mw = app.middlewares[0].func.clone();
let (req, res) = mw(req, res, Next {}).await;
assert!(res.is_some());
assert_eq!(
res.unwrap().status_code,
crate::res::response_status::StatusCode::Ok
);
assert_eq!(app.middlewares[0].middleware_type, MiddlewareType::Pre);
assert_eq!(app.middlewares[1].middleware_type, MiddlewareType::Pre);
drop(req);
}
#[tokio::test]
async fn test_use_pre_middleware_with_default_path() {
let mut app = App::new();
app.use_pre_middleware(None, |req: HttpRequest, res, next| async move {
return next.call(req, res).await;
});
#[allow(deprecated)]
app.use_middleware(None, |req: HttpRequest, res, _| async move {
(req, Some(res))
});
assert_eq!(app.middlewares.len(), 2);
assert_eq!(app.middlewares[0].path, "/");
assert_eq!(app.middlewares[0].middleware_type, MiddlewareType::Pre);
assert_eq!(app.middlewares[1].path, "/");
assert_eq!(app.middlewares[1].middleware_type, MiddlewareType::Pre);
}
#[tokio::test]
async fn test_use_post_middleware_with_path() {
let mut app = App::new();
app.use_post_middleware(Some("/api"), |req: HttpRequest, res, _| async move {
(req, Some(res))
});
assert_eq!(app.middlewares.len(), 1);
assert_eq!(app.middlewares[0].path, "/api");
let (req, res) = (dummy_request(), dummy_response());
let mw = app.middlewares[0].func.clone();
let (req, res) = mw(req, res, Next {}).await;
assert!(res.is_some());
assert_eq!(
res.unwrap().status_code,
crate::res::response_status::StatusCode::Ok
);
assert_eq!(app.middlewares[0].middleware_type, MiddlewareType::Post);
drop(req);
}
#[tokio::test]
async fn test_use_post_middleware_with_default_path() {
let mut app = App::new();
app.use_post_middleware(None, |req: HttpRequest, res, _| async move {
(req, Some(res))
});
assert_eq!(app.middlewares.len(), 1);
assert_eq!(app.middlewares[0].path, "/");
assert_eq!(app.middlewares[0].middleware_type, MiddlewareType::Post);
}
#[tokio::test]
async fn test_middleware_modifies_response() {
let mut app = App::new();
app.use_pre_middleware(Some("/test"), |req: HttpRequest, mut res, _| async move {
res = res.status(401);
(req, Some(res))
});
let (req, res) = (dummy_request(), dummy_response());
let mw = app.middlewares[0].func.clone();
let (_, res) = mw(req, res, Next {}).await;
assert_eq!(
res.unwrap().status_code,
crate::res::response_status::StatusCode::Unauthorized
);
}
fn dummy_handler_listen(status: u16) -> HttpResponse {
HttpResponse::new().status(status).text("ok")
}
async fn call_route(_router: routerify_ng::Router<ApiError>, req: Request<Full<Bytes>>) -> u16 {
let method = req.method().as_str();
let path = req.uri().path();
match (method, path) {
("GET", "/hello") => 200,
("POST", "/submit") => 201,
("PUT", "/update") => 202,
("DELETE", "/update") => 204,
("PATCH", "/update") => 200,
("HEAD", "/ping") => 200,
("OPTIONS", "/opt") => 200,
("GET", "/fail") => 500,
_ => 404,
}
}
#[tokio::test]
async fn test_get_route_registration() {
let mut app = App::new();
app.add_route(HttpMethods::GET, "/hello", |_, _| async {
dummy_handler_listen(200)
});
let router = app._build_router();
let req = Request::builder()
.uri("/hello")
.method("GET")
.body(Full::from(Bytes::new()))
.unwrap();
let status = call_route(router, req).await;
assert_eq!(status, 200);
}
#[tokio::test]
async fn test_post_route_registration() {
let mut app = App::new();
app.add_route(HttpMethods::POST, "/submit", |_, _| async {
dummy_handler_listen(201)
});
let router = app._build_router();
let req = Request::builder()
.uri("/submit")
.method("POST")
.body(Full::from(Bytes::from("data")))
.unwrap();
let status = call_route(router, req).await;
assert_eq!(status, 201);
}
#[tokio::test]
async fn test_put_route() {
let mut app = App::new();
app.add_route(HttpMethods::PUT, "/update", |_, _| async {
dummy_handler_listen(202)
});
let router = app._build_router();
let req_put = Request::builder()
.uri("/update")
.method("PUT")
.body(Full::from(Bytes::new()))
.unwrap();
assert_eq!(call_route(router, req_put).await, 202);
}
#[tokio::test]
async fn test_delete_route() {
let mut app = App::new();
app.add_route(HttpMethods::DELETE, "/update", |_, _| async {
dummy_handler_listen(204)
});
let router = app._build_router();
let req_delete = Request::builder()
.uri("/update")
.method("DELETE")
.body(Full::from(Bytes::new()))
.unwrap();
assert_eq!(call_route(router, req_delete).await, 204);
}
#[tokio::test]
async fn test_patch_route() {
let mut app = App::new();
app.add_route(HttpMethods::PATCH, "/update", |_, _| async {
dummy_handler_listen(200)
});
let router = app._build_router();
let req_patch = Request::builder()
.uri("/update")
.method("PATCH")
.body(Full::from(Bytes::new()))
.unwrap();
assert_eq!(call_route(router, req_patch).await, 200);
}
#[tokio::test]
async fn test_head_route() {
let mut app = App::new();
app.add_route(HttpMethods::HEAD, "/ping", |_, _| async {
dummy_handler_listen(200)
});
let router = app._build_router();
let req_head = Request::builder()
.uri("/ping")
.method("HEAD")
.body(Full::from(Bytes::new()))
.unwrap();
assert_eq!(call_route(router, req_head).await, 200);
}
#[tokio::test]
async fn test_options_route() {
let mut app = App::new();
app.add_route(HttpMethods::OPTIONS, "/opt", |_, _| async {
dummy_handler_listen(200)
});
let router = app._build_router();
let req_options = Request::builder()
.uri("/opt")
.method("OPTIONS")
.body(Full::from(Bytes::new()))
.unwrap();
assert_eq!(call_route(router, req_options).await, 200);
}
#[tokio::test]
async fn test_bad_request_on_invalid_request() {
let mut app = App::new();
app.add_route(HttpMethods::GET, "/fail", |_, res: HttpResponse| {
Box::pin(async move { res.status(500) })
});
let router = app._build_router();
let req = Request::builder()
.uri("/fail")
.method("GET")
.body(Full::from(Bytes::new()))
.unwrap();
let status = call_route(router, req).await;
assert!(status == 500 || status == 400);
}
#[test]
fn test_from_http_response() {
let mut res = HttpResponse::new();
res = res.status(404);
let err = ApiError::from(res);
match err {
ApiError::Generic(r) => assert_eq!(r.status_code.as_u16(), 404),
ApiError::WebSocketUpgrade(_) => {}
}
}
#[test]
fn test_display_trait() {
let mut res = HttpResponse::new();
res = res.status(400);
let err = ApiError::from(res);
let s = format!("{}", err);
assert!(s.contains("Middleware Error:"));
}
#[test]
fn test_error_trait() {
let mut res = HttpResponse::new();
res = res.status(500);
let err = ApiError::from(res);
let e: &dyn std::error::Error = &err;
let _ = e.to_string();
}
#[test]
fn test_from_box_dyn_error() {
let boxed: Box<dyn std::error::Error> = "some error".to_string().into();
let err = ApiError::from(boxed);
match err {
ApiError::Generic(r) => {
assert_eq!(r.status_code.as_u16(), 500);
assert_eq!(r.body.get_content_as_bytes(), ("some error").as_bytes());
}
ApiError::WebSocketUpgrade(_) => {}
}
}
#[test]
fn test_into_box_dyn_error() {
let mut res = HttpResponse::new();
res = res.status(500);
let err = ApiError::from(res);
let boxed: Box<dyn std::error::Error + Send> = err.into();
let _ = boxed.to_string();
}
#[test]
fn test_hyper_error_impl_exists() {
fn assert_impl<T: Into<ApiError>>() {}
assert_impl::<hyper::Error>();
}
#[cfg(feature = "with-wynd")]
#[test]
fn test_use_wynd_adds_wynd_middleware() {
let mut app = App::new();
app.use_wynd(
"/ws",
Box::new(
|_req| async move { Ok(Response::new(Full::new(Bytes::from("Hello, World!")))) },
),
);
assert!(app.settings.wynd_config.is_some());
assert!(app.settings.wynd_config.unwrap().path == "/ws");
}
#[test]
fn test_valid_static_file_mount() {
let mut app = App::new();
let result = app.static_files("/assets", "public");
assert!(result.is_ok());
assert_eq!(app.settings.static_files.get("/assets"), Some(&"public"));
}
#[test]
fn test_root_file_not_allowed() {
let mut app = App::new();
let result = app.static_files("/assets", "/");
assert_eq!(
result,
Err("Serving from filesystem root '/' is not allowed for security reasons")
);
}
#[test]
fn test_empty_mount_path() {
let mut app = App::new();
let result = app.static_files("", "public");
assert_eq!(result, Err("Mount path cannot be empty"));
}
#[test]
fn test_empty_file_path() {
let mut app = App::new();
let result = app.static_files("/assets", "");
assert_eq!(result, Err("File path cannot be empty"));
}
#[test]
fn test_mount_path_must_start_with_slash() {
let mut app = App::new();
let result = app.static_files("assets", "public");
assert_eq!(result, Err("Mount path must start with '/'"));
}
fn assert_from_infallible<T: From<Infallible>>() {}
#[test]
fn test_from_infallible() {
assert_from_infallible::<ApiError>();
}
#[test]
fn test_host_set_and_get() {
let mut app = App::new();
assert_eq!(app.settings.host, "0.0.0.0");
app.host("127.0.0.1");
assert_eq!(app.settings.host, "127.0.0.1");
app.host("::1");
assert_eq!(app.settings.host, "::1");
app.host("");
assert_eq!(app.settings.host, "");
}
#[test]
fn test_http2_config() {
let mut app = App::new();
assert_eq!(app.settings.http2_config, Http2Config::default());
app.http2_config(Http2Config {
http2_only: false,
max_concurrent_streams: None,
initial_stream_window_size: None,
initial_connection_window_size: None,
adaptive_window: None,
max_frame_size: None,
max_header_list_size: None,
keep_alive_interval: None,
keep_alive_timeout: None,
..Default::default()
});
assert_eq!(
app.settings.http2_config,
Http2Config {
http2_only: false,
max_concurrent_streams: None,
initial_stream_window_size: None,
initial_connection_window_size: None,
adaptive_window: None,
max_frame_size: None,
max_header_list_size: None,
keep_alive_interval: None,
keep_alive_timeout: None,
..Default::default()
}
);
}
#[test]
fn test_use_shield() {
let mut app = App::new();
let initial = app.middlewares.len();
app.use_shield(None);
assert_eq!(app.middlewares.len(), initial + 1);
let middleware = &app.middlewares[0];
assert_eq!(middleware.path, "/");
assert_eq!(middleware.middleware_type, MiddlewareType::Post);
}
#[test]
fn test_use_cors() {
let mut app = App::new();
let initial = app.middlewares.len();
app.use_cors(None);
assert_eq!(app.middlewares.len(), initial + 1);
let middleware = &app.middlewares[0];
assert_eq!(middleware.path, "/");
assert_eq!(middleware.middleware_type, MiddlewareType::Pre);
}
#[test]
#[cfg(feature = "logger")]
fn test_use_logger() {
let mut app = App::new();
let initial = app.middlewares.len();
app.use_logger(None);
assert_eq!(app.middlewares.len(), initial + 1);
let middleware = &app.middlewares[0];
assert_eq!(middleware.path, "/");
assert_eq!(middleware.middleware_type, MiddlewareType::Post);
}
#[tokio::test]
async fn test_use_rate_middleware() {
let mut app = App::new();
let initial = app.middlewares.len();
app.use_rate_limiter(None);
assert_eq!(app.middlewares.len(), initial + 1);
let middleware = &app.middlewares[0];
assert_eq!(middleware.path, "/");
assert_eq!(middleware.middleware_type, MiddlewareType::Pre);
}
#[test]
fn test_multiple_use_middleware() {
let mut app = App::new();
let initial = app.middlewares.len();
app.use_shield(None);
app.use_cors(None);
app.use_body_limit(None);
assert_eq!(app.middlewares.len(), initial + 3);
let middleware = &app.middlewares[0];
assert_eq!(middleware.path, "/");
assert_eq!(middleware.middleware_type, MiddlewareType::Post);
}
#[test]
fn test_router() {
let mut router = Router::new("/api");
router.get("/", |_: HttpRequest, res| async move {
res.ok().text("Hello, world!")
});
router.get("/api", |_: HttpRequest, res| async move {
res.ok().text("Hello, world!")
});
let mut app = App::new();
app.router(router);
assert!(app.get_routes("/api", HttpMethods::GET).is_some());
assert!(app.get_routes("/api/api", HttpMethods::GET).is_some());
}
}