refactor: integration-tests and format whole project with cargo fmt
This commit is contained in:
parent
d477a5f286
commit
9aa9b49318
18 changed files with 665 additions and 94 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -66,4 +66,3 @@ impl MessageResponse {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -61,4 +61,4 @@ impl ApiDoc {
|
|||
pub fn openapi_spec() -> utoipa::openapi::OpenApi {
|
||||
Self::openapi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
121
crates/backend/tests/common/db_helpers/auth_helpers.rs
Normal file
121
crates/backend/tests/common/db_helpers/auth_helpers.rs
Normal file
|
@ -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<String>,
|
||||
name: Option<String>,
|
||||
password: String,
|
||||
) -> Result<entity::user::Model, backend::error::ApiError> {
|
||||
self.create_user_with_password(db, username, name, password)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn verify_user_login(
|
||||
&self,
|
||||
db: &Database,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Uuid, backend::error::ApiError> {
|
||||
db.verify_local_user(username, password).await
|
||||
}
|
||||
|
||||
pub async fn create_authenticated_user(
|
||||
&self,
|
||||
db: &Database,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> 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<Vec<(entity::user::Model, String)>, 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<bool> {
|
||||
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<String>,
|
||||
password: Option<String>,
|
||||
) -> 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))
|
||||
}
|
||||
}
|
3
crates/backend/tests/common/db_helpers/mod.rs
Normal file
3
crates/backend/tests/common/db_helpers/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod auth_helpers;
|
||||
pub mod project_helpers;
|
||||
pub mod user_helpers;
|
125
crates/backend/tests/common/db_helpers/project_helpers.rs
Normal file
125
crates/backend/tests/common/db_helpers/project_helpers.rs
Normal file
|
@ -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<String>,
|
||||
) -> Result<entity::project::Model, backend::error::ApiError> {
|
||||
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<entity::project::Model, backend::error::ApiError> {
|
||||
self.create_project(db, Some(name)).await
|
||||
}
|
||||
|
||||
pub async fn create_multiple_projects(
|
||||
&self,
|
||||
db: &Database,
|
||||
count: usize,
|
||||
) -> Result<Vec<entity::project::Model>, 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<Option<entity::project::Model>, backend::error::ApiError> {
|
||||
db.get_project(id).await
|
||||
}
|
||||
|
||||
pub async fn get_all_projects(
|
||||
&self,
|
||||
db: &Database,
|
||||
) -> Result<Vec<entity::project::Model>, backend::error::ApiError> {
|
||||
db.get_projects().await
|
||||
}
|
||||
|
||||
pub async fn update_project(
|
||||
&self,
|
||||
db: &Database,
|
||||
id: &Uuid,
|
||||
name: String,
|
||||
) -> Result<entity::project::Model, backend::error::ApiError> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
121
crates/backend/tests/common/db_helpers/user_helpers.rs
Normal file
121
crates/backend/tests/common/db_helpers/user_helpers.rs
Normal file
|
@ -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<String>,
|
||||
name: Option<String>,
|
||||
) -> Result<entity::user::Model, backend::error::ApiError> {
|
||||
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<String>,
|
||||
name: Option<String>,
|
||||
password: String,
|
||||
) -> Result<entity::user::Model, backend::error::ApiError> {
|
||||
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<Vec<entity::user::Model>, 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<Option<entity::user::Model>, backend::error::ApiError> {
|
||||
db.get_user(id).await
|
||||
}
|
||||
|
||||
pub async fn get_all_users(
|
||||
&self,
|
||||
db: &Database,
|
||||
) -> Result<Vec<entity::user::Model>, 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Postgres>, ContainerAsync<Redis>, 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<Postgres>) {
|
||||
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;
|
||||
|
|
74
crates/backend/tests/common/setup.rs
Normal file
74
crates/backend/tests/common/setup.rs
Normal file
|
@ -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<Postgres>, ContainerAsync<Redis>, 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<Postgres>) {
|
||||
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");
|
||||
}
|
|
@ -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<std::sync::Mutex<Vec<uuid::Uuid>>>,
|
||||
pub created_projects: std::sync::Arc<std::sync::Mutex<Vec<uuid::Uuid>>>,
|
||||
}
|
||||
|
||||
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
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
164
crates/backend/tests/example_refactored_test.rs
Normal file
164
crates/backend/tests/example_refactored_test.rs
Normal file
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue