use std::error::Error;
use std::hash::{Hash, Hasher};
use std::time::Duration;
use std::vec;
use http::Uri;
use integresql::*;
use log::debug;
use postgres::Client as SyncClient;
use serial_test::serial;
use tokio::time::sleep;
use tokio::{self, task::AbortHandle};
use tokio_postgres::{Client, NoTls};
use xxhash_rust::xxh64::Xxh64;
const TEST_BASE_URL: &str = "http://integresql:5000/api";
const SLEEP_DURATION: Duration = Duration::from_millis(500);
const DEV_DB: &str = "development";
#[tokio::test]
#[test_log::test]
#[serial]
async fn test_async_setup() {
let admin_client = get_admin_client(DEV_DB).await;
reset_db(&admin_client).await;
let dbs_before = list_databases(&admin_client).await;
assert_eq!(0, dbs_before.len(), "Expected no databases before setup");
let manager = make_manager();
let result = manager.get_template_db_async("KEY_1", async_setup).await;
assert!(result.is_ok(), "Failed to set up template");
sleep(SLEEP_DURATION).await;
let expected_hash = calculate_template_hash("KEY_1");
validate_database_setup(&admin_client, &expected_hash).await;
validate_get_test_db(result.unwrap(), &expected_hash);
}
#[tokio::test]
#[test_log::test]
#[serial]
async fn test_async_setup_with_overrides() {
let admin_client = get_admin_client(DEV_DB).await;
reset_db(&admin_client).await;
let manager = make_manager().with_overrides(Some("other_db_host".to_string()), None);
let template = manager
.get_template_db_async("KEY_OVERRIDES", async_setup)
.await
.expect("Failed to set up template");
sleep(SLEEP_DURATION).await;
let readonly_db = template
.get_readonly_test_db()
.expect("Failed to get readonly test DB");
assert_eq!(
readonly_db.host, "other_db_host",
"Expected readonly DB host to use override"
);
let (client, handle) = connect_async(&readonly_db)
.await
.expect("Failed to connect using overridden host");
client
.query("SELECT 1", &[])
.await
.expect("Failed to query readonly test DB");
drop(client);
handle.abort();
tokio::task::yield_now().await;
}
#[tokio::test]
#[test_log::test]
#[serial]
async fn test_async_full_workflow() {
let admin_client = get_admin_client(DEV_DB).await;
reset_db(&admin_client).await;
let dbs_before = list_databases(&admin_client).await;
assert_eq!(0, dbs_before.len(), "Expected no databases before setup");
let manager = make_manager();
let result = manager.get_template_db_async("KEY_2", async_setup).await;
assert!(result.is_ok(), "Failed to set up template");
sleep(SLEEP_DURATION).await;
let expected_hash = calculate_template_hash("KEY_2");
validate_get_test_db(result.unwrap(), &expected_hash);
}
#[tokio::test]
#[test_log::test]
#[serial]
async fn test_sync_setup() {
let admin_client = get_admin_client(DEV_DB).await;
reset_db(&admin_client).await;
let dbs_before = list_databases(&admin_client).await;
assert_eq!(0, dbs_before.len(), "Expected no databases before setup");
let manager = make_manager();
let result = manager.get_template_db_sync("KEY_3", sync_setup);
assert!(result.is_ok(), "Failed to set up template");
sleep(SLEEP_DURATION).await;
let expected_hash = calculate_template_hash("KEY_3");
validate_database_setup(&admin_client, &expected_hash).await;
}
#[tokio::test]
#[test_log::test]
#[serial]
async fn test_reset_tracking() {
let admin_client = get_admin_client(DEV_DB).await;
reset_db(&admin_client).await;
let dbs_before = list_databases(&admin_client).await;
assert_eq!(0, dbs_before.len(), "Expected no databases before setup");
let manager = make_manager();
let keys = vec!["KEY_1", "KEY_2", "KEY_3"];
for key in &keys {
let result = manager.get_template_db_sync(*key, sync_setup);
assert!(result.is_ok(), "Failed to set up template for {}", key);
sleep(SLEEP_DURATION).await; let template_hash = calculate_template_hash(*key);
let expected_prefix = format!("integresql_test_{}_", template_hash);
let dbs_after = list_databases(&admin_client).await;
let test_db_count = dbs_after
.iter()
.filter(|db| db.starts_with(&expected_prefix))
.count();
assert!(
test_db_count >= 4,
"Expected at least 4 test databases for {}",
key
);
}
manager
.clear_db_tracking()
.expect("Failed to delete all templates");
sleep(Duration::from_secs(4)).await; let dbs_after = list_databases(&admin_client).await;
let test_db_count = dbs_after
.iter()
.filter(|db| db.starts_with("integresql_test_"))
.count();
assert_eq!(0, test_db_count, "Expected no test databases after reset");
let template_db_count = dbs_after
.iter()
.filter(|db| db.starts_with("integresql_template_"))
.count();
assert_eq!(
3, template_db_count,
"Expected 3 template databases after reset"
);
}
fn validate_get_test_db(template: TemplateDb, template_hash: &str) {
let writable_db = template.get_writable_test_db().unwrap();
let readonly_db = template.get_readonly_test_db().unwrap();
let both = vec![writable_db, readonly_db];
let (host, user, password) = get_env_settings();
for db in both {
assert_eq!(db.port, 5432);
assert_eq!(db.template_hash(), template_hash);
assert_eq!(db.host, host, "Test database host does not match expected");
assert_eq!(
db.username, user,
"Test database user does not match expected"
);
assert_eq!(
db.password, password,
"Test database password does not match expected"
);
let expected_prefix = format!("integresql_test_{}_", template_hash);
assert!(
db.database.starts_with(&expected_prefix),
"Test database name does not start with expected prefix"
);
}
}
async fn validate_database_setup(admin_client: &Client, template_hash: &str) {
let dbs_after = list_databases(admin_client).await;
let template_dbs = dbs_after
.iter()
.filter(|db| db.starts_with("integresql_template_"))
.collect::<Vec<_>>();
let test_dbs = dbs_after
.iter()
.filter(|db| db.starts_with("integresql_test_"))
.collect::<Vec<_>>();
assert_eq!(
template_dbs.len(),
1,
"Expected exactly one template database, found: {:?}",
template_dbs
);
assert_eq!(
test_dbs.len(),
dbs_after.len() - 1,
"Expected {} test databases, found: {:?}",
dbs_after.len() - 1,
test_dbs
);
let template_db = format!("integresql_template_{}", template_hash);
assert_eq!(
template_dbs[0], &template_db,
"Expected template database name to be {}, found: {}",
template_db, template_dbs[0]
);
let test_db_prefix = format!("integresql_test_{}_", template_hash);
let all_have_expected_prefix = test_dbs.iter().all(|db| db.starts_with(&test_db_prefix));
assert!(
all_have_expected_prefix,
"Not all test databases have the expected prefix: {}",
test_db_prefix
);
for db in test_dbs {
let client = get_admin_client(db).await;
let values = client
.query("SELECT name FROM test_schema.test_table", &[])
.await
.expect("Failed to query test table")
.into_iter()
.map(|row| row.get::<_, String>(0))
.collect::<Vec<_>>();
assert_eq!(
values.len(),
3,
"Expected 3 rows in test_table, found: {}",
values.len()
);
let expected_values = vec!["value one", "value two", "value three"];
assert_eq!(
values, expected_values,
"Test table values do not match expected: {:?}",
values
);
}
}
async fn list_databases(client: &Client) -> Vec<String> {
let rows = client
.query(
"SELECT datname FROM pg_database WHERE datname LIKE 'integresql_%'",
&[],
)
.await
.expect("Failed to query databases");
rows.into_iter()
.map(|row| row.get::<_, String>(0))
.collect()
}
async fn reset_db(client: &Client) {
let agent = get_http_client();
agent
.delete("http://integresql:5000/api/v1/admin/templates")
.call()
.expect("Failed to send reset request to integresql");
let remaining_dbs = client
.query(
"SELECT datname FROM pg_database WHERE datname LIKE 'integresql_%'",
&[],
)
.await
.expect("Failed to query remaining databases")
.into_iter()
.map(|row| row.get::<_, String>(0))
.collect::<Vec<_>>();
for db in remaining_dbs {
client
.execute(&format!("DROP DATABASE IF EXISTS {}", db), &[])
.await
.expect("Failed to drop database");
}
}
const TIMEOUT: Duration = Duration::from_secs(2);
fn get_http_client() -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(Some(TIMEOUT))
.build()
.new_agent()
}
fn get_env_settings() -> (String, String, String) {
let host = std::env::var("PGHOST").expect("PGHOST must be set");
let user = std::env::var("PGUSER").expect("PGUSER must be set");
let password = std::env::var("PGPASSWORD").expect("PGPASSWORD must be set");
(host, user, password)
}
async fn get_admin_client(db_name: &str) -> Client {
let (host, user, password) = get_env_settings();
let conn_str = format!(
"host={} user={} password={} dbname={}",
host, user, password, db_name
);
let (client, connection) = tokio_postgres::connect(&conn_str, NoTls)
.await
.expect("Failed to connect to the database");
debug!("Opening postgres connection to {}", db_name);
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {}", e);
}
debug!("Closing postgres connection");
});
client
}
fn sync_setup(config: ConnectionSettings) -> InitializeResult {
let thread = std::thread::spawn(move || {
let mut client = connect_sync(&config);
client
.execute("CREATE SCHEMA IF NOT EXISTS test_schema", &[])
.expect("Failed to create schema");
client
.execute(
"CREATE TABLE IF NOT EXISTS test_schema.test_table (id SERIAL PRIMARY KEY, name TEXT)",
&[],
)
.expect("Failed to create table");
client.execute("INSERT INTO test_schema.test_table (name) VALUES ('value one'), ('value two'), ('value three');", &[])
.expect("Failed to insert values");
});
match thread.join() {
Ok(_) => Ok(()),
Err(_) => Err("panic in sync_setup thread".into()),
}
}
async fn async_setup(config: ConnectionSettings) -> InitializeResult {
let (client, abort_handle) = connect_async(&config).await?;
client
.execute("CREATE SCHEMA IF NOT EXISTS test_schema", &[])
.await?;
client
.execute(
"CREATE TABLE IF NOT EXISTS test_schema.test_table (id SERIAL PRIMARY KEY, name TEXT)",
&[],
)
.await?;
client.execute("INSERT INTO test_schema.test_table (name) VALUES ('value one'), ('value two'), ('value three');", &[]).await?;
abort_handle.abort();
tokio::task::yield_now().await; Ok(())
}
fn connect_sync(config: &ConnectionSettings) -> SyncClient {
let conn_str = config.to_libpq_url();
SyncClient::connect(&conn_str, NoTls).expect("Failed to connect to the database")
}
async fn connect_async(
config: &ConnectionSettings,
) -> Result<(Client, AbortHandle), tokio_postgres::Error> {
let conn_str = config.to_libpq_url();
let (client, connection) = tokio_postgres::connect(&conn_str, NoTls).await?;
let handle = tokio::spawn(async move {
debug!("Connecting to database: {}", conn_str);
if let Err(e) = connection.await {
eprintln!("Connection error: {}", e);
}
debug!("Closing postgres connection");
});
Ok((client, handle.abort_handle()))
}
fn make_manager() -> DbManager {
let url = Uri::from_static(TEST_BASE_URL);
let timeout = Duration::from_secs(10);
DbManager::new(url, timeout)
}
fn calculate_template_hash(key: &str) -> String {
let mut hasher = Xxh64::new(0);
key.hash(&mut hasher);
let hash = hasher.finish();
format!("{:016x}", hash)
}