pubky_testnet/
ephemeral_testnet.rs1use crate::Testnet;
2use http_relay::HttpRelay;
3use pubky::{Keypair, Pubky};
4use pubky_homeserver::{ConfigToml, ConnectionString, HomeserverApp, MockDataDir};
5
6#[cfg(feature = "embedded-postgres")]
7use crate::embedded_postgres::EmbeddedPostgres;
8
9pub struct EphemeralTestnet {
34 pub testnet: Testnet,
36 #[cfg(feature = "embedded-postgres")]
39 #[allow(dead_code)]
40 embedded_postgres: Option<EmbeddedPostgres>,
41}
42
43pub struct EphemeralTestnetBuilder {
77 postgres_connection_string: Option<ConnectionString>,
78 homeserver_config: Option<ConfigToml>,
79 homeserver_keypair: Option<Keypair>,
80 http_relay: bool,
81 #[cfg(feature = "embedded-postgres")]
82 use_embedded_postgres: bool,
83}
84
85impl EphemeralTestnetBuilder {
86 pub fn new() -> Self {
88 Self {
89 postgres_connection_string: None,
90 homeserver_config: None,
91 homeserver_keypair: None,
92 http_relay: false,
93 #[cfg(feature = "embedded-postgres")]
94 use_embedded_postgres: false,
95 }
96 }
97
98 pub fn config(mut self, config: ConfigToml) -> Self {
100 self.homeserver_config = Some(config);
101 self
102 }
103
104 pub fn keypair(mut self, keypair: Keypair) -> Self {
106 self.homeserver_keypair = Some(keypair);
107 self
108 }
109
110 pub fn postgres(mut self, connection_string: ConnectionString) -> Self {
112 self.postgres_connection_string = Some(connection_string);
113 self
114 }
115
116 pub fn with_http_relay(mut self) -> Self {
118 self.http_relay = true;
119 self
120 }
121
122 #[cfg(feature = "embedded-postgres")]
141 pub fn with_embedded_postgres(mut self) -> Self {
142 self.use_embedded_postgres = true;
143 self
144 }
145
146 pub async fn build(self) -> anyhow::Result<EphemeralTestnet> {
152 #[cfg(feature = "embedded-postgres")]
153 if self.use_embedded_postgres && self.postgres_connection_string.is_some() {
154 anyhow::bail!(
155 "Cannot use both embedded postgres and a custom connection string. \
156 Use either .with_embedded_postgres() or .postgres(), not both."
157 );
158 }
159
160 #[cfg(feature = "embedded-postgres")]
161 let (embedded_postgres, postgres_connection_string) = if self.use_embedded_postgres {
162 let embedded = EmbeddedPostgres::start().await?;
163 let conn_string = embedded.connection_string()?;
164 (Some(embedded), Some(conn_string))
165 } else {
166 (None, self.postgres_connection_string)
167 };
168
169 #[cfg(not(feature = "embedded-postgres"))]
170 let postgres_connection_string = self.postgres_connection_string;
171
172 let mut testnet = if let Some(postgres) = postgres_connection_string {
173 Testnet::new_with_custom_postgres(postgres).await?
174 } else {
175 Testnet::new().await?
176 };
177
178 if self.http_relay {
179 testnet.create_http_relay().await?;
180 }
181
182 let mut config = self
183 .homeserver_config
184 .unwrap_or_else(ConfigToml::minimal_test_config);
185
186 if let Some(connection_string) = testnet.postgres_connection_string.as_ref() {
187 config.general.database_url = connection_string.clone();
188 }
189
190 let keypair = self
191 .homeserver_keypair
192 .unwrap_or_else(|| Keypair::from_secret(&[0; 32]));
193 let mock_dir = MockDataDir::new(config, Some(keypair))?;
194 testnet.create_homeserver_app_with_mock(mock_dir).await?;
195
196 Ok(EphemeralTestnet {
197 testnet,
198 #[cfg(feature = "embedded-postgres")]
199 embedded_postgres,
200 })
201 }
202}
203
204impl Default for EphemeralTestnetBuilder {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl EphemeralTestnet {
211 pub fn builder() -> EphemeralTestnetBuilder {
224 EphemeralTestnetBuilder::new()
225 }
226
227 #[deprecated(
233 since = "0.5.0",
234 note = "Use EphemeralTestnet::builder().config(ConfigToml::default_test_config()).build() for explicit behavior"
235 )]
236 pub async fn start() -> anyhow::Result<Self> {
237 let mut testnet = Testnet::new().await?;
238 testnet.create_http_relay().await?;
239 testnet.create_homeserver().await?;
240 Ok(Self {
241 testnet,
242 #[cfg(feature = "embedded-postgres")]
243 embedded_postgres: None,
244 })
245 }
246
247 #[deprecated(
252 since = "0.5.0",
253 note = "Use EphemeralTestnet::builder().postgres(...).config(ConfigToml::default_test_config()).build() instead"
254 )]
255 pub async fn start_with_custom_postgres(
256 postgres_connection_string: ConnectionString,
257 ) -> anyhow::Result<Self> {
258 let mut testnet = Testnet::new_with_custom_postgres(postgres_connection_string).await?;
259 testnet.create_http_relay().await?;
260 testnet.create_homeserver().await?;
261 Ok(Self {
262 testnet,
263 #[cfg(feature = "embedded-postgres")]
264 embedded_postgres: None,
265 })
266 }
267
268 #[deprecated(
273 since = "0.5.0",
274 note = "Use Testnet::new_with_custom_postgres() and create_http_relay() for fine-grained control"
275 )]
276 pub async fn start_minimal_with_custom_postgres(
277 postgres_connection_string: ConnectionString,
278 ) -> anyhow::Result<Self> {
279 let mut me = Self {
280 testnet: Testnet::new_with_custom_postgres(postgres_connection_string).await?,
281 #[cfg(feature = "embedded-postgres")]
282 embedded_postgres: None,
283 };
284 me.testnet.create_http_relay().await?;
285 Ok(me)
286 }
287
288 #[deprecated(
293 since = "0.5.0",
294 note = "Use Testnet::new() and create_http_relay() for fine-grained control"
295 )]
296 pub async fn start_minimal() -> anyhow::Result<Self> {
297 let mut me = Self {
298 testnet: Testnet::new().await?,
299 #[cfg(feature = "embedded-postgres")]
300 embedded_postgres: None,
301 };
302 me.testnet.create_http_relay().await?;
303 Ok(me)
304 }
305
306 pub async fn create_random_homeserver(&mut self) -> anyhow::Result<&HomeserverApp> {
308 self.create_random_homeserver_with_config(None).await
309 }
310
311 pub async fn create_random_homeserver_with_config(
314 &mut self,
315 config: Option<ConfigToml>,
316 ) -> anyhow::Result<&HomeserverApp> {
317 let mut config = config.unwrap_or_else(ConfigToml::minimal_test_config);
318
319 if let Some(connection_string) = self.testnet.postgres_connection_string.as_ref() {
320 config.general.database_url = connection_string.clone();
321 }
322
323 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
324 self.testnet.create_homeserver_app_with_mock(mock_dir).await
325 }
326
327 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
329 self.testnet.client_builder()
330 }
331
332 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
334 self.testnet.client()
335 }
336
337 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
341 self.testnet.sdk()
342 }
343
344 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
346 self.testnet.pkarr_client_builder()
347 }
348
349 pub fn homeserver_app(&self) -> &pubky_homeserver::HomeserverApp {
351 self.testnet
352 .homeservers
353 .first()
354 .expect("homeservers should be non-empty")
355 }
356
357 pub fn http_relay(&self) -> &HttpRelay {
359 self.testnet
360 .http_relays
361 .first()
362 .expect("no http relay configured - use .with_http_relay() when building")
363 }
364}
365
366#[cfg(test)]
367mod test {
368 use super::*;
369
370 #[tokio::test]
374 async fn test_two_testnet_in_a_row() {
375 {
376 let _ = EphemeralTestnet::builder().build().await.unwrap();
377 }
378
379 {
380 let _ = EphemeralTestnet::builder().build().await.unwrap();
381 }
382 }
383
384 #[tokio::test]
385 async fn test_homeserver_with_random_keypair() {
386 let mut testnet = Testnet::new().await.unwrap();
388 testnet.create_http_relay().await.unwrap();
389 let mut network = EphemeralTestnet {
390 testnet,
391 #[cfg(feature = "embedded-postgres")]
392 embedded_postgres: None,
393 };
394 assert!(network.testnet.homeservers.is_empty());
395
396 let _ = network.create_random_homeserver().await.unwrap();
397 let _ = network.create_random_homeserver().await.unwrap();
398 assert!(network.testnet.homeservers.len() == 2);
399
400 assert_ne!(
402 network.testnet.homeservers[0].public_key(),
403 network.testnet.homeservers[1].public_key()
404 );
405 }
406
407 #[tokio::test]
408 async fn test_builder_default() {
409 let network = EphemeralTestnet::builder().build().await.unwrap();
411 let homeserver = network.homeserver_app();
412
413 assert!(
415 homeserver.admin_server().is_none(),
416 "Builder should use minimal config with admin disabled by default"
417 );
418 assert!(
419 homeserver.metrics_server().is_none(),
420 "Builder should use minimal config with metrics disabled by default"
421 );
422 }
423
424 #[tokio::test]
425 async fn test_builder_with_custom_config() {
426 let mut config = ConfigToml::minimal_test_config();
428 config.metrics.enabled = true;
429
430 let network = EphemeralTestnet::builder()
431 .config(config)
432 .build()
433 .await
434 .unwrap();
435
436 let homeserver = network.homeserver_app();
437 assert!(
438 homeserver.metrics_server().is_some(),
439 "Custom config should enable metrics"
440 );
441 assert!(
442 homeserver.admin_server().is_none(),
443 "Custom config should keep admin disabled"
444 );
445 }
446
447 #[tokio::test]
448 async fn test_builder_with_custom_keypair() {
449 let keypair = Keypair::random();
451 let expected_public_key = keypair.public_key();
452
453 let network = EphemeralTestnet::builder()
454 .keypair(keypair)
455 .build()
456 .await
457 .unwrap();
458
459 let homeserver = network.homeserver_app();
460 assert_eq!(
461 homeserver.public_key(),
462 expected_public_key,
463 "Custom keypair should be used"
464 );
465 }
466}