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/db/project.rs b/crates/backend/src/db/project.rs index 760833f..05f06a5 100644 --- a/crates/backend/src/db/project.rs +++ b/crates/backend/src/db/project.rs @@ -6,9 +6,9 @@ use crate::db::entity::project; use sea_orm::ActiveValue::{NotSet, Set, Unchanged}; use sea_orm::{ActiveModelTrait, DeleteResult, EntityTrait}; use serde::Deserialize; +use utoipa::ToSchema; use uuid::Uuid; use validator::Validate; -use utoipa::ToSchema; #[derive(Deserialize, Validate, ToSchema)] pub struct CreateProject { 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 + }); + } +}