From 00b1b87da4cb425c28d8dfc4bc516350933f706c Mon Sep 17 00:00:00 2001 From: Max Hohlfeld Date: Mon, 9 Dec 2024 16:06:31 +0100 Subject: [PATCH] feat: wip test helper --- Cargo.lock | 27 ++++---- Cargo.toml | 2 + src/utils/mod.rs | 1 + src/utils/test_helper/mod.rs | 128 +++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 src/utils/test_helper/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c54a26b4..e32a8b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -757,10 +757,12 @@ dependencies = [ "chrono", "dotenv", "futures-util", + "idna 1.0.2", "lettre", "pico-args", "quick-xml", "rand", + "regex", "serde", "serde_json", "sqlx", @@ -1714,23 +1716,24 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "idna_adapter" -version = "1.2.0" +name = "idna" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" dependencies = [ "icu_normalizer", "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -1846,7 +1849,7 @@ dependencies = [ "futures-util", "hostname", "httpdate", - "idna", + "idna 1.0.2", "mime", "native-tls", "nom", @@ -3058,12 +3061,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] diff --git a/Cargo.toml b/Cargo.toml index 13d6c3bc..82b22181 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ actix-web-static-files = "4.0" static-files = "0.2.1" zxcvbn = "3.1.0" thiserror = "1.0.63" +idna = "=1.0.2" +regex = "1.11.1" [build-dependencies] built = "0.7.4" diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4f1ebc57..600b027b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,6 +4,7 @@ pub mod password_help; pub mod token_generation; pub mod event_planning_template; mod application_error; +pub mod test_helper; pub use application_error::ApplicationError; use chrono::{NaiveDate, Utc}; diff --git a/src/utils/test_helper/mod.rs b/src/utils/test_helper/mod.rs new file mode 100644 index 00000000..b2ade9a4 --- /dev/null +++ b/src/utils/test_helper/mod.rs @@ -0,0 +1,128 @@ +use std::{cell::OnceCell, env, str::FromStr, sync::Arc}; + +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use regex::{Captures, Regex}; +use sqlx::{postgres::{PgConnectOptions, PgPoolOptions}, Connection, Executor, PgConnection, PgPool}; + + +pub struct DbTestContext { + //pub app: Router, + pub db_pool: PgPool, +} + +#[proc_macro_attribute] +pub fn db_test(_: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + let test_name = input.sig.ident.clone(); + let test_arguments = input.sig.inputs; + let test_block = input.block; + let inner_test_name = syn::Ident::new( + format!("inner_{}", test_name).as_str(), + input.sig.ident.span(), + ); + + let setup = quote! { + let context = abc_web::test_helpers::setup().await; + }; + + let teardown = quote! { + abc_web::test_helpers::teardown(context).await; + }; + + let output = quote!( + #[::tokio::test] + async fn #test_name() { + #setup + async fn #inner_test_name(#test_arguments) #test_block + #inner_test_name(&context).await; + #teardown + } + ); + + TokenStream::from(output) +} + +#[allow(unused)] +pub async fn setup() -> DbTestContext { + //let init_config: OnceCell = OnceCell::new(); + //let config = init_config.get_or_init(|| load_config(&Environment::Test).unwrap()); + + let test_db_pool = setup_db(&env::var("DATABASE_URL").unwrap()).await; + + //let app = init_routes(AppState { + // db_pool: test_db_pool.clone(), + //}); + + DbTestContext { + //app, + db_pool: test_db_pool, + } +} + +#[allow(unused)] +pub async fn teardown(context: DbTestContext) { + //drop(context.app); + + teardown_db(context.db_pool); +} + +#[allow(unused)] +pub async fn setup_db(url: &str) -> PgPool { + let test_db_config = prepare_db(url).await; + + let pool = PgPoolOptions::new() + .connect(url) + .await + .unwrap(); + + pool +} + +/// Drops a dedicated database for a test case. +/// +/// This function is automatically called by the [`abc-macros::db_test`] macro. It ensures test-specific database are cleaned up after each test run so we don't end up with large numbers of unused databases. +pub async fn teardown_db(pool: PgPool) { + let cloned_pool = pool.clone(); + //let mut connect_options = pool.connect_options(); + //let db_config = Arc::make_mut(&mut connect_options); + let db_config = cloned_pool.connect_options(); + + drop(pool); + + let root_db_config = db_config.clone().database("postgres"); + let mut connection: PgConnection = Connection::connect_with(&root_db_config).await.unwrap(); + + let test_db_name = db_config.get_database().unwrap(); + + let query = format!("DROP DATABASE IF EXISTS {}", test_db_name); + connection.execute(query.as_str()).await.unwrap(); +} + +async fn prepare_db(url: &str) -> String { + let db_config = PgConnectOptions::from_str(url).expect("Invalid DATABASE_URL!"); + let db_name = db_config.get_database().unwrap(); + + let root_db_config = db_config.clone().database("postgres"); + let mut connection: PgConnection = Connection::connect_with(&root_db_config).await.unwrap(); + + let test_db_name = build_test_db_name(db_name); + + let query = format!("CREATE DATABASE {} TEMPLATE {}", test_db_name, db_name); + connection.execute(query.as_str()).await.unwrap(); + + let regex = Regex::new(r"(.+)\/(.+$)").unwrap(); + let test_db_url = regex.replace(url, |caps: &Captures| { + format!("{}/{}", &caps[1], test_db_name) + }); + + test_db_url.to_string() +} + +fn build_test_db_name(base_name: &str) -> String { + let test_db_suffix: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect(); + format!("{}_{}", base_name, test_db_suffix).to_lowercase() +}