User-Integration Tests and better infrastructure #28

Merged
mixel merged 8 commits from integration-test-addition into main 2025-06-20 17:03:40 +02:00
7 changed files with 34 additions and 91 deletions
Showing only changes of commit 42dae815a6 - Show all commits

View file

@ -16,6 +16,6 @@ pub fn register_controllers(cfg: &mut ServiceConfig) {
.service(web::scope("/template").configure(template::setup)) .service(web::scope("/template").configure(template::setup))
.service(web::scope("/auth").configure(auth::setup)) .service(web::scope("/auth").configure(auth::setup))
.service( .service(
web::resource("ok").to(|| async { actix_web::HttpResponse::Ok().body("available") }), web::resource("/ok").to(|| async { actix_web::HttpResponse::Ok().body("available") }),
); );
} }

View file

@ -78,10 +78,12 @@ async fn get_user(
responses( responses(
(status = 200, description = "User created successfully", body = entity::user::Model, content_type = "application/json"), (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 = 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") (status = 500, description = "Internal server error", body = String, content_type = "application/json")
) )
)] )]
#[post("")] #[post("")]
// TODO: if a user with the same username already exists, return 409 Conflict
async fn create_user( async fn create_user(
db: web::Data<Database>, db: web::Data<Database>,
user: web::Json<CreateUser>, user: web::Json<CreateUser>,

View file

@ -1,3 +1,4 @@
/*
use crate::common::test_helpers::TestContext; use crate::common::test_helpers::TestContext;
use backend::{Database, db::entity}; use backend::{Database, db::entity};
use uuid::Uuid; use uuid::Uuid;
@ -119,3 +120,4 @@ impl TestContext {
Ok((user, can_login)) Ok((user, can_login))
} }
} }
*/

View file

@ -11,7 +11,7 @@ impl TestContext {
) -> Result<entity::user::Model, backend::error::ApiError> { ) -> Result<entity::user::Model, backend::error::ApiError> {
let test_id = &self.test_id; let test_id = &self.test_id;
let username = username.unwrap_or_else(|| format!("user_{}", 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 password = "password123".to_string();
let user = db.create_user(name, username, password).await?; let user = db.create_user(name, username, password).await?;
@ -23,26 +23,6 @@ impl TestContext {
Ok(user) 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( pub async fn create_multiple_users(
&self, &self,
db: &Database, db: &Database,
@ -52,7 +32,7 @@ impl TestContext {
for i in 0..count { for i in 0..count {
let username = format!("user_{}_{}", self.test_id, i); 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?; let user = self.create_user(db, Some(username), Some(name)).await?;
users.push(user); users.push(user);
} }
@ -76,10 +56,8 @@ impl TestContext {
} }
pub async fn assert_user_exists(&self, db: &Database, id: Uuid) -> bool { pub async fn assert_user_exists(&self, db: &Database, id: Uuid) -> bool {
match self.get_user_by_id(db, id).await { dbg!("Check if user exists with ID: {}", id);
Ok(Some(_)) => true, matches!(self.get_user_by_id(db, id).await, Ok(Some(_)))
_ => false,
}
} }
pub async fn assert_user_count(&self, db: &Database, expected: usize) -> bool { pub async fn assert_user_count(&self, db: &Database, expected: usize) -> bool {

View file

@ -1,3 +1,5 @@
use std::{thread, time::Duration};
use backend::{Database, build_database_url}; use backend::{Database, build_database_url};
use log::{debug, info}; use log::{debug, info};
use migration::{Migrator, MigratorTrait}; use migration::{Migrator, MigratorTrait};
@ -29,6 +31,8 @@ pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Databa
// Wait for PostgreSQL to be ready // Wait for PostgreSQL to be ready
wait_for_postgres_ready(&postgres).await; wait_for_postgres_ready(&postgres).await;
dbg!("PostgreSQL is ready - Starting to sleep");
thread::sleep(Duration::from_secs(10));
unsafe { unsafe {
std::env::set_var("DB_HOST", "127.0.0.1"); std::env::set_var("DB_HOST", "127.0.0.1");
@ -45,12 +49,12 @@ pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Databa
// Configure connection pool for tests // Configure connection pool for tests
let mut opts = ConnectOptions::new(database_url); let mut opts = ConnectOptions::new(database_url);
opts.max_connections(200) opts.max_connections(10)
.min_connections(5) .min_connections(2)
.connect_timeout(std::time::Duration::from_secs(15)) .connect_timeout(std::time::Duration::from_secs(30))
.acquire_timeout(std::time::Duration::from_secs(15)) .acquire_timeout(std::time::Duration::from_secs(30))
.idle_timeout(std::time::Duration::from_secs(30)) .idle_timeout(std::time::Duration::from_secs(60))
.max_lifetime(std::time::Duration::from_secs(300)); .max_lifetime(std::time::Duration::from_secs(600));
let database = Database::new(opts).await.unwrap(); let database = Database::new(opts).await.unwrap();

View file

@ -39,24 +39,7 @@ pub fn get_unique_test_id() -> String {
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap()
.as_millis(); .as_millis();
format!("test_{}_{}", timestamp, counter) format!("test_{}_{}", counter, timestamp)
}
pub struct UserFactory;
impl UserFactory {
pub fn create_request(username: Option<String>, name: Option<String>) -> 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)
}
} }
#[derive(Clone)] #[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) { pub async fn cleanup_all(&self, db: &Database) {
self.cleanup_projects(db).await; self.cleanup_projects(db).await;
self.cleanup_users(db).await; self.cleanup_users(db).await;
@ -109,23 +80,3 @@ macro_rules! create_test_app {
.await .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
}
}};
}

View file

@ -1,10 +1,7 @@
use actix_web::{http::header, test}; use actix_web::{http::header, test};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{common::test_helpers::TestContext, create_test_app};
common::test_helpers::{TestContext, UserFactory},
create_test_app,
};
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -19,11 +16,17 @@ mod tests {
#[actix_web::test] #[actix_web::test]
async fn test_create_user() { async fn test_create_user() {
let ctx = TestContext::new(); 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!(); 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() let resp = test::TestRequest::post()
.uri("/api/v1/user") .uri("/api/v1/user")
@ -32,7 +35,9 @@ mod tests {
.send_request(&app) .send_request(&app)
.await; .await;
dbg!(&resp);
let status = resp.status(); let status = resp.status();
assert!( assert!(
status.is_success(), status.is_success(),
"Expected success status, got: {}", "Expected success status, got: {}",
@ -60,10 +65,9 @@ mod tests {
let app = create_test_app!(); let app = create_test_app!();
// Create user using helper
let user = ctx.create_user(db, None, None).await.unwrap(); 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); assert!(ctx.assert_user_exists(db, user.id).await);
// Delete the user via API // Delete the user via API
@ -73,6 +77,8 @@ mod tests {
.await; .await;
let delete_status = delete_resp.status(); let delete_status = delete_resp.status();
dbg!(&delete_resp);
let delete_message: String = test::read_body_json(delete_resp).await; let delete_message: String = test::read_body_json(delete_resp).await;
assert_eq!(delete_message, format!("User {} deleted", user.id)); assert_eq!(delete_message, format!("User {} deleted", user.id));
assert!( assert!(