use std::net::SocketAddr;
use axum_test::{TestServer, TestServerConfig};
use tokio::net::TcpListener;
#[cfg(feature = "with-db")]
use crate::Error;
use crate::{
app::{AppContext, Hooks},
boot::{self, BootResult},
config::Server,
environment::Environment,
Result,
};
#[cfg(feature = "with-db")]
use std::ops::Deref;
#[cfg(feature = "with-db")]
pub struct BootResultWrapper {
inner: BootResult,
test_db: Box<dyn super::db::TestSupport>,
}
#[cfg(feature = "with-db")]
impl BootResultWrapper {
#[must_use]
pub fn new(boot: BootResult, test_db: Box<dyn super::db::TestSupport>) -> Self {
Self {
inner: boot,
test_db,
}
}
}
#[cfg(feature = "with-db")]
impl Deref for BootResultWrapper {
type Target = BootResult;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg(feature = "with-db")]
impl Drop for BootResultWrapper {
fn drop(&mut self) {
self.test_db.cleanup_db();
}
}
pub struct RequestConfig {
pub save_cookies: bool,
pub default_content_type: Option<String>,
pub default_scheme: String,
}
impl Default for RequestConfig {
fn default() -> Self {
RequestConfigBuilder::new().build()
}
}
pub struct RequestConfigBuilder {
save_cookies: bool,
default_content_type: Option<String>,
default_scheme: String,
}
impl RequestConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self {
save_cookies: false,
default_content_type: Some("application/json".to_string()),
default_scheme: "http".to_string(),
}
}
#[must_use]
pub fn save_cookies(mut self, save: bool) -> Self {
self.save_cookies = save;
self
}
#[must_use]
pub fn default_content_type<S: Into<String>>(mut self, content_type: S) -> Self {
self.default_content_type = Some(content_type.into());
self
}
#[must_use]
pub fn default_scheme<S: Into<String>>(mut self, scheme: S) -> Self {
self.default_scheme = scheme.into();
self
}
#[must_use]
pub fn build(self) -> RequestConfig {
RequestConfig {
save_cookies: self.save_cookies,
default_content_type: self.default_content_type,
default_scheme: self.default_scheme,
}
}
}
impl Default for RequestConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl From<RequestConfig> for TestServerConfig {
fn from(request_config: RequestConfig) -> Self {
Self {
default_content_type: request_config.default_content_type,
save_cookies: request_config.save_cookies,
..Default::default()
}
}
}
pub const TEST_PORT_SERVER: i32 = 5555;
pub const TEST_BINDING_SERVER: &str = "localhost";
#[must_use]
pub fn get_base_url_port(port: i32) -> String {
format!("http://{TEST_BINDING_SERVER}:{port}/")
}
pub async fn get_available_port() -> i32 {
let addr = format!("{TEST_BINDING_SERVER}:0");
let listener = TcpListener::bind(addr)
.await
.expect("Failed to bind to address");
i32::from(
listener
.local_addr()
.expect("Failed to get local address")
.port(),
)
}
pub async fn boot_test<H: Hooks>() -> Result<BootResult> {
let config = H::load_config(&Environment::Test).await?;
let boot = H::boot(boot::StartMode::ServerOnly, &Environment::Test, config).await?;
Ok(boot)
}
#[cfg(feature = "with-db")]
pub async fn boot_test_with_create_db<H: Hooks>() -> Result<BootResultWrapper> {
let mut config = H::load_config(&Environment::Test).await?;
let test_db = super::db::init_test_db_creation(&config.database.uri)?;
test_db.init_db().await;
config.database.uri = test_db.get_connection_str().to_string();
let boot = match H::boot(boot::StartMode::ServerOnly, &Environment::Test, config).await {
Ok(boot) => boot,
Err(err) => {
test_db.cleanup_db();
return Err(Error::string(&err.to_string()));
}
};
Ok(BootResultWrapper::new(boot, test_db))
}
pub async fn boot_test_unique_port<H: Hooks>(port: Option<i32>) -> Result<BootResult> {
let mut config = H::load_config(&Environment::Test).await?;
config.server = Server {
port: port.unwrap_or(TEST_PORT_SERVER),
binding: TEST_BINDING_SERVER.to_string(),
..config.server
};
H::boot(boot::StartMode::ServerOnly, &Environment::Test, config).await
}
#[allow(clippy::future_not_send)]
async fn request_internal<F, Fut>(callback: F, boot: &BootResult, test_server_config: RequestConfig)
where
F: FnOnce(TestServer, AppContext) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let routes = boot.router.clone().unwrap();
let server = TestServer::new_with_config(
routes.into_make_service_with_connect_info::<SocketAddr>(),
test_server_config,
)
.unwrap();
callback(server, boot.app_context.clone()).await;
}
#[allow(clippy::future_not_send)]
pub async fn request<H: Hooks, F, Fut>(callback: F)
where
F: FnOnce(TestServer, AppContext) -> Fut,
Fut: std::future::Future<Output = ()>,
{
request_with_config::<H, F, Fut>(RequestConfig::default(), callback).await;
}
#[allow(clippy::future_not_send)]
#[cfg(feature = "with-db")]
pub async fn request_with_create_db<H: Hooks, F, Fut>(callback: F)
where
F: FnOnce(TestServer, AppContext) -> Fut,
Fut: std::future::Future<Output = ()>,
{
request_config_with_create_db::<H, F, Fut>(RequestConfig::default(), callback).await;
}
pub async fn request_with_config<H: Hooks, F, Fut>(config: RequestConfig, callback: F)
where
F: FnOnce(TestServer, AppContext) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let boot: BootResult = boot_test::<H>().await.unwrap();
request_internal::<F, Fut>(callback, &boot, config).await;
}
#[allow(clippy::future_not_send)]
#[cfg(feature = "with-db")]
pub async fn request_config_with_create_db<H: Hooks, F, Fut>(config: RequestConfig, callback: F)
where
F: FnOnce(TestServer, AppContext) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let boot_wrapper: BootResultWrapper = boot_test_with_create_db::<H>().await.unwrap();
request_internal::<F, Fut>(callback, &boot_wrapper.inner, config).await;
}