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 = "docker-postgres")]
7use crate::docker_postgres::DockerPostgres;
8
9pub struct EphemeralTestnet {
40 pub testnet: Testnet,
42 #[cfg(feature = "docker-postgres")]
45 #[allow(dead_code)]
46 docker_postgres: Option<DockerPostgres>,
47}
48
49pub struct EphemeralTestnetBuilder {
83 postgres_connection_string: Option<ConnectionString>,
84 homeserver_config: Option<ConfigToml>,
85 homeserver_keypair: Option<Keypair>,
86 http_relay: bool,
87 #[cfg(feature = "docker-postgres")]
88 use_docker_postgres: bool,
89}
90
91impl EphemeralTestnetBuilder {
92 pub fn new() -> Self {
94 Self {
95 postgres_connection_string: None,
96 homeserver_config: None,
97 homeserver_keypair: None,
98 http_relay: false,
99 #[cfg(feature = "docker-postgres")]
100 use_docker_postgres: false,
101 }
102 }
103
104 pub fn config(mut self, config: ConfigToml) -> Self {
106 self.homeserver_config = Some(config);
107 self
108 }
109
110 pub fn keypair(mut self, keypair: Keypair) -> Self {
112 self.homeserver_keypair = Some(keypair);
113 self
114 }
115
116 pub fn postgres(mut self, connection_string: ConnectionString) -> Self {
118 self.postgres_connection_string = Some(connection_string);
119 self
120 }
121
122 pub fn with_http_relay(mut self) -> Self {
124 self.http_relay = true;
125 self
126 }
127
128 #[cfg(feature = "docker-postgres")]
146 pub fn with_docker_postgres(mut self) -> Self {
147 self.use_docker_postgres = true;
148 self
149 }
150
151 #[cfg(feature = "docker-postgres")]
153 #[deprecated(since = "0.9.0", note = "Renamed to `with_docker_postgres()`")]
154 pub fn with_embedded_postgres(self) -> Self {
155 self.with_docker_postgres()
156 }
157
158 pub async fn build(self) -> anyhow::Result<EphemeralTestnet> {
164 #[cfg(feature = "docker-postgres")]
165 if self.use_docker_postgres && self.postgres_connection_string.is_some() {
166 anyhow::bail!(
167 "Cannot use both docker postgres and a custom connection string. \
168 Use either .with_docker_postgres() or .postgres(), not both."
169 );
170 }
171
172 #[cfg(feature = "docker-postgres")]
173 let (docker_postgres, postgres_connection_string) = if self.use_docker_postgres {
174 let pg = DockerPostgres::start().await?;
175 let conn_string = pg.connection_string()?;
176 (Some(pg), Some(conn_string))
177 } else {
178 (None, self.postgres_connection_string)
179 };
180
181 #[cfg(not(feature = "docker-postgres"))]
182 let postgres_connection_string = self.postgres_connection_string;
183
184 let mut testnet = if let Some(postgres) = postgres_connection_string {
185 Testnet::new_with_custom_postgres(postgres).await?
186 } else {
187 Testnet::new().await?
188 };
189
190 if self.http_relay {
191 testnet.create_http_relay().await?;
192 }
193
194 let mut config = self
195 .homeserver_config
196 .unwrap_or_else(ConfigToml::minimal_test_config);
197
198 if let Some(connection_string) = testnet.postgres_connection_string.as_ref() {
199 config.general.database_url = connection_string.clone();
200 }
201
202 let keypair = self
203 .homeserver_keypair
204 .unwrap_or_else(crate::common::testnet_keypair);
205 let mock_dir = MockDataDir::new(config, Some(keypair))?;
206 testnet.create_homeserver_app_with_mock(mock_dir).await?;
207
208 Ok(EphemeralTestnet {
209 testnet,
210 #[cfg(feature = "docker-postgres")]
211 docker_postgres,
212 })
213 }
214}
215
216impl Default for EphemeralTestnetBuilder {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222impl EphemeralTestnet {
223 pub fn builder() -> EphemeralTestnetBuilder {
236 EphemeralTestnetBuilder::new()
237 }
238
239 #[deprecated(
245 since = "0.5.0",
246 note = "Use EphemeralTestnet::builder().config(ConfigToml::default_test_config()).build() for explicit behavior"
247 )]
248 pub async fn start() -> anyhow::Result<Self> {
249 let mut testnet = Testnet::new().await?;
250 testnet.create_http_relay().await?;
251 testnet.create_homeserver().await?;
252 Ok(Self {
253 testnet,
254 #[cfg(feature = "docker-postgres")]
255 docker_postgres: None,
256 })
257 }
258
259 #[deprecated(
264 since = "0.5.0",
265 note = "Use EphemeralTestnet::builder().postgres(...).config(ConfigToml::default_test_config()).build() instead"
266 )]
267 pub async fn start_with_custom_postgres(
268 postgres_connection_string: ConnectionString,
269 ) -> anyhow::Result<Self> {
270 let mut testnet = Testnet::new_with_custom_postgres(postgres_connection_string).await?;
271 testnet.create_http_relay().await?;
272 testnet.create_homeserver().await?;
273 Ok(Self {
274 testnet,
275 #[cfg(feature = "docker-postgres")]
276 docker_postgres: None,
277 })
278 }
279
280 #[deprecated(
285 since = "0.5.0",
286 note = "Use Testnet::new_with_custom_postgres() and create_http_relay() for fine-grained control"
287 )]
288 pub async fn start_minimal_with_custom_postgres(
289 postgres_connection_string: ConnectionString,
290 ) -> anyhow::Result<Self> {
291 let mut me = Self {
292 testnet: Testnet::new_with_custom_postgres(postgres_connection_string).await?,
293 #[cfg(feature = "docker-postgres")]
294 docker_postgres: None,
295 };
296 me.testnet.create_http_relay().await?;
297 Ok(me)
298 }
299
300 #[deprecated(
305 since = "0.5.0",
306 note = "Use Testnet::new() and create_http_relay() for fine-grained control"
307 )]
308 pub async fn start_minimal() -> anyhow::Result<Self> {
309 let mut me = Self {
310 testnet: Testnet::new().await?,
311 #[cfg(feature = "docker-postgres")]
312 docker_postgres: None,
313 };
314 me.testnet.create_http_relay().await?;
315 Ok(me)
316 }
317
318 pub async fn create_random_homeserver(&mut self) -> anyhow::Result<&HomeserverApp> {
320 self.create_random_homeserver_with_config(None).await
321 }
322
323 pub async fn create_random_homeserver_with_config(
326 &mut self,
327 config: Option<ConfigToml>,
328 ) -> anyhow::Result<&HomeserverApp> {
329 let mut config = config.unwrap_or_else(ConfigToml::minimal_test_config);
330
331 if let Some(connection_string) = self.testnet.postgres_connection_string.as_ref() {
332 config.general.database_url = connection_string.clone();
333 }
334
335 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
336 self.testnet.create_homeserver_app_with_mock(mock_dir).await
337 }
338
339 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
341 self.testnet.client_builder()
342 }
343
344 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
346 self.testnet.client()
347 }
348
349 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
353 self.testnet.sdk()
354 }
355
356 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
358 self.testnet.pkarr_client_builder()
359 }
360
361 pub fn homeserver_app(&self) -> &pubky_homeserver::HomeserverApp {
363 self.testnet
364 .homeservers
365 .first()
366 .expect("homeservers should be non-empty")
367 }
368
369 pub fn http_relay(&self) -> &HttpRelay {
371 self.testnet
372 .http_relays
373 .first()
374 .expect("no http relay configured - use .with_http_relay() when building")
375 }
376}
377
378#[cfg(test)]
379mod test {
380 use super::*;
381
382 #[tokio::test]
386 async fn test_two_testnet_in_a_row() {
387 {
388 let _ = EphemeralTestnet::builder().build().await.unwrap();
389 }
390
391 {
392 let _ = EphemeralTestnet::builder().build().await.unwrap();
393 }
394 }
395
396 #[tokio::test]
397 async fn test_homeserver_with_random_keypair() {
398 let mut testnet = Testnet::new().await.unwrap();
400 testnet.create_http_relay().await.unwrap();
401 let mut network = EphemeralTestnet {
402 testnet,
403 #[cfg(feature = "docker-postgres")]
404 docker_postgres: None,
405 };
406 assert!(network.testnet.homeservers.is_empty());
407
408 let _ = network.create_random_homeserver().await.unwrap();
409 let _ = network.create_random_homeserver().await.unwrap();
410 assert!(network.testnet.homeservers.len() == 2);
411
412 assert_ne!(
414 network.testnet.homeservers[0].public_key(),
415 network.testnet.homeservers[1].public_key()
416 );
417 }
418
419 #[tokio::test]
420 async fn test_builder_default() {
421 let network = EphemeralTestnet::builder().build().await.unwrap();
423 let homeserver = network.homeserver_app();
424
425 assert!(
427 homeserver.admin_server().is_none(),
428 "Builder should use minimal config with admin disabled by default"
429 );
430 assert!(
431 homeserver.metrics_server().is_none(),
432 "Builder should use minimal config with metrics disabled by default"
433 );
434 }
435
436 #[tokio::test]
437 async fn test_builder_with_custom_config() {
438 let mut config = ConfigToml::minimal_test_config();
440 config.metrics.enabled = true;
441
442 let network = EphemeralTestnet::builder()
443 .config(config)
444 .build()
445 .await
446 .unwrap();
447
448 let homeserver = network.homeserver_app();
449 assert!(
450 homeserver.metrics_server().is_some(),
451 "Custom config should enable metrics"
452 );
453 assert!(
454 homeserver.admin_server().is_none(),
455 "Custom config should keep admin disabled"
456 );
457 }
458
459 #[tokio::test]
460 async fn test_builder_with_custom_keypair() {
461 let keypair = Keypair::random();
463 let expected_public_key = keypair.public_key();
464
465 let network = EphemeralTestnet::builder()
466 .keypair(keypair)
467 .build()
468 .await
469 .unwrap();
470
471 let homeserver = network.homeserver_app();
472 assert_eq!(
473 homeserver.public_key(),
474 expected_public_key,
475 "Custom keypair should be used"
476 );
477 }
478}