User-Integration Tests and better infrastructure #28
7 changed files with 103 additions and 232 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -635,6 +635,7 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"migration",
|
"migration",
|
||||||
|
"rand 0.8.5",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -47,6 +47,9 @@ testcontainers-modules = { version = "0.12.1", features = [
|
||||||
"postgres",
|
"postgres",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
# Only needed for tests right now
|
||||||
|
rand = "*"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
serve = []
|
serve = []
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
pub mod auth_helpers;
|
|
||||||
pub mod project_helpers;
|
|
||||||
pub mod user_helpers;
|
pub mod user_helpers;
|
||||||
|
pub mod project_helpers;
|
||||||
|
pub mod auth_helpers;
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
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};
|
||||||
|
use sea_orm::ConnectOptions;
|
||||||
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
|
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
|
||||||
use testcontainers_modules::{postgres::Postgres, redis::Redis};
|
use testcontainers_modules::{postgres::Postgres, redis::Redis};
|
||||||
|
|
||||||
pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Database) {
|
pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Database) {
|
||||||
let postgres = Postgres::default()
|
let postgres = Postgres::default()
|
||||||
|
.with_tag("latest")
|
||||||
.with_env_var("POSTGRES_DB", "test_db")
|
.with_env_var("POSTGRES_DB", "test_db")
|
||||||
.with_env_var("POSTGRES_USER", "postgres")
|
.with_env_var("POSTGRES_USER", "postgres")
|
||||||
.with_env_var("POSTGRES_PASSWORD", "postgres")
|
.with_env_var("POSTGRES_PASSWORD", "postgres")
|
||||||
|
@ -14,6 +16,7 @@ pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Databa
|
||||||
.expect("Failed to start PostgreSQL container");
|
.expect("Failed to start PostgreSQL container");
|
||||||
|
|
||||||
let redis = Redis::default()
|
let redis = Redis::default()
|
||||||
|
.with_tag("latest")
|
||||||
.start()
|
.start()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to start Redis container");
|
.expect("Failed to start Redis container");
|
||||||
|
@ -40,7 +43,16 @@ pub async fn setup() -> (ContainerAsync<Postgres>, ContainerAsync<Redis>, Databa
|
||||||
let database_url = build_database_url();
|
let database_url = build_database_url();
|
||||||
info!("Database URL: {}", database_url);
|
info!("Database URL: {}", database_url);
|
||||||
|
|
||||||
let database = Database::new(database_url.into()).await.unwrap();
|
// Configure connection pool for tests
|
||||||
|
let mut opts = ConnectOptions::new(database_url);
|
||||||
|
opts.max_connections(200)
|
||||||
|
.min_connections(5)
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(15))
|
||||||
|
.acquire_timeout(std::time::Duration::from_secs(15))
|
||||||
|
.idle_timeout(std::time::Duration::from_secs(30))
|
||||||
|
.max_lifetime(std::time::Duration::from_secs(300));
|
||||||
|
|
||||||
|
let database = Database::new(opts).await.unwrap();
|
||||||
|
|
||||||
Migrator::up(database.connection(), None).await.unwrap();
|
Migrator::up(database.connection(), None).await.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ use lazy_static::lazy_static;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use testcontainers::ContainerAsync;
|
use testcontainers::ContainerAsync;
|
||||||
use testcontainers_modules::{postgres::Postgres, redis::Redis};
|
use testcontainers_modules::{postgres::Postgres, redis::Redis};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use super::setup;
|
use super::setup;
|
||||||
|
|
||||||
|
@ -60,6 +59,7 @@ impl UserFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct TestContext {
|
pub struct TestContext {
|
||||||
pub test_id: String,
|
pub test_id: String,
|
||||||
pub created_users: std::sync::Arc<std::sync::Mutex<Vec<uuid::Uuid>>>,
|
pub created_users: std::sync::Arc<std::sync::Mutex<Vec<uuid::Uuid>>>,
|
||||||
|
@ -113,13 +113,19 @@ macro_rules! create_test_app {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! with_test_context {
|
macro_rules! with_test_context {
|
||||||
($test_fn:expr) => {{
|
($test_fn:expr) => {{
|
||||||
|
async {
|
||||||
let ctx = $crate::common::test_helpers::TestContext::new();
|
let ctx = $crate::common::test_helpers::TestContext::new();
|
||||||
let db = $crate::common::test_helpers::get_database().await;
|
let db = $crate::common::test_helpers::get_database().await;
|
||||||
|
|
||||||
let result = $test_fn(ctx.clone(), db).await;
|
let result = {
|
||||||
|
let ctx = &ctx;
|
||||||
|
let db = &*db;
|
||||||
|
$test_fn(ctx.clone(), db).await
|
||||||
|
};
|
||||||
|
|
||||||
ctx.cleanup_all(db).await;
|
ctx.cleanup_all(db).await;
|
||||||
|
|
||||||
result
|
result
|
||||||
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use actix_web::{http::header, test};
|
use actix_web::{http::header, test};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{common::test_helpers::UserFactory, create_test_app};
|
use crate::{
|
||||||
|
common::test_helpers::{TestContext, UserFactory},
|
||||||
|
create_test_app,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@ -16,6 +19,9 @@ mod tests {
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_create_user() {
|
async fn test_create_user() {
|
||||||
|
let ctx = TestContext::new();
|
||||||
|
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();
|
let user_data = UserFactory::create_unique_request();
|
||||||
|
|
||||||
|
@ -27,45 +33,40 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
|
assert!(
|
||||||
|
status.is_success(),
|
||||||
|
"Expected success status, got: {}",
|
||||||
|
status
|
||||||
|
);
|
||||||
|
|
||||||
let user: RespCreateUser = test::read_body_json(resp).await;
|
let user: RespCreateUser = test::read_body_json(resp).await;
|
||||||
|
|
||||||
// Verify that the user was created with the expected structure
|
|
||||||
assert!(!user.name.is_empty());
|
assert!(!user.name.is_empty());
|
||||||
assert!(!user.username.is_empty());
|
assert!(!user.username.is_empty());
|
||||||
assert!(user.username.starts_with("user_test_"));
|
assert!(user.username.starts_with("user_test_"));
|
||||||
assert!(user.name.starts_with("Test User"));
|
assert!(user.name.starts_with("Test User"));
|
||||||
assert!(status.is_success());
|
assert!(status.is_success());
|
||||||
|
|
||||||
// Cleanup - delete the created user
|
let user_id = uuid::Uuid::parse_str(&user.id).unwrap();
|
||||||
let _delete_resp = test::TestRequest::delete()
|
assert!(ctx.assert_user_exists(db, user_id).await);
|
||||||
.uri(&format!("/api/v1/user/{}", user.id))
|
|
||||||
.send_request(&app)
|
ctx.cleanup_all(db).await;
|
||||||
.await;
|
|
||||||
// Don't assert on cleanup status in case of race conditions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_delete_user() {
|
async fn test_delete_user() {
|
||||||
|
let ctx = TestContext::new();
|
||||||
|
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 user to delete
|
// Create user using helper
|
||||||
let create_resp = test::TestRequest::post()
|
let user = ctx.create_user(db, None, None).await.unwrap();
|
||||||
.uri("/api/v1/user")
|
|
||||||
.insert_header(header::ContentType::json())
|
|
||||||
.set_payload(user_data.to_string())
|
|
||||||
.send_request(&app)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let create_status = create_resp.status();
|
// Verify user exists before deletion
|
||||||
assert!(
|
assert!(ctx.assert_user_exists(db, user.id).await);
|
||||||
create_status.is_success(),
|
|
||||||
"Failed to create user: {}",
|
|
||||||
create_status
|
|
||||||
);
|
|
||||||
let user: RespCreateUser = test::read_body_json(create_resp).await;
|
|
||||||
|
|
||||||
// Delete the user
|
// Delete the user via API
|
||||||
let delete_resp = test::TestRequest::delete()
|
let delete_resp = test::TestRequest::delete()
|
||||||
.uri(&format!("/api/v1/user/{}", user.id))
|
.uri(&format!("/api/v1/user/{}", user.id))
|
||||||
.send_request(&app)
|
.send_request(&app)
|
||||||
|
@ -79,71 +80,83 @@ mod tests {
|
||||||
"Failed to delete user with status: {:?}",
|
"Failed to delete user with status: {:?}",
|
||||||
delete_status
|
delete_status
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Verify user no longer exists in database
|
||||||
|
assert!(ctx.assert_user_not_exists(db, user.id).await);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
ctx.cleanup_all(db).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_get_users() {
|
async fn test_get_users() {
|
||||||
|
let ctx = TestContext::new();
|
||||||
|
let db = crate::common::test_helpers::get_database().await;
|
||||||
|
|
||||||
let app = create_test_app!();
|
let app = create_test_app!();
|
||||||
|
|
||||||
|
// Create some test users
|
||||||
|
let users = ctx.create_multiple_users(db, 3).await.unwrap();
|
||||||
|
assert_eq!(users.len(), 3);
|
||||||
|
|
||||||
|
// Test the API endpoint
|
||||||
let resp = test::TestRequest::get()
|
let resp = test::TestRequest::get()
|
||||||
.uri("/api/v1/user")
|
.uri("/api/v1/user")
|
||||||
.send_request(&app)
|
.send_request(&app)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let users: Vec<RespCreateUser> = test::read_body_json(resp).await;
|
let api_users: Vec<RespCreateUser> = test::read_body_json(resp).await;
|
||||||
|
|
||||||
assert!(status.is_success());
|
assert!(status.is_success());
|
||||||
assert!(users.is_empty() || !users.is_empty()); // Just verify it returns a valid array
|
assert!(api_users.len() >= 3); // At least our 3 users (could be more from other tests)
|
||||||
|
|
||||||
|
// Verify our users are in the response
|
||||||
|
for user in &users {
|
||||||
|
let found = api_users.iter().any(|api_user| {
|
||||||
|
api_user.id == user.id.to_string()
|
||||||
|
&& api_user.username == user.username
|
||||||
|
&& api_user.name == user.name
|
||||||
|
});
|
||||||
|
assert!(found, "User {} not found in API response", user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
// Verify database consistency
|
||||||
async fn test_create_user_duplicate_username() {
|
assert!(ctx.assert_user_count(db, 3).await);
|
||||||
let app = create_test_app!();
|
|
||||||
let user_data = UserFactory::create_unique_request();
|
|
||||||
|
|
||||||
// Create first user
|
|
||||||
let resp1 = test::TestRequest::post()
|
|
||||||
.uri("/api/v1/user")
|
|
||||||
.insert_header(header::ContentType::json())
|
|
||||||
.set_payload(user_data.to_string())
|
|
||||||
.send_request(&app)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let status1 = resp1.status();
|
|
||||||
let user1: RespCreateUser = test::read_body_json(resp1).await;
|
|
||||||
assert!(status1.is_success());
|
|
||||||
|
|
||||||
// Try to create user with same username
|
|
||||||
let resp2 = test::TestRequest::post()
|
|
||||||
.uri("/api/v1/user")
|
|
||||||
.insert_header(header::ContentType::json())
|
|
||||||
.set_payload(user_data.to_string())
|
|
||||||
.send_request(&app)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let status2 = resp2.status();
|
|
||||||
assert!(status2.is_client_error() || status2.is_server_error());
|
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
let _delete_resp = test::TestRequest::delete()
|
ctx.cleanup_all(db).await;
|
||||||
.uri(&format!("/api/v1/user/{}", user1.id))
|
|
||||||
.send_request(&app)
|
|
||||||
.await;
|
|
||||||
// Don't assert on cleanup status in case of race conditions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_delete_nonexistent_user() {
|
async fn test_delete_nonexistent_user() {
|
||||||
|
let ctx = TestContext::new();
|
||||||
|
let db = crate::common::test_helpers::get_database().await;
|
||||||
|
|
||||||
let app = create_test_app!();
|
let app = create_test_app!();
|
||||||
let fake_id = "00000000-0000-0000-0000-000000000000";
|
let fake_id = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
// Verify the fake ID doesn't exist in database
|
||||||
|
let fake_uuid = uuid::Uuid::parse_str(fake_id).unwrap();
|
||||||
|
assert!(ctx.assert_user_not_exists(db, fake_uuid).await);
|
||||||
|
|
||||||
|
// Try to delete non-existent user
|
||||||
let resp = test::TestRequest::delete()
|
let resp = test::TestRequest::delete()
|
||||||
.uri(&format!("/api/v1/user/{}", fake_id))
|
.uri(&format!("/api/v1/user/{}", fake_id))
|
||||||
.send_request(&app)
|
.send_request(&app)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
assert!(status.is_client_error() || status.is_server_error());
|
assert!(
|
||||||
|
status.is_client_error() || status.is_server_error(),
|
||||||
|
"Expected error for non-existent user, got: {}",
|
||||||
|
status
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify it still doesn't exist
|
||||||
|
assert!(ctx.assert_user_not_exists(db, fake_uuid).await);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
ctx.cleanup_all(db).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
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