add more sophistiated integration_test setup

This commit is contained in:
Mika 2025-06-17 11:27:43 +02:00
parent 51945ee955
commit 74a0c0f079
7 changed files with 124 additions and 175 deletions

1
Cargo.lock generated
View file

@ -632,6 +632,7 @@ dependencies = [
"argon2", "argon2",
"dotenvy", "dotenvy",
"env_logger", "env_logger",
"lazy_static",
"log", "log",
"migration", "migration",
"sea-orm", "sea-orm",

View file

@ -38,6 +38,7 @@ temp-env = "*"
serial_test = "*" serial_test = "*"
tokio = { version = "1", features = ["time"] } tokio = { version = "1", features = ["time"] }
serde_json = "1" serde_json = "1"
lazy_static = "1.5"
# Testcontainers # Testcontainers
testcontainers = { version = "0.24" } testcontainers = { version = "0.24" }

View file

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

View file

@ -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<Postgres>,
_redis: ContainerAsync<Redis>,
database: Database,
}
lazy_static! {
static ref TEST_STATE: tokio::sync::OnceCell<TestState> = 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<F, Fut, R>(test: F) -> R
where
F: FnOnce(DatabaseTransaction) -> Fut,
Fut: Future<Output = R>,
{
let db = get_database().await;
let tx = db
.connection()
.begin()
.await
.expect("Failed to start transaction");
test(tx).await
}

View file

@ -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::*;
}

View file

@ -0,0 +1,6 @@
pub mod auth;
// pub mod class;
// pub mod group;
// pub mod project;
// pub mod template;
// pub mod user;

View file

@ -1,188 +1,30 @@
use actix_web::{App, test, web}; mod common;
use backend::{Database, build_database_url, controller}; mod endpoints;
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<Postgres>, ContainerAsync<Redis>, 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)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use actix_web::{App, test, web};
use actix_web::http::StatusCode; use backend::controller;
use crate::common::test_helpers::{get_database, with_transaction};
#[actix_web::test] #[actix_web::test]
#[serial] async fn test_auth_with_transaction() {
async fn test_user_crud_flow() { with_transaction(|_tx| async {
let (_pg_container, _redis_container, database) = setup_test_environment().await; let db = get_database().await;
let app = test::init_service( let app = test::init_service(
App::new() App::new()
.app_data(web::Data::new(database.clone())) .app_data(web::Data::new(db.clone()))
.service(web::scope("/api/v1").configure(controller::register_controllers)), .service(web::scope("/api/v1").configure(controller::register_controllers)),
) )
.await; .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 req = test::TestRequest::get().uri("/api/v1/user").to_request();
let resp = test::call_service(&app, req).await; 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!(resp.status().is_success() || resp.status().is_client_error());
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)),
)
.await; .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
);
}
} }
} }