1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
5#![deny(rustdoc::broken_intra_doc_links)]
6#![cfg_attr(any(), deny(clippy::unwrap_used))]
7use anyhow::Result;
8use http_relay::HttpRelay;
9use pubky::{Keypair, Pubky};
10use pubky_homeserver::{
11 storage_config::StorageConfigToml, ConfigToml, ConnectionString, DomainPort, HomeserverApp,
12 MockDataDir,
13};
14use std::{str::FromStr, time::Duration};
15use url::Url;
16
17pub struct Testnet {
23 pub(crate) dht: pkarr::mainline::Testnet,
24 pub(crate) pkarr_relays: Vec<pkarr_relay::Relay>,
25 pub(crate) http_relays: Vec<HttpRelay>,
26 pub(crate) homeservers: Vec<HomeserverApp>,
27 pub(crate) postgres_connection_string: Option<ConnectionString>,
28
29 temp_dirs: Vec<tempfile::TempDir>,
30}
31
32impl Testnet {
33 fn new_inner(seeded: bool) -> Result<Self> {
34 let dht = pkarr::mainline::Testnet::builder(2)
35 .seeded(seeded)
36 .build()?;
37
38 let testnet = Self {
39 dht,
40 pkarr_relays: vec![],
41 http_relays: vec![],
42 homeservers: vec![],
43 temp_dirs: vec![],
44 postgres_connection_string: Self::extract_postgres_connection_string_from_env_variable(
45 ),
46 };
47
48 Ok(testnet)
49 }
50
51 pub async fn new() -> Result<Self> {
53 Self::new_inner(true)
54 }
55
56 pub async fn new_unseeded() -> Result<Self> {
58 Self::new_inner(false)
59 }
60
61 pub async fn new_with_custom_postgres(
65 postgres_connection_string: ConnectionString,
66 ) -> Result<Self> {
67 let dht = pkarr::mainline::Testnet::builder(2).build()?;
68 let testnet: Testnet = Self {
69 dht,
70 pkarr_relays: vec![],
71 http_relays: vec![],
72 homeservers: vec![],
73 temp_dirs: vec![],
74 postgres_connection_string: Some(postgres_connection_string),
75 };
76
77 Ok(testnet)
78 }
79
80 fn extract_postgres_connection_string_from_env_variable() -> Option<ConnectionString> {
84 if let Ok(raw_con_string) = std::env::var("TEST_PUBKY_CONNECTION_STRING") {
85 if let Ok(con_string) = ConnectionString::new(&raw_con_string) {
86 return Some(con_string);
87 } else {
88 tracing::warn!("Invalid database connection string in TEST_PUBKY_CONNECTION_STRING environment variable. Ignoring it.");
89 }
90 }
91 None
92 }
93
94 pub async fn create_homeserver(&mut self) -> Result<&HomeserverApp> {
99 let mut config = ConfigToml::default_test_config();
100 if let Some(connection_string) = self.postgres_connection_string.as_ref() {
101 config.general.database_url = connection_string.clone();
102 }
103 let mock_dir = MockDataDir::new(config, Some(crate::common::testnet_keypair()))?;
104 self.create_homeserver_app_with_mock(mock_dir).await
105 }
106
107 pub async fn create_random_homeserver(&mut self) -> Result<&HomeserverApp> {
112 let mut config = ConfigToml::default_test_config();
113 if let Some(connection_string) = self.postgres_connection_string.as_ref() {
114 config.general.database_url = connection_string.clone();
115 }
116 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
117 self.create_homeserver_app_with_mock(mock_dir).await
118 }
119
120 pub async fn create_homeserver_app_with_mock(
124 &mut self,
125 mut mock_dir: MockDataDir,
126 ) -> Result<&HomeserverApp> {
127 mock_dir.config_toml.pkdns.dht_bootstrap_nodes = Some(self.dht_bootstrap_nodes());
128 if !self.dht_relay_urls().is_empty() {
129 mock_dir.config_toml.pkdns.dht_relay_nodes = Some(self.dht_relay_urls().to_vec());
130 }
131 mock_dir.config_toml.storage.backend = StorageConfigToml::InMemory;
132 let homeserver = HomeserverApp::start_with_mock_data_dir(mock_dir).await?;
133 self.homeservers.push(homeserver);
134 Ok(self
135 .homeservers
136 .last()
137 .expect("homeservers should be non-empty"))
138 }
139
140 pub async fn create_http_relay(&mut self) -> Result<&HttpRelay> {
142 let relay = HttpRelay::builder()
143 .http_port(0) .cors_allow_all(true)
145 .run()
146 .await?;
147 self.http_relays.push(relay);
148 Ok(self
149 .http_relays
150 .last()
151 .expect("http relays should be non-empty"))
152 }
153
154 pub async fn create_pkarr_relay(&mut self) -> Result<Url> {
158 let dir = tempfile::tempdir()?;
159 let mut builder = pkarr_relay::Relay::builder();
160 builder
161 .disable_rate_limiter()
162 .http_port(0)
163 .storage(dir.path().to_path_buf())
164 .report_policy(pkarr::dht::ReportPolicy::testnet())
165 .dht(|config| {
166 config.bootstrap = Some(
167 self.dht
168 .bootstrap
169 .iter()
170 .map(|address| address.parse().expect("testnet bootstrap address is valid"))
171 .collect(),
172 );
173 config
174 });
175 let relay = unsafe { builder.run().await? };
176 let url = relay.local_url();
177 self.pkarr_relays.push(relay);
178 self.temp_dirs.push(dir);
179 Ok(url)
180 }
181
182 pub fn dht_bootstrap_nodes(&self) -> Vec<DomainPort> {
186 self.dht
187 .bootstrap
188 .iter()
189 .map(|address| {
190 DomainPort::from_str(address)
191 .expect("bootstrap nodes from the pkarr dht are always valid domain:port pairs")
192 })
193 .collect()
194 }
195
196 pub fn dht_relay_urls(&self) -> Vec<Url> {
198 self.pkarr_relays.iter().map(|r| r.local_url()).collect()
199 }
200
201 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
203 let relays = self.dht_relay_urls();
204
205 let mut builder = pubky::PubkyHttpClient::builder();
206 builder.pkarr(|builder| {
207 builder
208 .no_default_network()
209 .bootstrap(&self.dht.bootstrap)
210 .dht_report_policy(pkarr::dht::ReportPolicy::testnet())
211 .request_timeout(Duration::from_millis(100));
214 if relays.is_empty() {
215 builder.no_relays()
216 } else {
217 builder
218 .relays(&relays)
219 .expect("testnet relays should be valid urls")
220 }
221 });
222
223 builder
224 }
225
226 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
230 self.client_builder().build()
231 }
232
233 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
237 Ok(Pubky::with_client(self.client()?))
238 }
239
240 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
242 let relays = self.dht_relay_urls();
243 let mut builder = pkarr::Client::builder();
244 builder.no_default_network(); builder
246 .bootstrap(&self.dht.bootstrap)
247 .dht_report_policy(pkarr::dht::ReportPolicy::testnet());
248 if !relays.is_empty() {
249 builder
250 .relays(&relays)
251 .expect("Testnet relays should be valid urls");
252 }
253
254 builder
255 }
256}
257
258#[cfg(test)]
259mod test {
260 use crate::Testnet;
261 use pubky::Keypair;
262
263 #[tokio::test]
265 #[crate::test]
266 async fn test_keep_relays_alive_even_when_dropped() {
267 let mut testnet = Testnet::new().await.unwrap();
268 {
269 let _relay = testnet.create_http_relay().await.unwrap();
270 }
271 assert_eq!(testnet.http_relays.len(), 1);
272 }
273
274 #[tokio::test]
276 #[crate::test]
277 async fn test_boostrap_node_conversion() {
278 let testnet = Testnet::new().await.unwrap();
279 let nodes = testnet.dht_bootstrap_nodes();
280 assert_eq!(nodes.len(), 2);
281 }
282
283 #[tokio::test]
286 #[crate::test]
287 async fn test_signup() {
288 let mut testnet = Testnet::new().await.unwrap();
289 testnet.create_homeserver().await.unwrap();
290
291 let hs = testnet.homeservers.first().unwrap();
292 let sdk = testnet.sdk().unwrap();
293
294 let signer = sdk.signer(Keypair::random());
295
296 let session = signer.signup_cookie(&hs.public_key(), None).await.unwrap();
297 assert_eq!(session.info().public_key(), &signer.public_key());
298 }
299
300 #[tokio::test]
301 async fn test_independent_dhts() {
302 let t1 = Testnet::new().await.unwrap();
303 let t2 = Testnet::new().await.unwrap();
304
305 assert_ne!(t1.dht.bootstrap, t2.dht.bootstrap);
306 }
307
308 #[tokio::test]
310 async fn test_homeserver_resolvable() {
311 let mut testnet = Testnet::new().await.unwrap();
312 let hs_pubky = testnet.create_homeserver().await.unwrap().public_key();
313
314 let pkarr_client = testnet.pkarr_client_builder().build().unwrap();
316 let _packet = pkarr_client
317 .resolve(&hs_pubky, pkarr::ResolvePolicy::CacheFirst)
318 .await
319 .unwrap();
320
321 let pubkey = hs_pubky.z32();
323 let _endpoint = pkarr_client
324 .resolve_https_endpoint(pubkey.as_str())
325 .await
326 .unwrap();
327 }
328
329 #[tokio::test]
333 #[crate::test]
334 async fn test_pkarr_relay_resolvable() {
335 let mut testnet = Testnet::new().await.unwrap();
336 testnet.create_pkarr_relay().await.unwrap();
337
338 let keypair = Keypair::random();
339
340 let client = testnet.pkarr_client_builder().build().unwrap();
342 let signed = pkarr::SignedPacket::builder().sign(&keypair).unwrap();
343 client.publish(&signed).await.unwrap();
344
345 let client = testnet.pkarr_client_builder().no_relays().build().unwrap();
348 let packet = client
349 .resolve(&keypair.public_key(), pkarr::ResolvePolicy::CacheFirst)
350 .await;
351 assert!(
352 packet.is_ok(),
353 "Published packet is not available over the DHT."
354 );
355
356 let client = testnet.pkarr_client_builder().no_dht().build().unwrap();
360 let packet = client
361 .resolve(&keypair.public_key(), pkarr::ResolvePolicy::CacheFirst)
362 .await;
363 assert!(
364 packet.is_ok(),
365 "Published packet is not available over the relay only."
366 );
367 }
368}