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 - }); - } -}