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 config.general.database_url = testnet
199 .postgres_connection_string
200 .clone()
201 .or(config.general.database_url);
202
203 let keypair = self
204 .homeserver_keypair
205 .unwrap_or_else(crate::common::testnet_keypair);
206 let mock_dir = MockDataDir::new(config, Some(keypair))?;
207 testnet.create_homeserver_app_with_mock(mock_dir).await?;
208
209 Ok(EphemeralTestnet {
210 testnet,
211 #[cfg(feature = "docker-postgres")]
212 docker_postgres,
213 })
214 }
215}
216
217impl Default for EphemeralTestnetBuilder {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl EphemeralTestnet {
224 pub fn builder() -> EphemeralTestnetBuilder {
237 EphemeralTestnetBuilder::new()
238 }
239
240 #[deprecated(
246 since = "0.5.0",
247 note = "Use EphemeralTestnet::builder().config(ConfigToml::default_test_config()).build() for explicit behavior"
248 )]
249 pub async fn start() -> anyhow::Result<Self> {
250 let mut testnet = Testnet::new().await?;
251 testnet.create_http_relay().await?;
252 testnet.create_homeserver().await?;
253 Ok(Self {
254 testnet,
255 #[cfg(feature = "docker-postgres")]
256 docker_postgres: None,
257 })
258 }
259
260 #[deprecated(
265 since = "0.5.0",
266 note = "Use EphemeralTestnet::builder().postgres(...).config(ConfigToml::default_test_config()).build() instead"
267 )]
268 pub async fn start_with_custom_postgres(
269 postgres_connection_string: ConnectionString,
270 ) -> anyhow::Result<Self> {
271 let mut testnet = Testnet::new_with_custom_postgres(postgres_connection_string).await?;
272 testnet.create_http_relay().await?;
273 testnet.create_homeserver().await?;
274 Ok(Self {
275 testnet,
276 #[cfg(feature = "docker-postgres")]
277 docker_postgres: None,
278 })
279 }
280
281 #[deprecated(
286 since = "0.5.0",
287 note = "Use Testnet::new_with_custom_postgres() and create_http_relay() for fine-grained control"
288 )]
289 pub async fn start_minimal_with_custom_postgres(
290 postgres_connection_string: ConnectionString,
291 ) -> anyhow::Result<Self> {
292 let mut me = Self {
293 testnet: Testnet::new_with_custom_postgres(postgres_connection_string).await?,
294 #[cfg(feature = "docker-postgres")]
295 docker_postgres: None,
296 };
297 me.testnet.create_http_relay().await?;
298 Ok(me)
299 }
300
301 #[deprecated(
306 since = "0.5.0",
307 note = "Use Testnet::new() and create_http_relay() for fine-grained control"
308 )]
309 pub async fn start_minimal() -> anyhow::Result<Self> {
310 let mut me = Self {
311 testnet: Testnet::new().await?,
312 #[cfg(feature = "docker-postgres")]
313 docker_postgres: None,
314 };
315 me.testnet.create_http_relay().await?;
316 Ok(me)
317 }
318
319 pub async fn create_random_homeserver(&mut self) -> anyhow::Result<&HomeserverApp> {
321 self.create_random_homeserver_with_config(None).await
322 }
323
324 pub async fn create_random_homeserver_with_config(
327 &mut self,
328 config: Option<ConfigToml>,
329 ) -> anyhow::Result<&HomeserverApp> {
330 let mut config = config.unwrap_or_else(ConfigToml::minimal_test_config);
331
332 config.general.database_url = self
333 .testnet
334 .postgres_connection_string
335 .clone()
336 .or(config.general.database_url);
337
338 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
339 self.testnet.create_homeserver_app_with_mock(mock_dir).await
340 }
341
342 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
344 self.testnet.client_builder()
345 }
346
347 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
349 self.testnet.client()
350 }
351
352 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
356 self.testnet.sdk()
357 }
358
359 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
361 self.testnet.pkarr_client_builder()
362 }
363
364 pub fn homeserver_app(&self) -> &pubky_homeserver::HomeserverApp {
366 self.testnet
367 .homeservers
368 .first()
369 .expect("homeservers should be non-empty")
370 }
371
372 pub fn http_relay(&self) -> &HttpRelay {
374 self.testnet
375 .http_relays
376 .first()
377 .expect("no http relay configured - use .with_http_relay() when building")
378 }
379}
380
381#[cfg(test)]
382mod test {
383 use super::*;
384
385 #[tokio::test]
389 #[crate::test]
390 async fn test_two_testnet_in_a_row() {
391 {
392 let _ = EphemeralTestnet::builder().build().await.unwrap();
393 }
394
395 {
396 let _ = EphemeralTestnet::builder().build().await.unwrap();
397 }
398 }
399
400 #[tokio::test]
401 #[crate::test]
402 async fn test_homeserver_with_random_keypair() {
403 let mut testnet = Testnet::new().await.unwrap();
405 testnet.create_http_relay().await.unwrap();
406 let mut network = EphemeralTestnet {
407 testnet,
408 #[cfg(feature = "docker-postgres")]
409 docker_postgres: None,
410 };
411 assert!(network.testnet.homeservers.is_empty());
412
413 let _ = network.create_random_homeserver().await.unwrap();
414 let _ = network.create_random_homeserver().await.unwrap();
415 assert!(network.testnet.homeservers.len() == 2);
416
417 assert_ne!(
419 network.testnet.homeservers[0].public_key(),
420 network.testnet.homeservers[1].public_key()
421 );
422 }
423
424 #[tokio::test]
425 #[crate::test]
426 async fn test_builder_default() {
427 let network = EphemeralTestnet::builder().build().await.unwrap();
429 let homeserver = network.homeserver_app();
430
431 assert!(
433 homeserver.admin_server().is_none(),
434 "Builder should use minimal config with admin disabled by default"
435 );
436 assert!(
437 homeserver.metrics_server().is_none(),
438 "Builder should use minimal config with metrics disabled by default"
439 );
440 }
441
442 #[tokio::test]
443 #[crate::test]
444 async fn test_builder_with_custom_config() {
445 let mut config = ConfigToml::minimal_test_config();
447 config.metrics.enabled = true;
448
449 let network = EphemeralTestnet::builder()
450 .config(config)
451 .build()
452 .await
453 .unwrap();
454
455 let homeserver = network.homeserver_app();
456 assert!(
457 homeserver.metrics_server().is_some(),
458 "Custom config should enable metrics"
459 );
460 assert!(
461 homeserver.admin_server().is_none(),
462 "Custom config should keep admin disabled"
463 );
464 }
465
466 #[tokio::test]
467 #[crate::test]
468 async fn test_builder_with_custom_keypair() {
469 let keypair = Keypair::random();
471 let expected_public_key = keypair.public_key();
472
473 let network = EphemeralTestnet::builder()
474 .keypair(keypair)
475 .build()
476 .await
477 .unwrap();
478
479 let homeserver = network.homeserver_app();
480 assert_eq!(
481 homeserver.public_key(),
482 expected_public_key,
483 "Custom keypair should be used"
484 );
485 }
486}