use std::sync::atomic::{AtomicUsize, Ordering};
use crate::database::{DatabaseConfig, URL_ENV};
mod idp;
pub use idp::{TEST_CLIENT_ID, TestIdentityProvider, TokenSpec};
pub const SERVER_URL_ENV: &str = "TEST_DATABASE_URL";
pub const DEFAULT_SERVER_URL: &str = "postgresql://localhost:5432/postgres";
pub struct TestDatabase {
config: DatabaseConfig,
server: String,
name: String,
}
impl TestDatabase {
pub fn start() -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let server =
std::env::var(SERVER_URL_ENV).unwrap_or_else(|_| DEFAULT_SERVER_URL.to_owned());
let name = format!("slave_test_{}_{n}", std::process::id());
admin(
&server,
&[
format!("DROP DATABASE IF EXISTS \"{name}\""),
format!("CREATE DATABASE \"{name}\""),
],
)
.unwrap_or_else(|err| {
panic!(
"cannot create a test database on {SERVER_URL_ENV}={server}: {err} \
(start PostgreSQL or set {SERVER_URL_ENV})"
)
});
let config = DatabaseConfig {
url: with_database(&server, &name),
};
Self {
config,
server,
name,
}
}
pub fn config(&self) -> &DatabaseConfig {
&self.config
}
pub fn envs(&self) -> [(&'static str, &str); 1] {
[(URL_ENV, self.config.url.as_str())]
}
pub async fn connect(&self, models: toasty::ModelSet) -> toasty::Db {
crate::database::connect(models, &self.config)
.await
.expect("open the test database")
}
}
impl Drop for TestDatabase {
fn drop(&mut self) {
let drop = format!("DROP DATABASE IF EXISTS \"{}\" WITH (FORCE)", self.name);
if let Err(err) = admin(&self.server, &[drop]) {
eprintln!("warning: could not drop test database {}: {err}", self.name);
}
}
}
fn admin(server: &str, statements: &[String]) -> Result<(), String> {
let server = server.to_owned();
let statements = statements.to_vec();
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| err.to_string())?;
runtime.block_on(async {
let (client, connection) = tokio_postgres::connect(&server, tokio_postgres::NoTls)
.await
.map_err(|err| err.to_string())?;
let connection = tokio::spawn(connection);
for statement in &statements {
client
.batch_execute(statement)
.await
.map_err(|err| err.to_string())?;
}
drop(client);
let _ = connection.await;
Ok(())
})
})
.join()
.map_err(|_| "the database thread panicked".to_owned())?
}
fn with_database(url: &str, name: &str) -> String {
let (base, query) = match url.split_once('?') {
Some((base, query)) => (base, Some(query)),
None => (url, None),
};
let authority_start = base.find("://").map_or(0, |i| i + 3);
let base = match base[authority_start..].find('/') {
Some(slash) => &base[..authority_start + slash],
None => base,
};
match query {
Some(query) => format!("{base}/{name}?{query}"),
None => format!("{base}/{name}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replaces_the_database_in_urls() {
assert_eq!(
with_database("postgresql://localhost:5432/postgres", "t1"),
"postgresql://localhost:5432/t1"
);
assert_eq!(
with_database("postgresql://u:p@db/x?sslmode=disable", "t1"),
"postgresql://u:p@db/t1?sslmode=disable"
);
assert_eq!(with_database("postgresql://db", "t1"), "postgresql://db/t1");
}
}