From cb84d40a484b4c80a7b7efb30e93407161e86d71 Mon Sep 17 00:00:00 2001 From: Mika Bomm Date: Thu, 19 Jun 2025 13:18:33 +0200 Subject: [PATCH 1/8] refactor: enhance user tests with dynamic user data generation and improved assertions --- crates/backend/tests/common/test_helpers.rs | 49 ++++++++ crates/backend/tests/endpoints/user.rs | 127 ++++++++++++++++---- 2 files changed, 151 insertions(+), 25 deletions(-) diff --git a/crates/backend/tests/common/test_helpers.rs b/crates/backend/tests/common/test_helpers.rs index 4b44d43..33fa894 100644 --- a/crates/backend/tests/common/test_helpers.rs +++ b/crates/backend/tests/common/test_helpers.rs @@ -1,5 +1,6 @@ use backend::Database; use lazy_static::lazy_static; +use std::sync::atomic::{AtomicU64, Ordering}; use testcontainers::ContainerAsync; use testcontainers_modules::{postgres::Postgres, redis::Redis}; @@ -30,6 +31,54 @@ pub async fn get_database() -> &'static Database { &state.database } +static TEST_COUNTER: AtomicU64 = AtomicU64::new(1); + +pub fn get_unique_test_id() -> String { + let counter = TEST_COUNTER.fetch_add(1, Ordering::SeqCst); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + format!("test_{}_{}", timestamp, counter) +} + +pub struct UserFactory; + +impl UserFactory { + pub fn create_request(username: Option, name: Option) -> serde_json::Value { + let test_id = get_unique_test_id(); + serde_json::json!({ + "username": username.unwrap_or_else(|| format!("user_{}", test_id)), + "name": name.unwrap_or_else(|| format!("Test User {}", test_id)), + "password": "password123" + }) + } + + pub fn create_unique_request() -> serde_json::Value { + Self::create_request(None, None) + } +} + +pub struct TestContext { + pub test_id: String, +} + +impl TestContext { + pub fn new() -> Self { + Self { + test_id: get_unique_test_id(), + } + } + + pub fn create_user_data(&self, username_prefix: Option<&str>, name: Option<&str>) -> serde_json::Value { + let username = username_prefix + .map(|prefix| format!("{}_{}", prefix, self.test_id)) + .unwrap_or_else(|| format!("user_{}", self.test_id)); + + UserFactory::create_request(Some(username), name.map(String::from)) + } +} + #[macro_export] macro_rules! create_test_app { () => {{ diff --git a/crates/backend/tests/endpoints/user.rs b/crates/backend/tests/endpoints/user.rs index bed9260..1d6bfaf 100644 --- a/crates/backend/tests/endpoints/user.rs +++ b/crates/backend/tests/endpoints/user.rs @@ -1,12 +1,10 @@ use actix_web::{http::header, test}; use serde::{Deserialize, Serialize}; -use crate::create_test_app; +use crate::{create_test_app, common::test_helpers::UserFactory}; #[cfg(test)] mod tests { - use serde_json::json; - use super::*; #[derive(Deserialize, Serialize, Debug, Clone)] @@ -19,46 +17,125 @@ mod tests { #[actix_web::test] async fn test_create_user() { let app = create_test_app!(); + let user_data = UserFactory::create_unique_request(); let resp = test::TestRequest::post() .uri("/api/v1/user") .insert_header(header::ContentType::json()) - .set_payload( - json!({ - "username": "testuser", - "name": "Test User", - "password": "password" - }) - .to_string(), - ) + .set_payload(user_data.to_string()) .send_request(&app) .await; let status = resp.status(); let user: RespCreateUser = test::read_body_json(resp).await; - assert!(user.name == "Test User"); - assert!(user.username == "testuser"); - + // Verify that the user was created with the expected structure + assert!(!user.name.is_empty()); + assert!(!user.username.is_empty()); + assert!(user.username.starts_with("user_test_")); + assert!(user.name.starts_with("Test User")); assert!(status.is_success()); - let resp_del = test::TestRequest::delete() + // Cleanup - delete the created user + let _delete_resp = test::TestRequest::delete() .uri(&format!("/api/v1/user/{}", user.id)) .send_request(&app) .await; - let status_del = resp_del.status(); + // Don't assert on cleanup status in case of race conditions + } - let delete_message: String = test::read_body_json(resp_del).await; + #[actix_web::test] + async fn test_delete_user() { + let app = create_test_app!(); + let user_data = UserFactory::create_unique_request(); + + // Create user to delete + let create_resp = test::TestRequest::post() + .uri("/api/v1/user") + .insert_header(header::ContentType::json()) + .set_payload(user_data.to_string()) + .send_request(&app) + .await; + + let create_status = create_resp.status(); + assert!(create_status.is_success(), "Failed to create user: {}", create_status); + let user: RespCreateUser = test::read_body_json(create_resp).await; + + // Delete the user + let delete_resp = test::TestRequest::delete() + .uri(&format!("/api/v1/user/{}", user.id)) + .send_request(&app) + .await; + let delete_status = delete_resp.status(); + + let delete_message: String = test::read_body_json(delete_resp).await; assert_eq!(delete_message, format!("User {} deleted", user.id)); + assert!(delete_status.is_success(), "Failed to delete user with status: {:?}", delete_status); + } - assert!( - status_del.is_success(), - "Failed to delete user with status: {:?}", - status_del - ); + #[actix_web::test] + async fn test_get_users() { + let app = create_test_app!(); - // Debugging output - dbg!(user); - dbg!(delete_message); + let resp = test::TestRequest::get() + .uri("/api/v1/user") + .send_request(&app) + .await; + + let status = resp.status(); + let users: Vec = test::read_body_json(resp).await; + + assert!(status.is_success()); + assert!(users.is_empty() || !users.is_empty()); // Just verify it returns a valid array + } + + #[actix_web::test] + async fn test_create_user_duplicate_username() { + let app = create_test_app!(); + let user_data = UserFactory::create_unique_request(); + + // Create first user + let resp1 = test::TestRequest::post() + .uri("/api/v1/user") + .insert_header(header::ContentType::json()) + .set_payload(user_data.to_string()) + .send_request(&app) + .await; + + let status1 = resp1.status(); + let user1: RespCreateUser = test::read_body_json(resp1).await; + assert!(status1.is_success()); + + // Try to create user with same username + let resp2 = test::TestRequest::post() + .uri("/api/v1/user") + .insert_header(header::ContentType::json()) + .set_payload(user_data.to_string()) + .send_request(&app) + .await; + + let status2 = resp2.status(); + assert!(status2.is_client_error() || status2.is_server_error()); + + // Cleanup + let _delete_resp = test::TestRequest::delete() + .uri(&format!("/api/v1/user/{}", user1.id)) + .send_request(&app) + .await; + // Don't assert on cleanup status in case of race conditions + } + + #[actix_web::test] + async fn test_delete_nonexistent_user() { + let app = create_test_app!(); + let fake_id = "00000000-0000-0000-0000-000000000000"; + + let resp = test::TestRequest::delete() + .uri(&format!("/api/v1/user/{}", fake_id)) + .send_request(&app) + .await; + + let status = resp.status(); + assert!(status.is_client_error() || status.is_server_error()); } } From 9dbfeef94f3fe7924d7218a9063b06476013eebf Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 10:37:40 +0200 Subject: [PATCH 2/8] refactor: reorganize module imports for consistency and clarity --- crates/backend/src/controller.rs | 3 ++- crates/backend/src/controller/project.rs | 2 +- crates/backend/src/controller/user.rs | 2 +- crates/backend/src/db/project.rs | 2 +- crates/backend/src/db/user.rs | 2 +- crates/backend/src/lib.rs | 34 ++---------------------- crates/backend/src/main.rs | 32 +++------------------- crates/backend/src/utils.rs | 29 ++++++++++++++++++++ crates/backend/src/utoipa.rs | 2 +- 9 files changed, 41 insertions(+), 67 deletions(-) create mode 100644 crates/backend/src/utils.rs diff --git a/crates/backend/src/controller.rs b/crates/backend/src/controller.rs index ee8e8a5..979c287 100644 --- a/crates/backend/src/controller.rs +++ b/crates/backend/src/controller.rs @@ -1,6 +1,7 @@ use actix_web::web::{self, ServiceConfig}; -pub mod auth; // TODO: Refactor to use re-exports instead of making module public +// TODO: Refactor to use re-exports instead of making module public +pub mod auth; pub mod class; pub mod group; pub mod project; diff --git a/crates/backend/src/controller/project.rs b/crates/backend/src/controller/project.rs index 476e954..2ff4524 100644 --- a/crates/backend/src/controller/project.rs +++ b/crates/backend/src/controller/project.rs @@ -4,7 +4,7 @@ use validator::Validate; use crate::db::project::CreateProject; use crate::db::Database; -use crate::entity; +use crate::db::entity; use crate::error::ApiError; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { diff --git a/crates/backend/src/controller/user.rs b/crates/backend/src/controller/user.rs index b2b3247..890147f 100644 --- a/crates/backend/src/controller/user.rs +++ b/crates/backend/src/controller/user.rs @@ -1,4 +1,4 @@ -use crate::{Database, entity, error::ApiError}; +use crate::{Database, db::entity, error::ApiError}; use actix_web::{Responder, delete, get, post, put, web}; use serde::Deserialize; use utoipa::ToSchema; diff --git a/crates/backend/src/db/project.rs b/crates/backend/src/db/project.rs index 23df789..e5c453e 100644 --- a/crates/backend/src/db/project.rs +++ b/crates/backend/src/db/project.rs @@ -2,7 +2,7 @@ use super::Database; use crate::error::ApiError; use log::debug; -use crate::entity::project; +use crate::db::entity::project; use sea_orm::ActiveValue::{NotSet, Set, Unchanged}; use sea_orm::{ActiveModelTrait, DeleteResult, EntityTrait}; use serde::Deserialize; diff --git a/crates/backend/src/db/user.rs b/crates/backend/src/db/user.rs index 4e5cf28..8aaf9cc 100644 --- a/crates/backend/src/db/user.rs +++ b/crates/backend/src/db/user.rs @@ -10,7 +10,7 @@ use sea_orm::{ }; use uuid::Uuid; -use crate::{Database, entity}; +use crate::{Database, db::entity}; impl Database { pub async fn get_users(&self) -> Result, ApiError> { diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index d36f5c5..e8802cb 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -2,38 +2,8 @@ pub mod controller; pub mod db; pub mod error; pub mod utoipa; +pub mod utils; pub use db::Database; pub use db::entity; - -use dotenvy; -use std::env; - -#[cfg(not(test))] -fn get_env_var(name: &str) -> dotenvy::Result { - dotenvy::var(name) -} - -#[cfg(test)] -fn get_env_var(name: &str) -> Result { - std::env::var(name) -} - -// TODO: Extract build_database_url into a utils module or similar -pub fn build_database_url() -> String { - let db_user = get_env_var("DB_USER").unwrap_or_else(|_| "pgg".to_owned()); - let db_name = get_env_var("DB_NAME").unwrap_or_else(|_| "pgg".to_owned()); - let db_password = get_env_var("DB_PASSWORD").unwrap_or_else(|_| "pgg".to_owned()); - let db_host = get_env_var("DB_HOST").expect("DB_HOST must be set in .env"); - let db_port = get_env_var("DB_PORT") - .map(|x| x.parse::().expect("DB_PORT is not a valid port")) - .unwrap_or(5432); - - let result = format!( - "postgresql://{}:{}@{}:{}/{}", - db_user, db_password, db_host, db_port, db_name - ); - - println!("Database URL: {}", result); - result -} +pub use utils::{build_database_url, get_env_var}; diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index eee7606..3605ea2 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -8,12 +8,13 @@ mod controller; mod db; mod error; mod utoipa; +mod utils; -pub use db::Database; -pub use db::entity; +use db::Database; use log::info; use migration::Migrator; use migration::MigratorTrait; +use utils::{build_database_url, get_env_var}; #[derive(Clone)] struct AppConfig { @@ -80,15 +81,6 @@ async fn main() -> std::io::Result<()> { .await } -#[cfg(not(test))] -fn get_env_var(name: &str) -> dotenvy::Result { - dotenvy::var(name) -} - -#[cfg(test)] -fn get_env_var(name: &str) -> Result { - std::env::var(name) -} pub async fn connect_to_redis_database() -> RedisSessionStore { let redis_host = get_env_var("REDIS_HOST").expect("REDIS_HOST must be set in .env"); @@ -102,24 +94,6 @@ pub async fn connect_to_redis_database() -> RedisSessionStore { .unwrap() } -pub fn build_database_url() -> String { - let db_user = get_env_var("DB_USER").unwrap_or_else(|_| "pgg".to_owned()); - let db_name = get_env_var("DB_NAME").unwrap_or_else(|_| "pgg".to_owned()); - let db_password = get_env_var("DB_PASSWORD").unwrap_or_else(|_| "pgg".to_owned()); - let db_host = get_env_var("DB_HOST").expect("DB_HOST must be set in .env"); - let db_port = get_env_var("DB_PORT") - .map(|x| x.parse::().expect("DB_PORT is not a valid port")) - .unwrap_or(5432); - - let result = format!( - "postgresql://{}:{}@{}:{}/{}", - db_user, db_password, db_host, db_port, db_name - ); - - println!("Database URL: {}", result); - result -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/backend/src/utils.rs b/crates/backend/src/utils.rs new file mode 100644 index 0000000..efb1e15 --- /dev/null +++ b/crates/backend/src/utils.rs @@ -0,0 +1,29 @@ +use log::info; + +#[cfg(not(test))] +pub fn get_env_var(name: &str) -> dotenvy::Result { + dotenvy::var(name) +} + +#[cfg(test)] +pub fn get_env_var(name: &str) -> Result { + std::env::var(name) +} + +pub fn build_database_url() -> String { + let db_user = get_env_var("DB_USER").unwrap_or_else(|_| "pgg".to_owned()); + let db_name = get_env_var("DB_NAME").unwrap_or_else(|_| "pgg".to_owned()); + let db_password = get_env_var("DB_PASSWORD").unwrap_or_else(|_| "pgg".to_owned()); + let db_host = get_env_var("DB_HOST").expect("DB_HOST must be set in .env"); + let db_port = get_env_var("DB_PORT") + .map(|x| x.parse::().expect("DB_PORT is not a valid port")) + .unwrap_or(5432); + + let result = format!( + "postgresql://{}:{}@{}:{}/{}", + db_user, db_password, db_host, db_port, db_name + ); + + info!("Database URL: {}", result); + result +} diff --git a/crates/backend/src/utoipa.rs b/crates/backend/src/utoipa.rs index 6814dd4..84646e1 100644 --- a/crates/backend/src/utoipa.rs +++ b/crates/backend/src/utoipa.rs @@ -1,6 +1,6 @@ use utoipa::OpenApi; -use crate::{controller, db, entity, error}; +use crate::{controller, db, db::entity, error}; #[derive(OpenApi)] #[openapi( From d41370cbb20c0b0ac52b6ff93eab703090d8b751 Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 11:12:38 +0200 Subject: [PATCH 3/8] refactor: integration-tests and format whole project with cargo fmt --- crates/backend/src/controller/class.rs | 2 +- crates/backend/src/controller/group.rs | 2 +- crates/backend/src/controller/project.rs | 4 +- crates/backend/src/controller/template.rs | 2 +- crates/backend/src/error.rs | 1 - crates/backend/src/lib.rs | 2 +- crates/backend/src/main.rs | 11 +- crates/backend/src/utoipa.rs | 2 +- .../tests/common/db_helpers/auth_helpers.rs | 121 +++++++++++++ crates/backend/tests/common/db_helpers/mod.rs | 3 + .../common/db_helpers/project_helpers.rs | 125 +++++++++++++ .../tests/common/db_helpers/user_helpers.rs | 121 +++++++++++++ crates/backend/tests/common/mod.rs | 77 +------- crates/backend/tests/common/setup.rs | 74 ++++++++ crates/backend/tests/common/test_helpers.rs | 32 +++- crates/backend/tests/endpoints/user.rs | 14 +- .../backend/tests/example_refactored_test.rs | 164 ++++++++++++++++++ 17 files changed, 664 insertions(+), 93 deletions(-) create mode 100644 crates/backend/tests/common/db_helpers/auth_helpers.rs create mode 100644 crates/backend/tests/common/db_helpers/mod.rs create mode 100644 crates/backend/tests/common/db_helpers/project_helpers.rs create mode 100644 crates/backend/tests/common/db_helpers/user_helpers.rs create mode 100644 crates/backend/tests/common/setup.rs create mode 100644 crates/backend/tests/example_refactored_test.rs diff --git a/crates/backend/src/controller/class.rs b/crates/backend/src/controller/class.rs index 9f9343c..591cdfa 100644 --- a/crates/backend/src/controller/class.rs +++ b/crates/backend/src/controller/class.rs @@ -1,4 +1,4 @@ -use actix_web::{delete, get, post, put, Responder}; +use actix_web::{Responder, delete, get, post, put}; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(get_classes) diff --git a/crates/backend/src/controller/group.rs b/crates/backend/src/controller/group.rs index e125764..8df024d 100644 --- a/crates/backend/src/controller/group.rs +++ b/crates/backend/src/controller/group.rs @@ -1,4 +1,4 @@ -use actix_web::{delete, get, post, put, Responder}; +use actix_web::{Responder, delete, get, post, put}; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(get_groups) diff --git a/crates/backend/src/controller/project.rs b/crates/backend/src/controller/project.rs index 2ff4524..0fc6794 100644 --- a/crates/backend/src/controller/project.rs +++ b/crates/backend/src/controller/project.rs @@ -1,10 +1,10 @@ -use actix_web::{delete, get, post, put, web, Result}; +use actix_web::{Result, delete, get, post, put, web}; use uuid::Uuid; use validator::Validate; -use crate::db::project::CreateProject; use crate::db::Database; use crate::db::entity; +use crate::db::project::CreateProject; use crate::error::ApiError; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { diff --git a/crates/backend/src/controller/template.rs b/crates/backend/src/controller/template.rs index 61b138d..e078b00 100644 --- a/crates/backend/src/controller/template.rs +++ b/crates/backend/src/controller/template.rs @@ -1,4 +1,4 @@ -use actix_web::{delete, get, post, put, Responder}; +use actix_web::{Responder, delete, get, post, put}; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(get_templates) diff --git a/crates/backend/src/error.rs b/crates/backend/src/error.rs index e34cbba..50f0872 100644 --- a/crates/backend/src/error.rs +++ b/crates/backend/src/error.rs @@ -66,4 +66,3 @@ impl MessageResponse { } } } - diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index e8802cb..b9d201e 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -1,8 +1,8 @@ pub mod controller; pub mod db; pub mod error; -pub mod utoipa; pub mod utils; +pub mod utoipa; pub use db::Database; pub use db::entity; diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index 3605ea2..866fef1 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -7,8 +7,8 @@ use utoipa_swagger_ui::SwaggerUi; mod controller; mod db; mod error; -mod utoipa; mod utils; +mod utoipa; use db::Database; use log::info; @@ -61,10 +61,10 @@ async fn main() -> std::io::Result<()> { .wrap(Logger::default()) .wrap(session_middleware) .service(web::scope("/api/v1").configure(controller::register_controllers)) - .service( - SwaggerUi::new("/swagger-ui/{_:.*}") - .url("/api-docs/openapi.json", crate::utoipa::ApiDoc::openapi_spec()), - ); + .service(SwaggerUi::new("/swagger-ui/{_:.*}").url( + "/api-docs/openapi.json", + crate::utoipa::ApiDoc::openapi_spec(), + )); #[cfg(feature = "serve")] let app = { @@ -81,7 +81,6 @@ async fn main() -> std::io::Result<()> { .await } - pub async fn connect_to_redis_database() -> RedisSessionStore { let redis_host = get_env_var("REDIS_HOST").expect("REDIS_HOST must be set in .env"); let redis_port = get_env_var("REDIS_PORT") diff --git a/crates/backend/src/utoipa.rs b/crates/backend/src/utoipa.rs index 84646e1..fad342d 100644 --- a/crates/backend/src/utoipa.rs +++ b/crates/backend/src/utoipa.rs @@ -61,4 +61,4 @@ impl ApiDoc { pub fn openapi_spec() -> utoipa::openapi::OpenApi { Self::openapi() } -} \ No newline at end of file +} diff --git a/crates/backend/tests/common/db_helpers/auth_helpers.rs b/crates/backend/tests/common/db_helpers/auth_helpers.rs new file mode 100644 index 0000000..ecd1ae5 --- /dev/null +++ b/crates/backend/tests/common/db_helpers/auth_helpers.rs @@ -0,0 +1,121 @@ +use crate::common::test_helpers::TestContext; +use backend::{Database, db::entity}; +use uuid::Uuid; + +impl TestContext { + pub async fn create_user_with_auth( + &self, + db: &Database, + username: Option, + name: Option, + password: String, + ) -> Result { + self.create_user_with_password(db, username, name, password) + .await + } + + pub async fn verify_user_login( + &self, + db: &Database, + username: &str, + password: &str, + ) -> Result { + db.verify_local_user(username, password).await + } + + pub async fn create_authenticated_user( + &self, + db: &Database, + username: Option, + password: Option, + ) -> Result<(entity::user::Model, String), backend::error::ApiError> { + let test_id = &self.test_id; + let username = username.unwrap_or_else(|| format!("auth_user_{}", test_id)); + let password = password.unwrap_or_else(|| "test_password_123".to_string()); + + let user = self + .create_user_with_password(db, Some(username.clone()), None, password.clone()) + .await?; + + Ok((user, password)) + } + + pub async fn create_multiple_authenticated_users( + &self, + db: &Database, + count: usize, + ) -> Result, backend::error::ApiError> { + let mut users = Vec::new(); + + for i in 0..count { + let username = format!("auth_user_{}_{}", self.test_id, i); + let password = format!("password_{}", i); + let user_data = self + .create_authenticated_user(db, Some(username), Some(password.clone())) + .await?; + users.push(user_data); + } + + Ok(users) + } + + pub async fn assert_user_can_login( + &self, + db: &Database, + username: &str, + password: &str, + ) -> bool { + match self.verify_user_login(db, username, password).await { + Ok(_) => true, + Err(_) => false, + } + } + + pub async fn assert_user_cannot_login( + &self, + db: &Database, + username: &str, + password: &str, + ) -> bool { + !self.assert_user_can_login(db, username, password).await + } + + pub async fn assert_user_login_returns_correct_id( + &self, + db: &Database, + username: &str, + password: &str, + expected_id: Uuid, + ) -> bool { + match self.verify_user_login(db, username, password).await { + Ok(id) => id == expected_id, + Err(_) => false, + } + } + + pub async fn test_invalid_login_attempts(&self, db: &Database, username: &str) -> Vec { + let invalid_passwords = vec!["wrong_password", "", "123", "password"]; + let mut results = Vec::new(); + + for password in invalid_passwords { + let can_login = self.assert_user_can_login(db, username, password).await; + results.push(!can_login); // We expect these to fail, so invert the result + } + + results + } + + pub async fn create_user_and_verify_auth( + &self, + db: &Database, + username: Option, + password: Option, + ) -> Result<(entity::user::Model, bool), backend::error::ApiError> { + let (user, pwd) = self + .create_authenticated_user(db, username, password) + .await?; + let can_login = self.assert_user_can_login(db, &user.username, &pwd).await; + + Ok((user, can_login)) + } +} diff --git a/crates/backend/tests/common/db_helpers/mod.rs b/crates/backend/tests/common/db_helpers/mod.rs new file mode 100644 index 0000000..d4f9e77 --- /dev/null +++ b/crates/backend/tests/common/db_helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_helpers; +pub mod project_helpers; +pub mod user_helpers; diff --git a/crates/backend/tests/common/db_helpers/project_helpers.rs b/crates/backend/tests/common/db_helpers/project_helpers.rs new file mode 100644 index 0000000..b947447 --- /dev/null +++ b/crates/backend/tests/common/db_helpers/project_helpers.rs @@ -0,0 +1,125 @@ +use crate::common::test_helpers::TestContext; +use backend::{ + Database, + db::{entity, project::CreateProject}, +}; +use uuid::Uuid; + +impl TestContext { + pub async fn create_project( + &self, + db: &Database, + name: Option, + ) -> Result { + let name = name.unwrap_or_else(|| format!("Test Project {}", self.test_id)); + let create_project = CreateProject { name }; + + let project = db.create_project(create_project).await?; + + if let Ok(mut projects) = self.created_projects.lock() { + projects.push(project.id); + } + + Ok(project) + } + + pub async fn create_project_with_name( + &self, + db: &Database, + name: String, + ) -> Result { + self.create_project(db, Some(name)).await + } + + pub async fn create_multiple_projects( + &self, + db: &Database, + count: usize, + ) -> Result, backend::error::ApiError> { + let mut projects = Vec::new(); + + for i in 0..count { + let name = format!("Test Project {} {}", self.test_id, i); + let project = self.create_project(db, Some(name)).await?; + projects.push(project); + } + + Ok(projects) + } + + pub async fn get_project_by_id( + &self, + db: &Database, + id: &Uuid, + ) -> Result, backend::error::ApiError> { + db.get_project(id).await + } + + pub async fn get_all_projects( + &self, + db: &Database, + ) -> Result, backend::error::ApiError> { + db.get_projects().await + } + + pub async fn update_project( + &self, + db: &Database, + id: &Uuid, + name: String, + ) -> Result { + let update_data = CreateProject { name }; + db.update_project(id, update_data).await + } + + pub async fn assert_project_exists(&self, db: &Database, id: &Uuid) -> bool { + match self.get_project_by_id(db, id).await { + Ok(Some(_)) => true, + _ => false, + } + } + + pub async fn assert_project_count(&self, db: &Database, expected: usize) -> bool { + match self.get_all_projects(db).await { + Ok(projects) => projects.len() == expected, + Err(_) => false, + } + } + + pub async fn assert_project_not_exists(&self, db: &Database, id: &Uuid) -> bool { + !self.assert_project_exists(db, id).await + } + + pub async fn assert_project_name(&self, db: &Database, id: &Uuid, expected_name: &str) -> bool { + match self.get_project_by_id(db, id).await { + Ok(Some(project)) => project.name == expected_name, + _ => false, + } + } + + pub async fn delete_project( + &self, + db: &Database, + id: &Uuid, + ) -> Result<(), backend::error::ApiError> { + db.delete_project(id).await?; + + if let Ok(mut projects) = self.created_projects.lock() { + projects.retain(|&project_id| project_id != *id); + } + + Ok(()) + } + + pub async fn cleanup_projects(&self, db: &Database) { + if let Ok(projects) = self.created_projects.lock() { + for project_id in projects.iter() { + let _ = db.delete_project(project_id).await; + } + } + + if let Ok(mut projects) = self.created_projects.lock() { + projects.clear(); + } + } +} diff --git a/crates/backend/tests/common/db_helpers/user_helpers.rs b/crates/backend/tests/common/db_helpers/user_helpers.rs new file mode 100644 index 0000000..2bbe483 --- /dev/null +++ b/crates/backend/tests/common/db_helpers/user_helpers.rs @@ -0,0 +1,121 @@ +use crate::common::test_helpers::TestContext; +use backend::{Database, db::entity}; +use uuid::Uuid; + +impl TestContext { + pub async fn create_user( + &self, + db: &Database, + username: Option, + name: Option, + ) -> Result { + let test_id = &self.test_id; + let username = username.unwrap_or_else(|| format!("user_{}", test_id)); + let name = name.unwrap_or_else(|| format!("Test User {}", test_id)); + let password = "password123".to_string(); + + let user = db.create_user(name, username, password).await?; + + if let Ok(mut users) = self.created_users.lock() { + users.push(user.id); + } + + Ok(user) + } + + pub async fn create_user_with_password( + &self, + db: &Database, + username: Option, + name: Option, + password: String, + ) -> Result { + let test_id = &self.test_id; + let username = username.unwrap_or_else(|| format!("user_{}", test_id)); + let name = name.unwrap_or_else(|| format!("Test User {}", test_id)); + + let user = db.create_user(name, username, password).await?; + + if let Ok(mut users) = self.created_users.lock() { + users.push(user.id); + } + + Ok(user) + } + + pub async fn create_multiple_users( + &self, + db: &Database, + count: usize, + ) -> Result, backend::error::ApiError> { + let mut users = Vec::new(); + + for i in 0..count { + let username = format!("user_{}_{}", self.test_id, i); + let name = format!("Test User {} {}", self.test_id, i); + let user = self.create_user(db, Some(username), Some(name)).await?; + users.push(user); + } + + Ok(users) + } + + pub async fn get_user_by_id( + &self, + db: &Database, + id: Uuid, + ) -> Result, backend::error::ApiError> { + db.get_user(id).await + } + + pub async fn get_all_users( + &self, + db: &Database, + ) -> Result, backend::error::ApiError> { + db.get_users().await + } + + pub async fn assert_user_exists(&self, db: &Database, id: Uuid) -> bool { + match self.get_user_by_id(db, id).await { + Ok(Some(_)) => true, + _ => false, + } + } + + pub async fn assert_user_count(&self, db: &Database, expected: usize) -> bool { + match self.get_all_users(db).await { + Ok(users) => users.len() == expected, + Err(_) => false, + } + } + + pub async fn assert_user_not_exists(&self, db: &Database, id: Uuid) -> bool { + !self.assert_user_exists(db, id).await + } + + pub async fn delete_user( + &self, + db: &Database, + id: Uuid, + ) -> Result<(), backend::error::ApiError> { + db.delete_user(id).await?; + + if let Ok(mut users) = self.created_users.lock() { + users.retain(|&user_id| user_id != id); + } + + Ok(()) + } + + pub async fn cleanup_users(&self, db: &Database) { + if let Ok(users) = self.created_users.lock() { + for user_id in users.iter() { + let _ = db.delete_user(*user_id).await; + } + } + + if let Ok(mut users) = self.created_users.lock() { + users.clear(); + } + } +} diff --git a/crates/backend/tests/common/mod.rs b/crates/backend/tests/common/mod.rs index bcb7519..9b34500 100644 --- a/crates/backend/tests/common/mod.rs +++ b/crates/backend/tests/common/mod.rs @@ -1,76 +1,5 @@ -use backend::{Database, build_database_url}; -use log::{debug, info}; -use migration::{Migrator, MigratorTrait}; -use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; -use testcontainers_modules::{postgres::Postgres, redis::Redis}; - +pub mod db_helpers; +pub mod setup; pub mod test_helpers; -pub async fn setup() -> (ContainerAsync, ContainerAsync, Database) { - let postgres = Postgres::default() - .with_env_var("POSTGRES_DB", "test_db") - .with_env_var("POSTGRES_USER", "postgres") - .with_env_var("POSTGRES_PASSWORD", "postgres") - .start() - .await - .expect("Failed to start PostgreSQL container"); - - let redis = Redis::default() - .start() - .await - .expect("Failed to start Redis container"); - - let postgres_port = postgres.get_host_port_ipv4(5432).await.unwrap(); - let redis_port = redis.get_host_port_ipv4(6379).await.unwrap(); - - debug!("PostgreSQL container started on port: {}", postgres_port); - debug!("Redis container started on port: {}", redis_port); - - // Wait for PostgreSQL to be ready - wait_for_postgres_ready(&postgres).await; - - unsafe { - std::env::set_var("DB_HOST", "127.0.0.1"); - std::env::set_var("DB_PORT", postgres_port.to_string()); - std::env::set_var("DB_NAME", "test_db"); - std::env::set_var("DB_USER", "postgres"); - std::env::set_var("DB_PASSWORD", "postgres"); - std::env::set_var("REDIS_HOST", "127.0.0.1"); - std::env::set_var("REDIS_PORT", redis_port.to_string()); - } - - let database_url = build_database_url(); - info!("Database URL: {}", database_url); - - let database = Database::new(database_url.into()).await.unwrap(); - - Migrator::up(database.connection(), None).await.unwrap(); - - (postgres, redis, database) -} - -async fn wait_for_postgres_ready(container: &ContainerAsync) { - info!("Waiting for PostgreSQL to be ready..."); - - for attempt in 1..=30 { - match container.stdout_to_vec().await { - Ok(logs) => { - let log_string = String::from_utf8_lossy(&logs); - - if log_string.contains("database system is ready to accept connections") { - info!("PostgreSQL is ready after {} attempts", attempt); - return; - } - - debug!("Attempt {}: PostgreSQL not ready yet", attempt); - } - Err(e) => { - debug!("Attempt {}: Failed to read logs: {}", attempt, e); - } - } - - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - } - - panic!("PostgreSQL failed to become ready within 30 seconds"); -} +pub use setup::setup; diff --git a/crates/backend/tests/common/setup.rs b/crates/backend/tests/common/setup.rs new file mode 100644 index 0000000..45e3e81 --- /dev/null +++ b/crates/backend/tests/common/setup.rs @@ -0,0 +1,74 @@ +use backend::{Database, build_database_url}; +use log::{debug, info}; +use migration::{Migrator, MigratorTrait}; +use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; +use testcontainers_modules::{postgres::Postgres, redis::Redis}; + +pub async fn setup() -> (ContainerAsync, ContainerAsync, Database) { + let postgres = Postgres::default() + .with_env_var("POSTGRES_DB", "test_db") + .with_env_var("POSTGRES_USER", "postgres") + .with_env_var("POSTGRES_PASSWORD", "postgres") + .start() + .await + .expect("Failed to start PostgreSQL container"); + + let redis = Redis::default() + .start() + .await + .expect("Failed to start Redis container"); + + let postgres_port = postgres.get_host_port_ipv4(5432).await.unwrap(); + let redis_port = redis.get_host_port_ipv4(6379).await.unwrap(); + + debug!("PostgreSQL container started on port: {}", postgres_port); + debug!("Redis container started on port: {}", redis_port); + + // Wait for PostgreSQL to be ready + wait_for_postgres_ready(&postgres).await; + + unsafe { + std::env::set_var("DB_HOST", "127.0.0.1"); + std::env::set_var("DB_PORT", postgres_port.to_string()); + std::env::set_var("DB_NAME", "test_db"); + std::env::set_var("DB_USER", "postgres"); + std::env::set_var("DB_PASSWORD", "postgres"); + std::env::set_var("REDIS_HOST", "127.0.0.1"); + std::env::set_var("REDIS_PORT", redis_port.to_string()); + } + + let database_url = build_database_url(); + info!("Database URL: {}", database_url); + + let database = Database::new(database_url.into()).await.unwrap(); + + Migrator::up(database.connection(), None).await.unwrap(); + + (postgres, redis, database) +} + +async fn wait_for_postgres_ready(container: &ContainerAsync) { + info!("Waiting for PostgreSQL to be ready..."); + + for attempt in 1..=30 { + match container.stdout_to_vec().await { + Ok(logs) => { + let log_string = String::from_utf8_lossy(&logs); + + if log_string.contains("database system is ready to accept connections") { + info!("PostgreSQL is ready after {} attempts", attempt); + return; + } + + debug!("Attempt {}: PostgreSQL not ready yet", attempt); + } + Err(e) => { + debug!("Attempt {}: Failed to read logs: {}", attempt, e); + } + } + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + + panic!("PostgreSQL failed to become ready within 30 seconds"); +} diff --git a/crates/backend/tests/common/test_helpers.rs b/crates/backend/tests/common/test_helpers.rs index 33fa894..8bc0c01 100644 --- a/crates/backend/tests/common/test_helpers.rs +++ b/crates/backend/tests/common/test_helpers.rs @@ -3,6 +3,7 @@ use lazy_static::lazy_static; use std::sync::atomic::{AtomicU64, Ordering}; use testcontainers::ContainerAsync; use testcontainers_modules::{postgres::Postgres, redis::Redis}; +use uuid::Uuid; use super::setup; @@ -61,22 +62,35 @@ impl UserFactory { pub struct TestContext { pub test_id: String, + pub created_users: std::sync::Arc>>, + pub created_projects: std::sync::Arc>>, } impl TestContext { pub fn new() -> Self { Self { test_id: get_unique_test_id(), + created_users: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + created_projects: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), } } - pub fn create_user_data(&self, username_prefix: Option<&str>, name: Option<&str>) -> serde_json::Value { + pub fn create_user_data( + &self, + username_prefix: Option<&str>, + name: Option<&str>, + ) -> serde_json::Value { let username = username_prefix .map(|prefix| format!("{}_{}", prefix, self.test_id)) .unwrap_or_else(|| format!("user_{}", self.test_id)); - + UserFactory::create_request(Some(username), name.map(String::from)) } + + pub async fn cleanup_all(&self, db: &Database) { + self.cleanup_projects(db).await; + self.cleanup_users(db).await; + } } #[macro_export] @@ -95,3 +109,17 @@ macro_rules! create_test_app { .await }}; } + +#[macro_export] +macro_rules! with_test_context { + ($test_fn:expr) => {{ + let ctx = $crate::common::test_helpers::TestContext::new(); + let db = $crate::common::test_helpers::get_database().await; + + let result = $test_fn(ctx.clone(), db).await; + + ctx.cleanup_all(db).await; + + result + }}; +} diff --git a/crates/backend/tests/endpoints/user.rs b/crates/backend/tests/endpoints/user.rs index 1d6bfaf..87ab41b 100644 --- a/crates/backend/tests/endpoints/user.rs +++ b/crates/backend/tests/endpoints/user.rs @@ -1,7 +1,7 @@ use actix_web::{http::header, test}; use serde::{Deserialize, Serialize}; -use crate::{create_test_app, common::test_helpers::UserFactory}; +use crate::{common::test_helpers::UserFactory, create_test_app}; #[cfg(test)] mod tests { @@ -58,7 +58,11 @@ mod tests { .await; let create_status = create_resp.status(); - assert!(create_status.is_success(), "Failed to create user: {}", create_status); + assert!( + create_status.is_success(), + "Failed to create user: {}", + create_status + ); let user: RespCreateUser = test::read_body_json(create_resp).await; // Delete the user @@ -70,7 +74,11 @@ mod tests { let delete_message: String = test::read_body_json(delete_resp).await; assert_eq!(delete_message, format!("User {} deleted", user.id)); - assert!(delete_status.is_success(), "Failed to delete user with status: {:?}", delete_status); + assert!( + delete_status.is_success(), + "Failed to delete user with status: {:?}", + delete_status + ); } #[actix_web::test] diff --git a/crates/backend/tests/example_refactored_test.rs b/crates/backend/tests/example_refactored_test.rs new file mode 100644 index 0000000..a9dae88 --- /dev/null +++ b/crates/backend/tests/example_refactored_test.rs @@ -0,0 +1,164 @@ +use crate::{common::test_helpers::TestContext, create_test_app, with_test_context}; +use actix_web::test; +use backend::Database; + +#[cfg(test)] +mod tests { + use super::*; + + #[actix_web::test] + async fn test_user_crud_operations() { + with_test_context!(|ctx: TestContext, db: &Database| async move { + // Create single user + let user = ctx + .create_user( + db, + Some("testuser".to_string()), + Some("Test User".to_string()), + ) + .await + .unwrap(); + assert_eq!(user.username, "testuser"); + assert_eq!(user.name, "Test User"); + + // Assert user exists + assert!(ctx.assert_user_exists(db, user.id).await); + + // Get user by ID + let retrieved_user = ctx.get_user_by_id(db, user.id).await.unwrap().unwrap(); + assert_eq!(retrieved_user.id, user.id); + + // Create multiple users + let users = ctx.create_multiple_users(db, 3).await.unwrap(); + assert_eq!(users.len(), 3); + + // Assert user count (initial user + 3 new users = 4) + assert!(ctx.assert_user_count(db, 4).await); + + // Delete a user + ctx.delete_user(db, user.id).await.unwrap(); + assert!(ctx.assert_user_not_exists(db, user.id).await); + + // All cleanup handled automatically + }); + } + + #[actix_web::test] + async fn test_project_crud_operations() { + with_test_context!(|ctx: TestContext, db: &Database| async move { + // Create project with custom name + let project = ctx + .create_project_with_name(db, "My Test Project".to_string()) + .await + .unwrap(); + assert_eq!(project.name, "My Test Project"); + + // Assert project exists + assert!(ctx.assert_project_exists(db, &project.id).await); + + // Update project + let updated = ctx + .update_project(db, &project.id, "Updated Project Name".to_string()) + .await + .unwrap(); + assert_eq!(updated.name, "Updated Project Name"); + + // Assert project name changed + assert!( + ctx.assert_project_name(db, &project.id, "Updated Project Name") + .await + ); + + // Create multiple projects + let projects = ctx.create_multiple_projects(db, 2).await.unwrap(); + assert_eq!(projects.len(), 2); + + // Assert project count (1 original + 2 new = 3) + assert!(ctx.assert_project_count(db, 3).await); + + // All cleanup automatic + }); + } + + #[actix_web::test] + async fn test_authentication_operations() { + with_test_context!(|ctx: TestContext, db: &Database| async move { + // Create authenticated user + let (user, password) = ctx + .create_authenticated_user( + db, + Some("authuser".to_string()), + Some("securepass123".to_string()), + ) + .await + .unwrap(); + + // Test successful login + assert!( + ctx.assert_user_can_login(db, "authuser", "securepass123") + .await + ); + + // Test login returns correct user ID + assert!( + ctx.assert_user_login_returns_correct_id(db, "authuser", "securepass123", user.id) + .await + ); + + // Test failed login with wrong password + assert!( + ctx.assert_user_cannot_login(db, "authuser", "wrongpassword") + .await + ); + + // Test multiple invalid login attempts + let invalid_results = ctx.test_invalid_login_attempts(db, "authuser").await; + assert!(invalid_results.iter().all(|&result| result)); // All should fail (return true) + + // Create and verify multiple auth users + let auth_users = ctx + .create_multiple_authenticated_users(db, 2) + .await + .unwrap(); + assert_eq!(auth_users.len(), 2); + + for (user, pwd) in &auth_users { + assert!(ctx.assert_user_can_login(db, &user.username, pwd).await); + } + + // All cleanup automatic + }); + } + + #[actix_web::test] + async fn test_mixed_operations_with_api() { + with_test_context!(|ctx: TestContext, db: &Database| async move { + let app = create_test_app!(); + + // Create user via helper + let user = ctx.create_user(db, None, None).await.unwrap(); + + // Create project via helper + let project = ctx.create_project(db, None).await.unwrap(); + + // Test API endpoints + let user_resp = test::TestRequest::get() + .uri(&format!("/api/v1/user/{}", user.id)) + .send_request(&app) + .await; + assert!(user_resp.status().is_success()); + + let projects_resp = test::TestRequest::get() + .uri("/api/v1/project") + .send_request(&app) + .await; + assert!(projects_resp.status().is_success()); + + // Assert both exist in database + assert!(ctx.assert_user_exists(db, user.id).await); + assert!(ctx.assert_project_exists(db, &project.id).await); + + // All cleanup automatic + }); + } +} From 64d9e9d51ded6f1e164e9164a4efd1faf1490317 Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 13:41:40 +0200 Subject: [PATCH 4/8] race conditions fixed. still some tests failing --- Cargo.lock | 1 + crates/backend/Cargo.toml | 3 + crates/backend/tests/common/db_helpers/mod.rs | 4 +- crates/backend/tests/common/setup.rs | 14 +- crates/backend/tests/common/test_helpers.rs | 18 +- crates/backend/tests/endpoints/user.rs | 131 +++++++------- .../backend/tests/example_refactored_test.rs | 164 ------------------ 7 files changed, 103 insertions(+), 232 deletions(-) delete mode 100644 crates/backend/tests/example_refactored_test.rs diff --git a/Cargo.lock b/Cargo.lock index a0af772..eb49e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,6 +635,7 @@ dependencies = [ "lazy_static", "log", "migration", + "rand 0.8.5", "sea-orm", "serde", "serde_json", diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index b343e49..0ee4366 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -47,6 +47,9 @@ testcontainers-modules = { version = "0.12.1", features = [ "postgres", ] } +# Only needed for tests right now +rand = "*" + [features] serve = [] diff --git a/crates/backend/tests/common/db_helpers/mod.rs b/crates/backend/tests/common/db_helpers/mod.rs index d4f9e77..276446e 100644 --- a/crates/backend/tests/common/db_helpers/mod.rs +++ b/crates/backend/tests/common/db_helpers/mod.rs @@ -1,3 +1,3 @@ -pub mod auth_helpers; -pub mod project_helpers; pub mod user_helpers; +pub mod project_helpers; +pub mod auth_helpers; diff --git a/crates/backend/tests/common/setup.rs b/crates/backend/tests/common/setup.rs index 45e3e81..02a757f 100644 --- a/crates/backend/tests/common/setup.rs +++ b/crates/backend/tests/common/setup.rs @@ -1,11 +1,13 @@ use backend::{Database, build_database_url}; use log::{debug, info}; use migration::{Migrator, MigratorTrait}; +use sea_orm::ConnectOptions; use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; use testcontainers_modules::{postgres::Postgres, redis::Redis}; pub async fn setup() -> (ContainerAsync, ContainerAsync, Database) { let postgres = Postgres::default() + .with_tag("latest") .with_env_var("POSTGRES_DB", "test_db") .with_env_var("POSTGRES_USER", "postgres") .with_env_var("POSTGRES_PASSWORD", "postgres") @@ -14,6 +16,7 @@ pub async fn setup() -> (ContainerAsync, ContainerAsync, Databa .expect("Failed to start PostgreSQL container"); let redis = Redis::default() + .with_tag("latest") .start() .await .expect("Failed to start Redis container"); @@ -40,7 +43,16 @@ pub async fn setup() -> (ContainerAsync, ContainerAsync, Databa let database_url = build_database_url(); info!("Database URL: {}", database_url); - let database = Database::new(database_url.into()).await.unwrap(); + // Configure connection pool for tests + let mut opts = ConnectOptions::new(database_url); + opts.max_connections(200) + .min_connections(5) + .connect_timeout(std::time::Duration::from_secs(15)) + .acquire_timeout(std::time::Duration::from_secs(15)) + .idle_timeout(std::time::Duration::from_secs(30)) + .max_lifetime(std::time::Duration::from_secs(300)); + + let database = Database::new(opts).await.unwrap(); Migrator::up(database.connection(), None).await.unwrap(); diff --git a/crates/backend/tests/common/test_helpers.rs b/crates/backend/tests/common/test_helpers.rs index 8bc0c01..9254fb0 100644 --- a/crates/backend/tests/common/test_helpers.rs +++ b/crates/backend/tests/common/test_helpers.rs @@ -3,7 +3,6 @@ use lazy_static::lazy_static; use std::sync::atomic::{AtomicU64, Ordering}; use testcontainers::ContainerAsync; use testcontainers_modules::{postgres::Postgres, redis::Redis}; -use uuid::Uuid; use super::setup; @@ -60,6 +59,7 @@ impl UserFactory { } } +#[derive(Clone)] pub struct TestContext { pub test_id: String, pub created_users: std::sync::Arc>>, @@ -113,13 +113,19 @@ macro_rules! create_test_app { #[macro_export] macro_rules! with_test_context { ($test_fn:expr) => {{ - let ctx = $crate::common::test_helpers::TestContext::new(); - let db = $crate::common::test_helpers::get_database().await; + async { + let ctx = $crate::common::test_helpers::TestContext::new(); + let db = $crate::common::test_helpers::get_database().await; - let result = $test_fn(ctx.clone(), db).await; + let result = { + let ctx = &ctx; + let db = &*db; + $test_fn(ctx.clone(), db).await + }; - ctx.cleanup_all(db).await; + ctx.cleanup_all(db).await; - result + result + } }}; } diff --git a/crates/backend/tests/endpoints/user.rs b/crates/backend/tests/endpoints/user.rs index 87ab41b..390a589 100644 --- a/crates/backend/tests/endpoints/user.rs +++ b/crates/backend/tests/endpoints/user.rs @@ -1,7 +1,10 @@ use actix_web::{http::header, test}; use serde::{Deserialize, Serialize}; -use crate::{common::test_helpers::UserFactory, create_test_app}; +use crate::{ + common::test_helpers::{TestContext, UserFactory}, + create_test_app, +}; #[cfg(test)] mod tests { @@ -16,6 +19,9 @@ mod tests { #[actix_web::test] async fn test_create_user() { + let ctx = TestContext::new(); + let db = crate::common::test_helpers::get_database().await; + let app = create_test_app!(); let user_data = UserFactory::create_unique_request(); @@ -27,45 +33,40 @@ mod tests { .await; let status = resp.status(); + assert!( + status.is_success(), + "Expected success status, got: {}", + status + ); + let user: RespCreateUser = test::read_body_json(resp).await; - // Verify that the user was created with the expected structure assert!(!user.name.is_empty()); assert!(!user.username.is_empty()); assert!(user.username.starts_with("user_test_")); assert!(user.name.starts_with("Test User")); assert!(status.is_success()); - // Cleanup - delete the created user - let _delete_resp = test::TestRequest::delete() - .uri(&format!("/api/v1/user/{}", user.id)) - .send_request(&app) - .await; - // Don't assert on cleanup status in case of race conditions + let user_id = uuid::Uuid::parse_str(&user.id).unwrap(); + assert!(ctx.assert_user_exists(db, user_id).await); + + ctx.cleanup_all(db).await; } #[actix_web::test] async fn test_delete_user() { + let ctx = TestContext::new(); + let db = crate::common::test_helpers::get_database().await; + let app = create_test_app!(); - let user_data = UserFactory::create_unique_request(); - // Create user to delete - let create_resp = test::TestRequest::post() - .uri("/api/v1/user") - .insert_header(header::ContentType::json()) - .set_payload(user_data.to_string()) - .send_request(&app) - .await; + // Create user using helper + let user = ctx.create_user(db, None, None).await.unwrap(); - let create_status = create_resp.status(); - assert!( - create_status.is_success(), - "Failed to create user: {}", - create_status - ); - let user: RespCreateUser = test::read_body_json(create_resp).await; + // Verify user exists before deletion + assert!(ctx.assert_user_exists(db, user.id).await); - // Delete the user + // Delete the user via API let delete_resp = test::TestRequest::delete() .uri(&format!("/api/v1/user/{}", user.id)) .send_request(&app) @@ -79,71 +80,83 @@ mod tests { "Failed to delete user with status: {:?}", delete_status ); + + // Verify user no longer exists in database + assert!(ctx.assert_user_not_exists(db, user.id).await); + + // Cleanup + ctx.cleanup_all(db).await; } #[actix_web::test] async fn test_get_users() { + let ctx = TestContext::new(); + let db = crate::common::test_helpers::get_database().await; + let app = create_test_app!(); + // Create some test users + let users = ctx.create_multiple_users(db, 3).await.unwrap(); + assert_eq!(users.len(), 3); + + // Test the API endpoint let resp = test::TestRequest::get() .uri("/api/v1/user") .send_request(&app) .await; let status = resp.status(); - let users: Vec = test::read_body_json(resp).await; + let api_users: Vec = test::read_body_json(resp).await; assert!(status.is_success()); - assert!(users.is_empty() || !users.is_empty()); // Just verify it returns a valid array - } + assert!(api_users.len() >= 3); // At least our 3 users (could be more from other tests) - #[actix_web::test] - async fn test_create_user_duplicate_username() { - let app = create_test_app!(); - let user_data = UserFactory::create_unique_request(); + // Verify our users are in the response + for user in &users { + let found = api_users.iter().any(|api_user| { + api_user.id == user.id.to_string() + && api_user.username == user.username + && api_user.name == user.name + }); + assert!(found, "User {} not found in API response", user.username); + } - // Create first user - let resp1 = test::TestRequest::post() - .uri("/api/v1/user") - .insert_header(header::ContentType::json()) - .set_payload(user_data.to_string()) - .send_request(&app) - .await; - - let status1 = resp1.status(); - let user1: RespCreateUser = test::read_body_json(resp1).await; - assert!(status1.is_success()); - - // Try to create user with same username - let resp2 = test::TestRequest::post() - .uri("/api/v1/user") - .insert_header(header::ContentType::json()) - .set_payload(user_data.to_string()) - .send_request(&app) - .await; - - let status2 = resp2.status(); - assert!(status2.is_client_error() || status2.is_server_error()); + // Verify database consistency + assert!(ctx.assert_user_count(db, 3).await); // Cleanup - let _delete_resp = test::TestRequest::delete() - .uri(&format!("/api/v1/user/{}", user1.id)) - .send_request(&app) - .await; - // Don't assert on cleanup status in case of race conditions + ctx.cleanup_all(db).await; } #[actix_web::test] async fn test_delete_nonexistent_user() { + let ctx = TestContext::new(); + let db = crate::common::test_helpers::get_database().await; + let app = create_test_app!(); let fake_id = "00000000-0000-0000-0000-000000000000"; + // Verify the fake ID doesn't exist in database + let fake_uuid = uuid::Uuid::parse_str(fake_id).unwrap(); + assert!(ctx.assert_user_not_exists(db, fake_uuid).await); + + // Try to delete non-existent user let resp = test::TestRequest::delete() .uri(&format!("/api/v1/user/{}", fake_id)) .send_request(&app) .await; let status = resp.status(); - assert!(status.is_client_error() || status.is_server_error()); + assert!( + status.is_client_error() || status.is_server_error(), + "Expected error for non-existent user, got: {}", + status + ); + + // Verify it still doesn't exist + assert!(ctx.assert_user_not_exists(db, fake_uuid).await); + + // Cleanup + ctx.cleanup_all(db).await; } } diff --git a/crates/backend/tests/example_refactored_test.rs b/crates/backend/tests/example_refactored_test.rs deleted file mode 100644 index a9dae88..0000000 --- a/crates/backend/tests/example_refactored_test.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::{common::test_helpers::TestContext, create_test_app, with_test_context}; -use actix_web::test; -use backend::Database; - -#[cfg(test)] -mod tests { - use super::*; - - #[actix_web::test] - async fn test_user_crud_operations() { - with_test_context!(|ctx: TestContext, db: &Database| async move { - // Create single user - let user = ctx - .create_user( - db, - Some("testuser".to_string()), - Some("Test User".to_string()), - ) - .await - .unwrap(); - assert_eq!(user.username, "testuser"); - assert_eq!(user.name, "Test User"); - - // Assert user exists - assert!(ctx.assert_user_exists(db, user.id).await); - - // Get user by ID - let retrieved_user = ctx.get_user_by_id(db, user.id).await.unwrap().unwrap(); - assert_eq!(retrieved_user.id, user.id); - - // Create multiple users - let users = ctx.create_multiple_users(db, 3).await.unwrap(); - assert_eq!(users.len(), 3); - - // Assert user count (initial user + 3 new users = 4) - assert!(ctx.assert_user_count(db, 4).await); - - // Delete a user - ctx.delete_user(db, user.id).await.unwrap(); - assert!(ctx.assert_user_not_exists(db, user.id).await); - - // All cleanup handled automatically - }); - } - - #[actix_web::test] - async fn test_project_crud_operations() { - with_test_context!(|ctx: TestContext, db: &Database| async move { - // Create project with custom name - let project = ctx - .create_project_with_name(db, "My Test Project".to_string()) - .await - .unwrap(); - assert_eq!(project.name, "My Test Project"); - - // Assert project exists - assert!(ctx.assert_project_exists(db, &project.id).await); - - // Update project - let updated = ctx - .update_project(db, &project.id, "Updated Project Name".to_string()) - .await - .unwrap(); - assert_eq!(updated.name, "Updated Project Name"); - - // Assert project name changed - assert!( - ctx.assert_project_name(db, &project.id, "Updated Project Name") - .await - ); - - // Create multiple projects - let projects = ctx.create_multiple_projects(db, 2).await.unwrap(); - assert_eq!(projects.len(), 2); - - // Assert project count (1 original + 2 new = 3) - assert!(ctx.assert_project_count(db, 3).await); - - // All cleanup automatic - }); - } - - #[actix_web::test] - async fn test_authentication_operations() { - with_test_context!(|ctx: TestContext, db: &Database| async move { - // Create authenticated user - let (user, password) = ctx - .create_authenticated_user( - db, - Some("authuser".to_string()), - Some("securepass123".to_string()), - ) - .await - .unwrap(); - - // Test successful login - assert!( - ctx.assert_user_can_login(db, "authuser", "securepass123") - .await - ); - - // Test login returns correct user ID - assert!( - ctx.assert_user_login_returns_correct_id(db, "authuser", "securepass123", user.id) - .await - ); - - // Test failed login with wrong password - assert!( - ctx.assert_user_cannot_login(db, "authuser", "wrongpassword") - .await - ); - - // Test multiple invalid login attempts - let invalid_results = ctx.test_invalid_login_attempts(db, "authuser").await; - assert!(invalid_results.iter().all(|&result| result)); // All should fail (return true) - - // Create and verify multiple auth users - let auth_users = ctx - .create_multiple_authenticated_users(db, 2) - .await - .unwrap(); - assert_eq!(auth_users.len(), 2); - - for (user, pwd) in &auth_users { - assert!(ctx.assert_user_can_login(db, &user.username, pwd).await); - } - - // All cleanup automatic - }); - } - - #[actix_web::test] - async fn test_mixed_operations_with_api() { - with_test_context!(|ctx: TestContext, db: &Database| async move { - let app = create_test_app!(); - - // Create user via helper - let user = ctx.create_user(db, None, None).await.unwrap(); - - // Create project via helper - let project = ctx.create_project(db, None).await.unwrap(); - - // Test API endpoints - let user_resp = test::TestRequest::get() - .uri(&format!("/api/v1/user/{}", user.id)) - .send_request(&app) - .await; - assert!(user_resp.status().is_success()); - - let projects_resp = test::TestRequest::get() - .uri("/api/v1/project") - .send_request(&app) - .await; - assert!(projects_resp.status().is_success()); - - // Assert both exist in database - assert!(ctx.assert_user_exists(db, user.id).await); - assert!(ctx.assert_project_exists(db, &project.id).await); - - // All cleanup automatic - }); - } -} From 8fb44d0ad9d69b3453dee1031e054ce1e73155b5 Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 13:41:55 +0200 Subject: [PATCH 5/8] remove rand again after testing --- Cargo.lock | 1 - crates/backend/Cargo.toml | 3 --- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb49e76..a0af772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,7 +635,6 @@ dependencies = [ "lazy_static", "log", "migration", - "rand 0.8.5", "sea-orm", "serde", "serde_json", diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 0ee4366..b343e49 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -47,9 +47,6 @@ testcontainers-modules = { version = "0.12.1", features = [ "postgres", ] } -# Only needed for tests right now -rand = "*" - [features] serve = [] From 42dae815a6467e33d966a5fccbd8fc114c048fc1 Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 16:26:44 +0200 Subject: [PATCH 6/8] WIP: moved stuff around --- crates/backend/src/controller.rs | 2 +- crates/backend/src/controller/user.rs | 2 + .../tests/common/db_helpers/auth_helpers.rs | 2 + .../tests/common/db_helpers/user_helpers.rs | 30 ++--------- crates/backend/tests/common/setup.rs | 16 +++--- crates/backend/tests/common/test_helpers.rs | 51 +------------------ crates/backend/tests/endpoints/user.rs | 22 +++++--- 7 files changed, 34 insertions(+), 91 deletions(-) diff --git a/crates/backend/src/controller.rs b/crates/backend/src/controller.rs index 979c287..b5e90a0 100644 --- a/crates/backend/src/controller.rs +++ b/crates/backend/src/controller.rs @@ -16,6 +16,6 @@ pub fn register_controllers(cfg: &mut ServiceConfig) { .service(web::scope("/template").configure(template::setup)) .service(web::scope("/auth").configure(auth::setup)) .service( - web::resource("ok").to(|| async { actix_web::HttpResponse::Ok().body("available") }), + web::resource("/ok").to(|| async { actix_web::HttpResponse::Ok().body("available") }), ); } diff --git a/crates/backend/src/controller/user.rs b/crates/backend/src/controller/user.rs index 890147f..5b1ee93 100644 --- a/crates/backend/src/controller/user.rs +++ b/crates/backend/src/controller/user.rs @@ -78,10 +78,12 @@ async fn get_user( responses( (status = 200, description = "User created successfully", body = entity::user::Model, content_type = "application/json"), (status = 400, description = "Invalid request data or validation error", body = String, content_type = "application/json"), + (status = 409, description = "User already exists", body = String, content_type = "application/json"), (status = 500, description = "Internal server error", body = String, content_type = "application/json") ) )] #[post("")] +// TODO: if a user with the same username already exists, return 409 Conflict async fn create_user( db: web::Data, user: web::Json, diff --git a/crates/backend/tests/common/db_helpers/auth_helpers.rs b/crates/backend/tests/common/db_helpers/auth_helpers.rs index ecd1ae5..4bd788f 100644 --- a/crates/backend/tests/common/db_helpers/auth_helpers.rs +++ b/crates/backend/tests/common/db_helpers/auth_helpers.rs @@ -1,3 +1,4 @@ +/* use crate::common::test_helpers::TestContext; use backend::{Database, db::entity}; use uuid::Uuid; @@ -119,3 +120,4 @@ impl TestContext { Ok((user, can_login)) } } +*/ diff --git a/crates/backend/tests/common/db_helpers/user_helpers.rs b/crates/backend/tests/common/db_helpers/user_helpers.rs index 2bbe483..bbfc036 100644 --- a/crates/backend/tests/common/db_helpers/user_helpers.rs +++ b/crates/backend/tests/common/db_helpers/user_helpers.rs @@ -11,7 +11,7 @@ impl TestContext { ) -> Result { let test_id = &self.test_id; let username = username.unwrap_or_else(|| format!("user_{}", test_id)); - let name = name.unwrap_or_else(|| format!("Test User {}", test_id)); + let name = name.unwrap_or_else(|| format!("name_{}", test_id)); let password = "password123".to_string(); let user = db.create_user(name, username, password).await?; @@ -23,26 +23,6 @@ impl TestContext { Ok(user) } - pub async fn create_user_with_password( - &self, - db: &Database, - username: Option, - name: Option, - password: String, - ) -> Result { - let test_id = &self.test_id; - let username = username.unwrap_or_else(|| format!("user_{}", test_id)); - let name = name.unwrap_or_else(|| format!("Test User {}", test_id)); - - let user = db.create_user(name, username, password).await?; - - if let Ok(mut users) = self.created_users.lock() { - users.push(user.id); - } - - Ok(user) - } - pub async fn create_multiple_users( &self, db: &Database, @@ -52,7 +32,7 @@ impl TestContext { for i in 0..count { let username = format!("user_{}_{}", self.test_id, i); - let name = format!("Test User {} {}", self.test_id, i); + let name = format!("name_{}_{}", self.test_id, i); let user = self.create_user(db, Some(username), Some(name)).await?; users.push(user); } @@ -76,10 +56,8 @@ impl TestContext { } pub async fn assert_user_exists(&self, db: &Database, id: Uuid) -> bool { - match self.get_user_by_id(db, id).await { - Ok(Some(_)) => true, - _ => false, - } + dbg!("Check if user exists with ID: {}", id); + matches!(self.get_user_by_id(db, id).await, Ok(Some(_))) } pub async fn assert_user_count(&self, db: &Database, expected: usize) -> bool { diff --git a/crates/backend/tests/common/setup.rs b/crates/backend/tests/common/setup.rs index 02a757f..071a30f 100644 --- a/crates/backend/tests/common/setup.rs +++ b/crates/backend/tests/common/setup.rs @@ -1,3 +1,5 @@ +use std::{thread, time::Duration}; + use backend::{Database, build_database_url}; use log::{debug, info}; use migration::{Migrator, MigratorTrait}; @@ -29,6 +31,8 @@ pub async fn setup() -> (ContainerAsync, ContainerAsync, Databa // Wait for PostgreSQL to be ready wait_for_postgres_ready(&postgres).await; + dbg!("PostgreSQL is ready - Starting to sleep"); + thread::sleep(Duration::from_secs(10)); unsafe { std::env::set_var("DB_HOST", "127.0.0.1"); @@ -45,12 +49,12 @@ pub async fn setup() -> (ContainerAsync, ContainerAsync, Databa // Configure connection pool for tests let mut opts = ConnectOptions::new(database_url); - opts.max_connections(200) - .min_connections(5) - .connect_timeout(std::time::Duration::from_secs(15)) - .acquire_timeout(std::time::Duration::from_secs(15)) - .idle_timeout(std::time::Duration::from_secs(30)) - .max_lifetime(std::time::Duration::from_secs(300)); + opts.max_connections(10) + .min_connections(2) + .connect_timeout(std::time::Duration::from_secs(30)) + .acquire_timeout(std::time::Duration::from_secs(30)) + .idle_timeout(std::time::Duration::from_secs(60)) + .max_lifetime(std::time::Duration::from_secs(600)); let database = Database::new(opts).await.unwrap(); diff --git a/crates/backend/tests/common/test_helpers.rs b/crates/backend/tests/common/test_helpers.rs index 9254fb0..57149ef 100644 --- a/crates/backend/tests/common/test_helpers.rs +++ b/crates/backend/tests/common/test_helpers.rs @@ -39,24 +39,7 @@ pub fn get_unique_test_id() -> String { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(); - format!("test_{}_{}", timestamp, counter) -} - -pub struct UserFactory; - -impl UserFactory { - pub fn create_request(username: Option, name: Option) -> serde_json::Value { - let test_id = get_unique_test_id(); - serde_json::json!({ - "username": username.unwrap_or_else(|| format!("user_{}", test_id)), - "name": name.unwrap_or_else(|| format!("Test User {}", test_id)), - "password": "password123" - }) - } - - pub fn create_unique_request() -> serde_json::Value { - Self::create_request(None, None) - } + format!("test_{}_{}", counter, timestamp) } #[derive(Clone)] @@ -75,18 +58,6 @@ impl TestContext { } } - pub fn create_user_data( - &self, - username_prefix: Option<&str>, - name: Option<&str>, - ) -> serde_json::Value { - let username = username_prefix - .map(|prefix| format!("{}_{}", prefix, self.test_id)) - .unwrap_or_else(|| format!("user_{}", self.test_id)); - - UserFactory::create_request(Some(username), name.map(String::from)) - } - pub async fn cleanup_all(&self, db: &Database) { self.cleanup_projects(db).await; self.cleanup_users(db).await; @@ -109,23 +80,3 @@ macro_rules! create_test_app { .await }}; } - -#[macro_export] -macro_rules! with_test_context { - ($test_fn:expr) => {{ - async { - let ctx = $crate::common::test_helpers::TestContext::new(); - let db = $crate::common::test_helpers::get_database().await; - - let result = { - let ctx = &ctx; - let db = &*db; - $test_fn(ctx.clone(), db).await - }; - - ctx.cleanup_all(db).await; - - result - } - }}; -} diff --git a/crates/backend/tests/endpoints/user.rs b/crates/backend/tests/endpoints/user.rs index 390a589..d55a03d 100644 --- a/crates/backend/tests/endpoints/user.rs +++ b/crates/backend/tests/endpoints/user.rs @@ -1,10 +1,7 @@ use actix_web::{http::header, test}; use serde::{Deserialize, Serialize}; -use crate::{ - common::test_helpers::{TestContext, UserFactory}, - create_test_app, -}; +use crate::{common::test_helpers::TestContext, create_test_app}; #[cfg(test)] mod tests { @@ -19,11 +16,17 @@ mod tests { #[actix_web::test] async fn test_create_user() { - let ctx = TestContext::new(); + let ctx: TestContext = TestContext::new(); let db = crate::common::test_helpers::get_database().await; let app = create_test_app!(); - let user_data = UserFactory::create_unique_request(); + + // Create JSON payload using TestContext's ID + let user_data = serde_json::json!({ + "username": format!("user_{}", ctx.test_id), + "name": format!("Test User {}", ctx.test_id), + "password": "password123" + }); let resp = test::TestRequest::post() .uri("/api/v1/user") @@ -32,7 +35,9 @@ mod tests { .send_request(&app) .await; + dbg!(&resp); let status = resp.status(); + assert!( status.is_success(), "Expected success status, got: {}", @@ -60,10 +65,9 @@ mod tests { let app = create_test_app!(); - // Create user using helper let user = ctx.create_user(db, None, None).await.unwrap(); - // Verify user exists before deletion + // Check if user exists before deletion assert!(ctx.assert_user_exists(db, user.id).await); // Delete the user via API @@ -73,6 +77,8 @@ mod tests { .await; let delete_status = delete_resp.status(); + dbg!(&delete_resp); + let delete_message: String = test::read_body_json(delete_resp).await; assert_eq!(delete_message, format!("User {} deleted", user.id)); assert!( From 7033ad7013907dea524d77a22bdb4ef5d840a1ec Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 16:35:52 +0200 Subject: [PATCH 7/8] it WORKS --- crates/backend/tests/common/test_helpers.rs | 20 +++++++++++++------- crates/backend/tests/endpoints/user.rs | 14 ++++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/backend/tests/common/test_helpers.rs b/crates/backend/tests/common/test_helpers.rs index 57149ef..2873e96 100644 --- a/crates/backend/tests/common/test_helpers.rs +++ b/crates/backend/tests/common/test_helpers.rs @@ -9,26 +9,32 @@ use super::setup; struct TestState { _postgres: ContainerAsync, _redis: ContainerAsync, - database: Database, } lazy_static! { static ref TEST_STATE: tokio::sync::OnceCell = tokio::sync::OnceCell::new(); } -pub async fn get_database() -> &'static Database { - let state = TEST_STATE +pub async fn get_database() -> Database { + let _state = TEST_STATE .get_or_init(|| async { - let (postgres, redis, database) = setup().await; + let (postgres, redis, _database) = setup().await; TestState { _postgres: postgres, _redis: redis, - database, } }) .await; - &state.database + // Create a new database connection for each test + let database_url = backend::build_database_url(); + let mut opts = sea_orm::ConnectOptions::new(database_url); + opts.max_connections(5) + .min_connections(1) + .connect_timeout(std::time::Duration::from_secs(10)) + .acquire_timeout(std::time::Duration::from_secs(10)); + + Database::new(opts).await.unwrap() } static TEST_COUNTER: AtomicU64 = AtomicU64::new(1); @@ -71,7 +77,7 @@ macro_rules! create_test_app { actix_web::test::init_service( actix_web::App::new() - .app_data(actix_web::web::Data::new(db.clone())) + .app_data(actix_web::web::Data::new(db)) .service( actix_web::web::scope("/api/v1") .configure(backend::controller::register_controllers), diff --git a/crates/backend/tests/endpoints/user.rs b/crates/backend/tests/endpoints/user.rs index d55a03d..7b73272 100644 --- a/crates/backend/tests/endpoints/user.rs +++ b/crates/backend/tests/endpoints/user.rs @@ -17,7 +17,7 @@ mod tests { #[actix_web::test] async fn test_create_user() { let ctx: TestContext = TestContext::new(); - let db = crate::common::test_helpers::get_database().await; + let db = &crate::common::test_helpers::get_database().await; let app = create_test_app!(); @@ -61,7 +61,7 @@ mod tests { #[actix_web::test] async fn test_delete_user() { let ctx = TestContext::new(); - let db = crate::common::test_helpers::get_database().await; + let db = &crate::common::test_helpers::get_database().await; let app = create_test_app!(); @@ -97,7 +97,7 @@ mod tests { #[actix_web::test] async fn test_get_users() { let ctx = TestContext::new(); - let db = crate::common::test_helpers::get_database().await; + let db = &crate::common::test_helpers::get_database().await; let app = create_test_app!(); @@ -127,8 +127,10 @@ mod tests { assert!(found, "User {} not found in API response", user.username); } - // Verify database consistency - assert!(ctx.assert_user_count(db, 3).await); + // Verify our created users exist in database + for user in &users { + assert!(ctx.assert_user_exists(db, user.id).await); + } // Cleanup ctx.cleanup_all(db).await; @@ -137,7 +139,7 @@ mod tests { #[actix_web::test] async fn test_delete_nonexistent_user() { let ctx = TestContext::new(); - let db = crate::common::test_helpers::get_database().await; + let db = &crate::common::test_helpers::get_database().await; let app = create_test_app!(); let fake_id = "00000000-0000-0000-0000-000000000000"; From c98eb9cdbb99a954f68570f6652fac963309c71c Mon Sep 17 00:00:00 2001 From: Mika Date: Fri, 20 Jun 2025 17:03:08 +0200 Subject: [PATCH 8/8] add back TODO comment for username validation --- crates/backend/src/controller/user.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/backend/src/controller/user.rs b/crates/backend/src/controller/user.rs index 5b1ee93..3adee87 100644 --- a/crates/backend/src/controller/user.rs +++ b/crates/backend/src/controller/user.rs @@ -15,6 +15,7 @@ pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { pub struct CreateUser { #[validate(length(min = 4, max = 255))] /// Username (minimum 4 characters, maximum 255 characters) + /// TODO: Don't allow spaces, only alphanumeric characters and underscores username: String, #[validate(length(min = 3))] /// Full name of the user (minimum 3 characters)