add more sophistiated integration_test setup
This commit is contained in:
parent
51945ee955
commit
74a0c0f079
7 changed files with 124 additions and 175 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -632,6 +632,7 @@ dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"migration",
|
"migration",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
43
crates/backend/tests/common/mod.rs
Normal file
43
crates/backend/tests/common/mod.rs
Normal 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)
|
||||||
|
}
|
47
crates/backend/tests/common/test_helpers.rs
Normal file
47
crates/backend/tests/common/test_helpers.rs
Normal 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
|
||||||
|
}
|
9
crates/backend/tests/endpoints/auth.rs
Normal file
9
crates/backend/tests/endpoints/auth.rs
Normal 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::*;
|
||||||
|
}
|
6
crates/backend/tests/endpoints/mod.rs
Normal file
6
crates/backend/tests/endpoints/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod auth;
|
||||||
|
// pub mod class;
|
||||||
|
// pub mod group;
|
||||||
|
// pub mod project;
|
||||||
|
// pub mod template;
|
||||||
|
// pub mod user;
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue