diff --git a/Cargo.lock b/Cargo.lock index 5a00126..a0af772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,7 @@ dependencies = [ "argon2", "dotenvy", "env_logger", + "lazy_static", "log", "migration", "sea-orm", diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 0cff6ba..b343e49 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -38,6 +38,7 @@ temp-env = "*" serial_test = "*" tokio = { version = "1", features = ["time"] } serde_json = "1" +lazy_static = "1.5" # Testcontainers testcontainers = { version = "0.24" } diff --git a/crates/backend/tests/common/mod.rs b/crates/backend/tests/common/mod.rs new file mode 100644 index 0000000..617e62f --- /dev/null +++ b/crates/backend/tests/common/mod.rs @@ -0,0 +1,43 @@ +use backend::{Database, build_database_url}; +use migration::{Migrator, MigratorTrait}; +use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; +use testcontainers_modules::{postgres::Postgres, redis::Redis}; + +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(); + + 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()); + } + + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + let database_url = build_database_url(); + let database = Database::new(database_url.into()).await.unwrap(); + + Migrator::up(database.connection(), None).await.unwrap(); + + (postgres, redis, database) +} diff --git a/crates/backend/tests/common/test_helpers.rs b/crates/backend/tests/common/test_helpers.rs new file mode 100644 index 0000000..c61eef6 --- /dev/null +++ b/crates/backend/tests/common/test_helpers.rs @@ -0,0 +1,47 @@ +use backend::Database; +use lazy_static::lazy_static; +use sea_orm::{DatabaseTransaction, TransactionTrait}; +use std::future::Future; +use testcontainers::ContainerAsync; +use testcontainers_modules::{postgres::Postgres, redis::Redis}; + +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 + .get_or_init(|| async { + let (postgres, redis, database) = setup().await; + TestState { + _postgres: postgres, + _redis: redis, + database, + } + }) + .await; + + &state.database +} + +pub async fn with_transaction(test: F) -> R +where + F: FnOnce(DatabaseTransaction) -> Fut, + Fut: Future, +{ + let db = get_database().await; + let tx = db + .connection() + .begin() + .await + .expect("Failed to start transaction"); + test(tx).await +} diff --git a/crates/backend/tests/endpoints/auth.rs b/crates/backend/tests/endpoints/auth.rs new file mode 100644 index 0000000..a887338 --- /dev/null +++ b/crates/backend/tests/endpoints/auth.rs @@ -0,0 +1,9 @@ +use actix_web::{App, test, web}; +use backend::controller; + +use crate::common::test_helpers::{get_database, with_transaction}; + +#[cfg(test)] +mod tests { + use super::*; +} diff --git a/crates/backend/tests/endpoints/mod.rs b/crates/backend/tests/endpoints/mod.rs new file mode 100644 index 0000000..5618255 --- /dev/null +++ b/crates/backend/tests/endpoints/mod.rs @@ -0,0 +1,6 @@ +pub mod auth; +// pub mod class; +// pub mod group; +// pub mod project; +// pub mod template; +// pub mod user; diff --git a/crates/backend/tests/integration_tests.rs b/crates/backend/tests/integration_tests.rs index c809cb5..996e997 100644 --- a/crates/backend/tests/integration_tests.rs +++ b/crates/backend/tests/integration_tests.rs @@ -1,188 +1,30 @@ -use actix_web::{App, test, web}; -use backend::{Database, build_database_url, controller}; -use migration::{Migrator, MigratorTrait}; -use serde_json::json; -use serial_test::serial; -use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}; -use testcontainers_modules::{postgres::Postgres, redis::Redis}; - -async fn setup_test_environment() -> (ContainerAsync, ContainerAsync, Database) { - // Start PostgreSQL container - let postgres_container = Postgres::default() - .with_env_var("POSTGRES_DB", "test_db") - .start() - .await - .unwrap(); - - let postgres_port = postgres_container.get_host_port_ipv4(5432).await.unwrap(); - - // Start Redis container - let redis_container = Redis::default().start().await.unwrap(); - let redis_port = redis_container.get_host_port_ipv4(6379).await.unwrap(); - - // Set environment variables for the application - 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()); - } - - // Wait a bit for containers to be ready - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - - // Use the existing build_database_url function - let database_url = build_database_url(); - let database = Database::new(database_url.into()).await.unwrap(); - - // Run migrations - Migrator::up(database.connection(), None).await.unwrap(); - - (postgres_container, redis_container, database) -} +mod common; +mod endpoints; #[cfg(test)] mod tests { - use super::*; - use actix_web::http::StatusCode; + use actix_web::{App, test, web}; + use backend::controller; + + use crate::common::test_helpers::{get_database, with_transaction}; #[actix_web::test] - #[serial] - async fn test_user_crud_flow() { - let (_pg_container, _redis_container, database) = setup_test_environment().await; + async fn test_auth_with_transaction() { + with_transaction(|_tx| async { + let db = get_database().await; - let app = test::init_service( - App::new() - .app_data(web::Data::new(database.clone())) - .service(web::scope("/api/v1").configure(controller::register_controllers)), - ) - .await; + let app = test::init_service( + App::new() + .app_data(web::Data::new(db.clone())) + .service(web::scope("/api/v1").configure(controller::register_controllers)), + ) + .await; - // Test creating a user - let create_user_payload = json!({ - "username": "testuser", - "email": "test@example.com", - "first_name": "Test", - "last_name": "User" - }); - - let req = test::TestRequest::post() - .uri("/api/v1/user") - .set_json(&create_user_payload) - .to_request(); - - let resp = test::call_service(&app, req).await; - - // Log response for debugging - let status = resp.status(); - let body = test::read_body(resp).await; - println!( - "Create user response: {} - {}", - status, - String::from_utf8_lossy(&body) - ); - - if status != StatusCode::CREATED { - // Try to get users list to see what endpoints are available let req = test::TestRequest::get().uri("/api/v1/user").to_request(); let resp = test::call_service(&app, req).await; - let resp_status = resp.status(); - let body = test::read_body(resp).await; - println!( - "Get users response: {} - {}", - resp_status, - String::from_utf8_lossy(&body) - ); - } - // For now, just verify the API is responding - assert!(status.is_success() || status.is_client_error()); - } - - #[actix_web::test] - #[serial] - async fn test_api_endpoints_respond() { - let (_pg_container, _redis_container, database) = setup_test_environment().await; - - let app = test::init_service( - App::new() - .app_data(web::Data::new(database.clone())) - .service(web::scope("/api/v1").configure(controller::register_controllers)), - ) + assert!(resp.status().is_success() || resp.status().is_client_error()); + }) .await; - - // Test various endpoints to ensure they respond - let endpoints = vec![ - "/api/v1/user", - "/api/v1/project", - "/api/v1/group", - "/api/v1/class", - "/api/v1/template", - ]; - - for endpoint in endpoints { - let req = test::TestRequest::get().uri(endpoint).to_request(); - - let resp = test::call_service(&app, req).await; - let status = resp.status(); - - println!("Endpoint {} responded with status: {}", endpoint, status); - - // Verify endpoint is reachable (not 404) - assert_ne!( - status, - StatusCode::NOT_FOUND, - "Endpoint {} should exist", - endpoint - ); - } - } - - #[actix_web::test] - #[serial] - async fn test_database_connection() { - let (_pg_container, _redis_container, database) = setup_test_environment().await; - - // Test that we can connect to the database - let connection = database.connection(); - assert!( - connection.ping().await.is_ok(), - "Database should be reachable" - ); - } - - #[actix_web::test] - #[serial] - async fn test_invalid_endpoints_return_404() { - let (_pg_container, _redis_container, database) = setup_test_environment().await; - - let app = test::init_service( - App::new() - .app_data(web::Data::new(database.clone())) - .service(web::scope("/api/v1").configure(controller::register_controllers)), - ) - .await; - - // Test non-existent endpoints - let invalid_endpoints = vec![ - "/api/v1/nonexistent", - "/api/v1/user/invalid/path", - "/api/v2/user", - ]; - - for endpoint in invalid_endpoints { - let req = test::TestRequest::get().uri(endpoint).to_request(); - - let resp = test::call_service(&app, req).await; - assert_eq!( - resp.status(), - StatusCode::NOT_FOUND, - "Invalid endpoint {} should return 404", - endpoint - ); - } } }