diff --git a/Cargo.lock b/Cargo.lock index b170d0d8..5a1d5994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -688,6 +688,7 @@ dependencies = [ "brass-config", "brass-macros", "built", + "change-detection", "chrono", "dotenv", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index b65d6908..a03d809d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,9 @@ members = [ "config", "macros", "web", ] resolver = "2" default-members = ["web"] + +[profile.dev.package.rinja_derive] +opt-level = 3 + +[profile.dev.package.sqlx-macros] +opt-level = 3 diff --git a/config/src/lib.rs b/config/src/lib.rs index 2936e403..6f6f5ca8 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use std::env; use std::net::IpAddr; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Config { /// the ip the server will bind to, e.g. 127.0.0.1 or ::1 pub server_address: IpAddr, @@ -19,7 +19,7 @@ pub struct Config { pub smtp_tlstype: SmtpTlsType, } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum SmtpTlsType { TLS, StartTLS, diff --git a/web/Cargo.toml b/web/Cargo.toml index 3adcad9c..f3812fc3 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -10,8 +10,6 @@ publish = false [dependencies] sqlx = { version = "^0.8", features = ["runtime-async-std-rustls", "postgres", "chrono"] } actix-web = { version = "4" } -# askama = { version = "0.13.0", git = "https://github.com/rinja-rs/askama", branch = "main", features = ["with-actix-web"] } -# askama_actix = { git = "https://github.com/rinja-rs/askama", branch = "main" } serde = { version = "1.0.164", features = ["derive"] } argon2 = { version = "0.5.0", features = [ "std"]} anyhow = "1.0.71" @@ -41,9 +39,7 @@ rinja = "0.3.5" [build-dependencies] built = "0.7.4" static-files = "0.2.1" +change-detection = "1.2.0" [dev-dependencies] insta = "1.41.1" - -# [profile.dev.package.askama_derive] -# opt-level = 3 diff --git a/web/build.rs b/web/build.rs index f6ecd805..24507cb6 100644 --- a/web/build.rs +++ b/web/build.rs @@ -1,3 +1,4 @@ +use change_detection::ChangeDetection; use static_files::{resource_dir, NpmBuild}; use std::{ fs::{self, copy}, @@ -5,9 +6,15 @@ use std::{ }; fn main() -> std::io::Result<()> { + ChangeDetection::path("static/utils.js") + .path("static/style.scss") + .path("static/brass.jpeg") + .path("static/package.json") + .generate(); + built::write_built_file().expect("Failed to acquire build-time information"); - NpmBuild::new("./static").change_detection().install()?.run("build-bulma")?; + NpmBuild::new("./static").install()?.run("build-bulma")?; let dist_path = Path::new("./static/dist"); let nm_path = Path::new("./static/node_modules"); diff --git a/web/src/endpoints/area/delete.rs b/web/src/endpoints/area/delete.rs index 83f58aee..82d04e0c 100644 --- a/web/src/endpoints/area/delete.rs +++ b/web/src/endpoints/area/delete.rs @@ -9,9 +9,7 @@ use crate::{ }; #[cfg(test)] -use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig}; -#[cfg(test)] -use actix_http::StatusCode; +use crate::utils::test_helper::{test_delete, DbTestContext, RequestConfig, StatusCode}; #[actix_web::delete("/area/delete/{id}")] pub async fn delete( diff --git a/web/src/endpoints/area/get_new.rs b/web/src/endpoints/area/get_new.rs index 4cc98389..c7714f9b 100644 --- a/web/src/endpoints/area/get_new.rs +++ b/web/src/endpoints/area/get_new.rs @@ -9,10 +9,8 @@ use rinja::Template; #[cfg(test)] use crate::utils::test_helper::{ - assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, + assert_snapshot, read_body, test_get, DbTestContext, RequestConfig, StatusCode }; -#[cfg(test)] -use actix_http::StatusCode; #[actix_web::get("/area/new")] async fn get(user: web::ReqData) -> Result { diff --git a/web/src/utils/test_helper/mod.rs b/web/src/utils/test_helper/mod.rs index 5dda2df1..e133e706 100644 --- a/web/src/utils/test_helper/mod.rs +++ b/web/src/utils/test_helper/mod.rs @@ -1,131 +1,10 @@ -use std::{cell::OnceCell, str::FromStr, sync::Arc}; - -use actix_http::Request; -use actix_web::{ - body::MessageBody, - dev::{Service, ServiceResponse}, - test::init_service, -}; -use brass_config::{load_config, Config, Environment}; -use lettre::transport::stub::StubTransport; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use regex::{Captures, Regex}; -use sqlx::{ - postgres::{PgConnectOptions, PgPoolOptions}, - Connection, Executor, PgConnection, PgPool, Pool, Postgres, -}; - +mod test_context; mod test_requests; -pub use test_requests::test_delete; -pub use test_requests::test_get; -pub use test_requests::test_post; +pub use test_context::{setup, teardown, DbTestContext}; pub use test_requests::RequestConfig; -pub use test_requests::read_body; +pub use test_requests::{read_body, test_delete, test_get, test_post}; -use crate::create_app; - -#[derive(Debug)] -pub struct DbTestContext { - pub db_pool: PgPool, - config: Config, -} - -impl DbTestContext { - pub async fn app( - &self, - ) -> impl Service< - Request, - Response = ServiceResponse, - Error = actix_web::error::Error, - > { - init_service(create_app( - self.config.clone(), - self.db_pool.clone(), - StubTransport::new_ok(), - )) - .await - } -} - -#[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(&config.database_url).await; - - let context = DbTestContext { - config: config.clone(), - db_pool: test_db_pool, - }; - - context -} - -#[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; - - println!("{test_db_config}"); - let pool = PgPoolOptions::new().connect(&test_db_config).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 mut connect_options = pool.connect_options(); - let db_config = Arc::make_mut(&mut 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) - }); - - println!("{test_db_url}"); - 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() -} +pub use actix_http::StatusCode as StatusCode; macro_rules! assert_snapshot { ($x:expr) => { diff --git a/web/src/utils/test_helper/test_context.rs b/web/src/utils/test_helper/test_context.rs new file mode 100644 index 00000000..3d125846 --- /dev/null +++ b/web/src/utils/test_helper/test_context.rs @@ -0,0 +1,111 @@ +use std::{cell::OnceCell, str::FromStr, sync::Arc, time::Duration}; + +use actix_http::Request; +use actix_web::{ + body::MessageBody, + dev::{Service, ServiceResponse}, + test::init_service, +}; +use lettre::transport::stub::StubTransport; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +use crate::create_app; +use brass_config::{load_config, Config, Environment}; +use regex::{Captures, Regex}; +use sqlx::{ + postgres::{PgConnectOptions, PgPoolOptions}, + Connection, Executor, PgConnection, PgPool, +}; + +pub struct DbTestContext { + pub db_pool: PgPool, + pub(crate) config: Config, +} + +impl DbTestContext { + pub async fn app( + &self, + ) -> impl Service< + Request, + Response = ServiceResponse, + Error = actix_web::error::Error, + > { + init_service(create_app( + self.config.clone(), + self.db_pool.clone(), + StubTransport::new_ok(), + )) + .await + } +} + +#[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_config = prepare_db(&config.database_url).await; + + let test_db_pool = PgPoolOptions::new() + .max_lifetime(Some(Duration::from_secs(1))) // workaround for idle connections + .connect(&test_db_config) + .await + .unwrap(); + + let context = DbTestContext { + config: config.clone(), + db_pool: test_db_pool.clone(), + }; + + context +} + +#[allow(unused)] +pub async fn teardown(context: DbTestContext) { + let mut connect_options = context.db_pool.connect_options(); + let db_config = Arc::make_mut(&mut connect_options); + + drop(context.db_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(); + // workaround + //let drop_query = format!("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid( ) AND datname = {}", test_db_name); + //connection.execute(drop_query.as_str()).await.unwrap(); + + let query = format!("DROP DATABASE IF EXISTS {}", test_db_name); + //println!("{query}"); + connection.execute(query.as_str()).await.unwrap(); +} + +pub(crate) 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) + }); + + //println!("{test_db_url}"); + test_db_url.to_string() +} + +pub(crate) 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() +} diff --git a/web/src/utils/test_helper/test_requests.rs b/web/src/utils/test_helper/test_requests.rs index 93f44fa5..12bcd14f 100644 --- a/web/src/utils/test_helper/test_requests.rs +++ b/web/src/utils/test_helper/test_requests.rs @@ -86,7 +86,7 @@ where T: Service, Error = Error>, R: MessageBody, { - let cookie = create_user_and_get_login_cookie(&pool, &app, &config).await; + let cookie = create_user_and_get_login_cookie(pool, &app, &config).await; let get_request = test::TestRequest::get() .uri(&config.uri) @@ -107,7 +107,7 @@ where R: MessageBody, F: Serialize, { - let cookie = create_user_and_get_login_cookie(&pool, &app, config).await; + let cookie = create_user_and_get_login_cookie(pool, &app, config).await; let post_request = test::TestRequest::post() .uri(&config.uri) @@ -127,7 +127,7 @@ where T: Service, Error = Error>, R: MessageBody, { - let cookie = create_user_and_get_login_cookie(&pool, &app, config).await; + let cookie = create_user_and_get_login_cookie(pool, &app, config).await; let delete_request = test::TestRequest::delete() .uri(&config.uri)