use std::path::PathBuf;
use std::sync::Arc;
use axum::Router;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
use tempfile::TempDir;
use testcontainers::core::{ContainerPort, WaitFor};
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, GenericImage, ImageExt};
use ares_api::routes;
use ares_api::state::AppState;
use ares_db::Database;
pub const TEST_API_KEY: &str = "test-secret-key";
pub struct TestApp {
pub router: Router,
pub schemas_dir: PathBuf,
_container: ContainerAsync<GenericImage>,
_tmp_dir: TempDir,
}
pub async fn setup_test_app() -> TestApp {
let tmp_dir = TempDir::new().expect("Failed to create temp dir");
let schemas_dir = tmp_dir.path().join("schemas");
std::fs::create_dir_all(&schemas_dir).expect("Failed to create schemas dir");
let container = start_postgres().await;
let pool = connect_to_container(&container).await;
let db = Database::from_pool(pool);
db.migrate().await.expect("Failed to run migrations");
let state = Arc::new(AppState {
db,
admin_token: Some(TEST_API_KEY.to_string()),
schemas_dir: schemas_dir.clone(),
proxy_config: None,
random_ua: false,
browser: false,
stealth: false,
tls_backend: ares_core::proxy::TlsBackend::default(),
});
TestApp {
router: routes::router(state),
schemas_dir,
_container: container,
_tmp_dir: tmp_dir,
}
}
pub async fn setup_test_app_no_auth() -> TestApp {
let tmp_dir = TempDir::new().expect("Failed to create temp dir");
let schemas_dir = tmp_dir.path().join("schemas");
std::fs::create_dir_all(&schemas_dir).expect("Failed to create schemas dir");
let container = start_postgres().await;
let pool = connect_to_container(&container).await;
let db = Database::from_pool(pool);
db.migrate().await.expect("Failed to run migrations");
let state = Arc::new(AppState {
db,
admin_token: None,
schemas_dir,
proxy_config: None,
random_ua: false,
browser: false,
stealth: false,
tls_backend: ares_core::proxy::TlsBackend::default(),
});
TestApp {
router: routes::router(state),
schemas_dir: tmp_dir.path().join("schemas"),
_container: container,
_tmp_dir: tmp_dir,
}
}
async fn start_postgres() -> ContainerAsync<GenericImage> {
GenericImage::new("postgres", "16")
.with_exposed_port(ContainerPort::Tcp(5432))
.with_wait_for(WaitFor::message_on_stderr(
"database system is ready to accept connections",
))
.with_env_var("POSTGRES_PASSWORD", "postgres")
.with_env_var("POSTGRES_DB", "ares_test")
.start()
.await
.expect("Failed to start PostgreSQL container")
}
async fn connect_to_container(container: &ContainerAsync<GenericImage>) -> PgPool {
let host = container.get_host().await.expect("Failed to get host");
let port = container
.get_host_port_ipv4(5432)
.await
.expect("Failed to get port");
let url = format!("postgresql://postgres:postgres@{host}:{port}/ares_test");
retry_connect(&url).await
}
async fn retry_connect(url: &str) -> PgPool {
let mut delay = std::time::Duration::from_millis(100);
let max_delay = std::time::Duration::from_secs(2);
let mut last_err = None;
for _ in 0..60 {
match PgPoolOptions::new().max_connections(5).connect(url).await {
Ok(pool) => return pool,
Err(e) => {
last_err = Some(e);
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, max_delay);
}
}
}
panic!("Failed to connect to test database at {url}: {last_err:?}");
}