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 {
34 pub testnet: Testnet,
36 #[cfg(feature = "docker-postgres")]
39 #[allow(dead_code)]
40 docker_postgres: Option<DockerPostgres>,
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 = "docker-postgres")]
82 use_docker_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 = "docker-postgres")]
94 use_docker_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 = "docker-postgres")]
140 pub fn with_docker_postgres(mut self) -> Self {
141 self.use_docker_postgres = true;
142 self
143 }
144
145 #[cfg(feature = "docker-postgres")]
147 #[deprecated(since = "0.9.0", note = "Renamed to `with_docker_postgres()`")]
148 pub fn with_embedded_postgres(self) -> Self {
149 self.with_docker_postgres()
150 }
151
152 pub async fn build(self) -> anyhow::Result<EphemeralTestnet> {
158 #[cfg(feature = "docker-postgres")]
159 if self.use_docker_postgres && self.postgres_connection_string.is_some() {
160 anyhow::bail!(
161 "Cannot use both docker postgres and a custom connection string. \
162 Use either .with_docker_postgres() or .postgres(), not both."
163 );
164 }
165
166 #[cfg(feature = "docker-postgres")]
167 let (docker_postgres, postgres_connection_string) = if self.use_docker_postgres {
168 let pg = DockerPostgres::start().await?;
169 let conn_string = pg.connection_string()?;
170 (Some(pg), Some(conn_string))
171 } else {
172 (None, self.postgres_connection_string)
173 };
174
175 #[cfg(not(feature = "docker-postgres"))]
176 let postgres_connection_string = self.postgres_connection_string;
177
178 let mut testnet = if let Some(postgres) = postgres_connection_string {
179 Testnet::new_with_custom_postgres(postgres).await?
180 } else {
181 Testnet::new().await?
182 };
183
184 if self.http_relay {
185 testnet.create_http_relay().await?;
186 }
187
188 let mut config = self
189 .homeserver_config
190 .unwrap_or_else(ConfigToml::minimal_test_config);
191
192 if let Some(connection_string) = testnet.postgres_connection_string.as_ref() {
193 config.general.database_url = connection_string.clone();
194 }
195
196 let keypair = self
197 .homeserver_keypair
198 .unwrap_or_else(|| Keypair::from_secret(&[0; 32]));
199 let mock_dir = MockDataDir::new(config, Some(keypair))?;
200 testnet.create_homeserver_app_with_mock(mock_dir).await?;
201
202 Ok(EphemeralTestnet {
203 testnet,
204 #[cfg(feature = "docker-postgres")]
205 docker_postgres,
206 })
207 }
208}
209
210impl Default for EphemeralTestnetBuilder {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl EphemeralTestnet {
217 pub fn builder() -> EphemeralTestnetBuilder {
230 EphemeralTestnetBuilder::new()
231 }
232
233 #[deprecated(
239 since = "0.5.0",
240 note = "Use EphemeralTestnet::builder().config(ConfigToml::default_test_config()).build() for explicit behavior"
241 )]
242 pub async fn start() -> anyhow::Result<Self> {
243 let mut testnet = Testnet::new().await?;
244 testnet.create_http_relay().await?;
245 testnet.create_homeserver().await?;
246 Ok(Self {
247 testnet,
248 #[cfg(feature = "docker-postgres")]
249 docker_postgres: None,
250 })
251 }
252
253 #[deprecated(
258 since = "0.5.0",
259 note = "Use EphemeralTestnet::builder().postgres(...).config(ConfigToml::default_test_config()).build() instead"
260 )]
261 pub async fn start_with_custom_postgres(
262 postgres_connection_string: ConnectionString,
263 ) -> anyhow::Result<Self> {
264 let mut testnet = Testnet::new_with_custom_postgres(postgres_connection_string).await?;
265 testnet.create_http_relay().await?;
266 testnet.create_homeserver().await?;
267 Ok(Self {
268 testnet,
269 #[cfg(feature = "docker-postgres")]
270 docker_postgres: None,
271 })
272 }
273
274 #[deprecated(
279 since = "0.5.0",
280 note = "Use Testnet::new_with_custom_postgres() and create_http_relay() for fine-grained control"
281 )]
282 pub async fn start_minimal_with_custom_postgres(
283 postgres_connection_string: ConnectionString,
284 ) -> anyhow::Result<Self> {
285 let mut me = Self {
286 testnet: Testnet::new_with_custom_postgres(postgres_connection_string).await?,
287 #[cfg(feature = "docker-postgres")]
288 docker_postgres: None,
289 };
290 me.testnet.create_http_relay().await?;
291 Ok(me)
292 }
293
294 #[deprecated(
299 since = "0.5.0",
300 note = "Use Testnet::new() and create_http_relay() for fine-grained control"
301 )]
302 pub async fn start_minimal() -> anyhow::Result<Self> {
303 let mut me = Self {
304 testnet: Testnet::new().await?,
305 #[cfg(feature = "docker-postgres")]
306 docker_postgres: None,
307 };
308 me.testnet.create_http_relay().await?;
309 Ok(me)
310 }
311
312 pub async fn create_random_homeserver(&mut self) -> anyhow::Result<&HomeserverApp> {
314 self.create_random_homeserver_with_config(None).await
315 }
316
317 pub async fn create_random_homeserver_with_config(
320 &mut self,
321 config: Option<ConfigToml>,
322 ) -> anyhow::Result<&HomeserverApp> {
323 let mut config = config.unwrap_or_else(ConfigToml::minimal_test_config);
324
325 if let Some(connection_string) = self.testnet.postgres_connection_string.as_ref() {
326 config.general.database_url = connection_string.clone();
327 }
328
329 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
330 self.testnet.create_homeserver_app_with_mock(mock_dir).await
331 }
332
333 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
335 self.testnet.client_builder()
336 }
337
338 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
340 self.testnet.client()
341 }
342
343 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
347 self.testnet.sdk()
348 }
349
350 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
352 self.testnet.pkarr_client_builder()
353 }
354
355 pub fn homeserver_app(&self) -> &pubky_homeserver::HomeserverApp {
357 self.testnet
358 .homeservers
359 .first()
360 .expect("homeservers should be non-empty")
361 }
362
363 pub fn http_relay(&self) -> &HttpRelay {
365 self.testnet
366 .http_relays
367 .first()
368 .expect("no http relay configured - use .with_http_relay() when building")
369 }
370}
371
372#[cfg(test)]
373mod test {
374 use super::*;
375
376 #[tokio::test]
380 async fn test_two_testnet_in_a_row() {
381 {
382 let _ = EphemeralTestnet::builder().build().await.unwrap();
383 }
384
385 {
386 let _ = EphemeralTestnet::builder().build().await.unwrap();
387 }
388 }
389
390 #[tokio::test]
391 async fn test_homeserver_with_random_keypair() {
392 let mut testnet = Testnet::new().await.unwrap();
394 testnet.create_http_relay().await.unwrap();
395 let mut network = EphemeralTestnet {
396 testnet,
397 #[cfg(feature = "docker-postgres")]
398 docker_postgres: None,
399 };
400 assert!(network.testnet.homeservers.is_empty());
401
402 let _ = network.create_random_homeserver().await.unwrap();
403 let _ = network.create_random_homeserver().await.unwrap();
404 assert!(network.testnet.homeservers.len() == 2);
405
406 assert_ne!(
408 network.testnet.homeservers[0].public_key(),
409 network.testnet.homeservers[1].public_key()
410 );
411 }
412
413 #[tokio::test]
414 async fn test_builder_default() {
415 let network = EphemeralTestnet::builder().build().await.unwrap();
417 let homeserver = network.homeserver_app();
418
419 assert!(
421 homeserver.admin_server().is_none(),
422 "Builder should use minimal config with admin disabled by default"
423 );
424 assert!(
425 homeserver.metrics_server().is_none(),
426 "Builder should use minimal config with metrics disabled by default"
427 );
428 }
429
430 #[tokio::test]
431 async fn test_builder_with_custom_config() {
432 let mut config = ConfigToml::minimal_test_config();
434 config.metrics.enabled = true;
435
436 let network = EphemeralTestnet::builder()
437 .config(config)
438 .build()
439 .await
440 .unwrap();
441
442 let homeserver = network.homeserver_app();
443 assert!(
444 homeserver.metrics_server().is_some(),
445 "Custom config should enable metrics"
446 );
447 assert!(
448 homeserver.admin_server().is_none(),
449 "Custom config should keep admin disabled"
450 );
451 }
452
453 #[tokio::test]
454 async fn test_builder_with_custom_keypair() {
455 let keypair = Keypair::random();
457 let expected_public_key = keypair.public_key();
458
459 let network = EphemeralTestnet::builder()
460 .keypair(keypair)
461 .build()
462 .await
463 .unwrap();
464
465 let homeserver = network.homeserver_app();
466 assert_eq!(
467 homeserver.public_key(),
468 expected_public_key,
469 "Custom keypair should be used"
470 );
471 }
472}