use ::anyhow::Context;
use ::anyhow::Result;
use ::axum::body::Body;
use ::axum::http::Method;
use ::axum::http::Request;
use ::axum::routing::IntoMakeService;
use ::axum::Router;
use ::axum::Server;
use ::hyper::body::to_bytes;
use ::hyper::body::Bytes;
use ::hyper::header;
use ::hyper::Client;
use ::std::net::SocketAddr;
use ::std::net::TcpListener;
use ::tokio::spawn;
use ::tokio::task::JoinHandle;
use crate::util::new_random_socket_addr;
use crate::TestResponse;
pub struct TestServer {
server_thread: JoinHandle<()>,
server_address: String,
}
impl TestServer {
pub fn new(app: IntoMakeService<Router>) -> Self {
let addr = new_random_socket_addr().expect("Cannot create socket address for use");
let test_server = Self::new_with_address(app, addr).expect("Cannot create TestServer");
test_server
}
pub fn new_with_address(
app: IntoMakeService<Router>,
socket_address: SocketAddr,
) -> Result<Self> {
let listener = TcpListener::bind(socket_address)
.with_context(|| "Failed to create TCPListener for TestServer")?;
let server_address = socket_address.to_string();
let server = Server::from_tcp(listener)
.with_context(|| "Failed to create ::axum::Server for TestServer")?
.serve(app);
let server_thread = spawn(async move {
server.await.expect("Expect server to start serving");
});
let test_server = Self {
server_thread,
server_address,
};
Ok(test_server)
}
pub async fn get(&self, path: &str) -> TestResponse {
self.send(Method::GET, path, &"")
.await
.with_context(|| format!("Error calling get on path {}", path))
.unwrap()
.assert_status_ok()
}
pub async fn get_fail(&self, path: &str) -> TestResponse {
self.send(Method::GET, path, &"")
.await
.with_context(|| format!("Error calling get_fail on path {}", path))
.unwrap()
.assert_status_not_ok()
}
pub async fn post(&self, path: &str, body: &str) -> TestResponse {
self.send(Method::POST, path, body)
.await
.with_context(|| format!("Error calling post on path {}", path))
.unwrap()
.assert_status_ok()
}
pub async fn post_fail(&self, path: &str, body: &str) -> TestResponse {
self.send(Method::POST, path, body)
.await
.with_context(|| format!("Error calling post_fail on path {}", path))
.unwrap()
.assert_status_not_ok()
}
pub async fn patch(&self, path: &str, body: &str) -> TestResponse {
self.send(Method::PATCH, path, body)
.await
.with_context(|| format!("Error calling patch on path {}", path))
.unwrap()
.assert_status_ok()
}
pub async fn patch_fail(&self, path: &str, body: &str) -> TestResponse {
self.send(Method::PATCH, path, body)
.await
.with_context(|| format!("Error calling patch_fail on path {}", path))
.unwrap()
.assert_status_not_ok()
}
pub async fn put(&self, path: &str, body: &str) -> TestResponse {
self.send(Method::PUT, path, body)
.await
.with_context(|| format!("Error calling put on path {}", path))
.unwrap()
.assert_status_ok()
}
pub async fn put_fail(&self, path: &str, body: &str) -> TestResponse {
self.send(Method::PUT, path, body)
.await
.with_context(|| format!("Error calling put_fail on path {}", path))
.unwrap()
.assert_status_not_ok()
}
pub async fn delete(&self, path: &str) -> TestResponse {
self.send(Method::DELETE, path, &"")
.await
.with_context(|| format!("Error calling delete_fail on path {}", path))
.unwrap()
.assert_status_ok()
}
pub async fn delete_fail(&self, path: &str) -> TestResponse {
self.send(Method::DELETE, path, &"")
.await
.with_context(|| format!("Error calling delete_fail on path {}", path))
.unwrap()
.assert_status_not_ok()
}
async fn send(&self, method: Method, path: &str, body_str: &str) -> Result<TestResponse> {
let request_url = path.to_string();
let request_path = build_request_path(&self.server_address, path);
let client = Client::new();
let body_bytes = Bytes::copy_from_slice(body_str.as_bytes());
let body: Body = body_bytes.into();
let hyper_response = client
.request(
Request::builder()
.uri(request_path)
.header(header::CONTENT_TYPE, "application/json")
.method(method)
.body(body)
.expect("expect Request built to be valid"),
)
.await
.expect("Expect TestResponse to come back");
let (parts, response_body) = hyper_response.into_parts();
let response_bytes = to_bytes(response_body).await?;
let contents = String::from_utf8_lossy(&response_bytes).to_string();
let status_code = parts.status;
let response = TestResponse::new(request_url, contents, status_code);
Ok(response)
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.server_thread.abort();
}
}
fn build_request_path(root_path: &str, sub_path: &str) -> String {
if sub_path == "" {
return format!("http://{}", root_path.to_string());
}
if sub_path.starts_with("/") {
return format!("http://{}{}", root_path, sub_path);
}
format!("http://{}/{}", root_path, sub_path)
}