use postgresql_embedded::{PostgreSQL, Settings, VersionReq};
use pubky_homeserver::ConnectionString;
use rand::Rng;
use std::time::Duration;
pub struct EmbeddedPostgres {
pg: PostgreSQL,
database_name: String,
}
impl EmbeddedPostgres {
pub async fn start() -> anyhow::Result<Self> {
let settings = Settings {
version: VersionReq::parse("=18.1.0")?,
installation_dir: dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join("pubky-testnet")
.join("postgresql"),
timeout: Some(Duration::from_secs(120)),
..Default::default()
};
let mut pg = PostgreSQL::new(settings);
pg.setup().await?;
pg.start().await?;
let database_name = format!(
"pubky_test_{}_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
rand::rng().random::<u32>()
);
pg.create_database(&database_name).await?;
Ok(Self { pg, database_name })
}
pub fn connection_string(&self) -> anyhow::Result<ConnectionString> {
let settings = self.pg.settings();
let url = format!(
"postgres://{}:{}@{}:{}/{}",
settings.username, settings.password, settings.host, settings.port, self.database_name
);
ConnectionString::new(&url).map_err(|e| anyhow::anyhow!("Invalid connection string: {}", e))
}
}
impl Drop for EmbeddedPostgres {
fn drop(&mut self) {
tracing::debug!(
"Stopping embedded PostgreSQL (database: {})",
self.database_name
);
}
}
#[cfg(test)]
mod tests {
use crate::EphemeralTestnet;
use pubky::Keypair;
#[tokio::test]
async fn test_embedded_postgres_full_lifecycle() {
let testnet = EphemeralTestnet::builder()
.with_embedded_postgres()
.with_http_relay()
.build()
.await
.expect("Failed to start testnet with embedded postgres");
assert!(!testnet.homeserver_app().public_key().to_string().is_empty());
let _ = testnet.http_relay();
let pubky = testnet.sdk().expect("Failed to create SDK");
let keypair = Keypair::random();
let signer = pubky.signer(keypair);
let session = signer
.signup(&testnet.homeserver_app().public_key(), None)
.await
.expect("Failed to signup user");
let path = "/pub/test.txt";
let data = b"Hello from embedded postgres test!";
session
.storage()
.put(path, data.as_slice())
.await
.expect("Failed to store data");
let response = session
.storage()
.get(path)
.await
.expect("Failed to get data");
let bytes = response.bytes().await.expect("Failed to read bytes");
assert_eq!(bytes.as_ref(), data);
drop(testnet);
let testnet2 = EphemeralTestnet::builder()
.with_embedded_postgres()
.build()
.await
.expect("Failed to start second testnet - cleanup may have failed");
assert!(!testnet2
.homeserver_app()
.public_key()
.to_string()
.is_empty());
}
#[tokio::test]
async fn test_embedded_postgres_and_custom_connection_string_fails() {
use pubky_homeserver::ConnectionString;
let connection = ConnectionString::new("postgres://localhost:5432/test").unwrap();
let result = EphemeralTestnet::builder()
.postgres(connection)
.with_embedded_postgres()
.build()
.await;
match result {
Ok(_) => panic!("Should fail when both postgres options are set"),
Err(err) => {
assert!(
err.to_string().contains(
"Cannot use both embedded postgres and a custom connection string"
),
"Expected error about conflicting postgres options, got: {}",
err
);
}
}
}
}