use crate::Testnet;
use http_relay::HttpRelay;
use pubky::{Keypair, Pubky};
use pubky_homeserver::{ConfigToml, ConnectionString, HomeserverApp, MockDataDir};
#[cfg(feature = "embedded-postgres")]
use crate::embedded_postgres::EmbeddedPostgres;
pub struct EphemeralTestnet {
pub testnet: Testnet,
#[cfg(feature = "embedded-postgres")]
#[allow(dead_code)]
embedded_postgres: Option<EmbeddedPostgres>,
}
pub struct EphemeralTestnetBuilder {
postgres_connection_string: Option<ConnectionString>,
homeserver_config: Option<ConfigToml>,
homeserver_keypair: Option<Keypair>,
http_relay: bool,
#[cfg(feature = "embedded-postgres")]
use_embedded_postgres: bool,
}
impl EphemeralTestnetBuilder {
pub fn new() -> Self {
Self {
postgres_connection_string: None,
homeserver_config: None,
homeserver_keypair: None,
http_relay: false,
#[cfg(feature = "embedded-postgres")]
use_embedded_postgres: false,
}
}
pub fn config(mut self, config: ConfigToml) -> Self {
self.homeserver_config = Some(config);
self
}
pub fn keypair(mut self, keypair: Keypair) -> Self {
self.homeserver_keypair = Some(keypair);
self
}
pub fn postgres(mut self, connection_string: ConnectionString) -> Self {
self.postgres_connection_string = Some(connection_string);
self
}
pub fn with_http_relay(mut self) -> Self {
self.http_relay = true;
self
}
#[cfg(feature = "embedded-postgres")]
pub fn with_embedded_postgres(mut self) -> Self {
self.use_embedded_postgres = true;
self
}
pub async fn build(self) -> anyhow::Result<EphemeralTestnet> {
#[cfg(feature = "embedded-postgres")]
if self.use_embedded_postgres && self.postgres_connection_string.is_some() {
anyhow::bail!(
"Cannot use both embedded postgres and a custom connection string. \
Use either .with_embedded_postgres() or .postgres(), not both."
);
}
#[cfg(feature = "embedded-postgres")]
let (embedded_postgres, postgres_connection_string) = if self.use_embedded_postgres {
let embedded = EmbeddedPostgres::start().await?;
let conn_string = embedded.connection_string()?;
(Some(embedded), Some(conn_string))
} else {
(None, self.postgres_connection_string)
};
#[cfg(not(feature = "embedded-postgres"))]
let postgres_connection_string = self.postgres_connection_string;
let mut testnet = if let Some(postgres) = postgres_connection_string {
Testnet::new_with_custom_postgres(postgres).await?
} else {
Testnet::new().await?
};
if self.http_relay {
testnet.create_http_relay().await?;
}
let mut config = self
.homeserver_config
.unwrap_or_else(ConfigToml::minimal_test_config);
if let Some(connection_string) = testnet.postgres_connection_string.as_ref() {
config.general.database_url = connection_string.clone();
}
let keypair = self
.homeserver_keypair
.unwrap_or_else(|| Keypair::from_secret(&[0; 32]));
let mock_dir = MockDataDir::new(config, Some(keypair))?;
testnet.create_homeserver_app_with_mock(mock_dir).await?;
Ok(EphemeralTestnet {
testnet,
#[cfg(feature = "embedded-postgres")]
embedded_postgres,
})
}
}
impl Default for EphemeralTestnetBuilder {
fn default() -> Self {
Self::new()
}
}
impl EphemeralTestnet {
pub fn builder() -> EphemeralTestnetBuilder {
EphemeralTestnetBuilder::new()
}
#[deprecated(
since = "0.5.0",
note = "Use EphemeralTestnet::builder().config(ConfigToml::default_test_config()).build() for explicit behavior"
)]
pub async fn start() -> anyhow::Result<Self> {
let mut testnet = Testnet::new().await?;
testnet.create_http_relay().await?;
testnet.create_homeserver().await?;
Ok(Self {
testnet,
#[cfg(feature = "embedded-postgres")]
embedded_postgres: None,
})
}
#[deprecated(
since = "0.5.0",
note = "Use EphemeralTestnet::builder().postgres(...).config(ConfigToml::default_test_config()).build() instead"
)]
pub async fn start_with_custom_postgres(
postgres_connection_string: ConnectionString,
) -> anyhow::Result<Self> {
let mut testnet = Testnet::new_with_custom_postgres(postgres_connection_string).await?;
testnet.create_http_relay().await?;
testnet.create_homeserver().await?;
Ok(Self {
testnet,
#[cfg(feature = "embedded-postgres")]
embedded_postgres: None,
})
}
#[deprecated(
since = "0.5.0",
note = "Use Testnet::new_with_custom_postgres() and create_http_relay() for fine-grained control"
)]
pub async fn start_minimal_with_custom_postgres(
postgres_connection_string: ConnectionString,
) -> anyhow::Result<Self> {
let mut me = Self {
testnet: Testnet::new_with_custom_postgres(postgres_connection_string).await?,
#[cfg(feature = "embedded-postgres")]
embedded_postgres: None,
};
me.testnet.create_http_relay().await?;
Ok(me)
}
#[deprecated(
since = "0.5.0",
note = "Use Testnet::new() and create_http_relay() for fine-grained control"
)]
pub async fn start_minimal() -> anyhow::Result<Self> {
let mut me = Self {
testnet: Testnet::new().await?,
#[cfg(feature = "embedded-postgres")]
embedded_postgres: None,
};
me.testnet.create_http_relay().await?;
Ok(me)
}
pub async fn create_random_homeserver(&mut self) -> anyhow::Result<&HomeserverApp> {
self.create_random_homeserver_with_config(None).await
}
pub async fn create_random_homeserver_with_config(
&mut self,
config: Option<ConfigToml>,
) -> anyhow::Result<&HomeserverApp> {
let mut config = config.unwrap_or_else(ConfigToml::minimal_test_config);
if let Some(connection_string) = self.testnet.postgres_connection_string.as_ref() {
config.general.database_url = connection_string.clone();
}
let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
self.testnet.create_homeserver_app_with_mock(mock_dir).await
}
pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
self.testnet.client_builder()
}
pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
self.testnet.client()
}
pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
self.testnet.sdk()
}
pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
self.testnet.pkarr_client_builder()
}
pub fn homeserver_app(&self) -> &pubky_homeserver::HomeserverApp {
self.testnet
.homeservers
.first()
.expect("homeservers should be non-empty")
}
pub fn http_relay(&self) -> &HttpRelay {
self.testnet
.http_relays
.first()
.expect("no http relay configured - use .with_http_relay() when building")
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn test_two_testnet_in_a_row() {
{
let _ = EphemeralTestnet::builder().build().await.unwrap();
}
{
let _ = EphemeralTestnet::builder().build().await.unwrap();
}
}
#[tokio::test]
async fn test_homeserver_with_random_keypair() {
let mut testnet = Testnet::new().await.unwrap();
testnet.create_http_relay().await.unwrap();
let mut network = EphemeralTestnet {
testnet,
#[cfg(feature = "embedded-postgres")]
embedded_postgres: None,
};
assert!(network.testnet.homeservers.is_empty());
let _ = network.create_random_homeserver().await.unwrap();
let _ = network.create_random_homeserver().await.unwrap();
assert!(network.testnet.homeservers.len() == 2);
assert_ne!(
network.testnet.homeservers[0].public_key(),
network.testnet.homeservers[1].public_key()
);
}
#[tokio::test]
async fn test_builder_default() {
let network = EphemeralTestnet::builder().build().await.unwrap();
let homeserver = network.homeserver_app();
assert!(
homeserver.admin_server().is_none(),
"Builder should use minimal config with admin disabled by default"
);
assert!(
homeserver.metrics_server().is_none(),
"Builder should use minimal config with metrics disabled by default"
);
}
#[tokio::test]
async fn test_builder_with_custom_config() {
let mut config = ConfigToml::minimal_test_config();
config.metrics.enabled = true;
let network = EphemeralTestnet::builder()
.config(config)
.build()
.await
.unwrap();
let homeserver = network.homeserver_app();
assert!(
homeserver.metrics_server().is_some(),
"Custom config should enable metrics"
);
assert!(
homeserver.admin_server().is_none(),
"Custom config should keep admin disabled"
);
}
#[tokio::test]
async fn test_builder_with_custom_keypair() {
let keypair = Keypair::random();
let expected_public_key = keypair.public_key();
let network = EphemeralTestnet::builder()
.keypair(keypair)
.build()
.await
.unwrap();
let homeserver = network.homeserver_app();
assert_eq!(
homeserver.public_key(),
expected_public_key,
"Custom keypair should be used"
);
}
}