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 = match seeded {
35 true => pkarr::mainline::Testnet::new(2)?,
36 false => pkarr::mainline::Testnet::new_unseeded(2)?,
37 };
38
39 let testnet = Self {
40 dht,
41 pkarr_relays: vec![],
42 http_relays: vec![],
43 homeservers: vec![],
44 temp_dirs: vec![],
45 postgres_connection_string: Self::extract_postgres_connection_string_from_env_variable(
46 ),
47 };
48
49 Ok(testnet)
50 }
51
52 pub async fn new() -> Result<Self> {
54 Self::new_inner(true)
55 }
56
57 pub async fn new_unseeded() -> Result<Self> {
59 Self::new_inner(false)
60 }
61
62 pub async fn new_with_custom_postgres(
66 postgres_connection_string: ConnectionString,
67 ) -> Result<Self> {
68 let dht = pkarr::mainline::Testnet::builder(2).build()?;
69 let testnet: Testnet = Self {
70 dht,
71 pkarr_relays: vec![],
72 http_relays: vec![],
73 homeservers: vec![],
74 temp_dirs: vec![],
75 postgres_connection_string: Some(postgres_connection_string),
76 };
77
78 Ok(testnet)
79 }
80
81 fn extract_postgres_connection_string_from_env_variable() -> Option<ConnectionString> {
85 if let Ok(raw_con_string) = std::env::var("TEST_PUBKY_CONNECTION_STRING") {
86 if let Ok(con_string) = ConnectionString::new(&raw_con_string) {
87 return Some(con_string);
88 } else {
89 tracing::warn!("Invalid database connection string in TEST_PUBKY_CONNECTION_STRING environment variable. Ignoring it.");
90 }
91 }
92 None
93 }
94
95 pub async fn create_homeserver(&mut self) -> Result<&HomeserverApp> {
100 let mut config = ConfigToml::default_test_config();
101 if let Some(connection_string) = self.postgres_connection_string.as_ref() {
102 config.general.database_url = connection_string.clone();
103 }
104 let mock_dir = MockDataDir::new(config, Some(Keypair::from_secret(&[0; 32])))?;
105 self.create_homeserver_app_with_mock(mock_dir).await
106 }
107
108 pub async fn create_random_homeserver(&mut self) -> Result<&HomeserverApp> {
113 let mut config = ConfigToml::default_test_config();
114 if let Some(connection_string) = self.postgres_connection_string.as_ref() {
115 config.general.database_url = connection_string.clone();
116 }
117 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
118 self.create_homeserver_app_with_mock(mock_dir).await
119 }
120
121 pub async fn create_homeserver_app_with_mock(
125 &mut self,
126 mut mock_dir: MockDataDir,
127 ) -> Result<&HomeserverApp> {
128 mock_dir.config_toml.pkdns.dht_bootstrap_nodes = Some(self.dht_bootstrap_nodes());
129 if !self.dht_relay_urls().is_empty() {
130 mock_dir.config_toml.pkdns.dht_relay_nodes = Some(self.dht_relay_urls().to_vec());
131 }
132 mock_dir.config_toml.storage = StorageConfigToml::InMemory;
133 let homeserver = HomeserverApp::start_with_mock_data_dir(mock_dir).await?;
134 self.homeservers.push(homeserver);
135 Ok(self
136 .homeservers
137 .last()
138 .expect("homeservers should be non-empty"))
139 }
140
141 pub async fn create_http_relay(&mut self) -> Result<&HttpRelay> {
143 let relay = HttpRelay::builder()
144 .http_port(0) .cors_allow_all(true)
146 .run()
147 .await?;
148 self.http_relays.push(relay);
149 Ok(self
150 .http_relays
151 .last()
152 .expect("http relays should be non-empty"))
153 }
154
155 pub async fn create_pkarr_relay(&mut self) -> Result<Url> {
159 let dir = tempfile::tempdir()?;
160 let mut builder = pkarr_relay::Relay::builder();
161 builder
162 .disable_rate_limiter()
163 .http_port(0)
164 .storage(dir.path().to_path_buf())
165 .pkarr(|builder| {
166 builder.no_default_network();
167 builder.bootstrap(&self.dht.bootstrap);
168 builder
169 });
170 let relay = unsafe { builder.run().await? };
171 let url = relay.local_url();
172 self.pkarr_relays.push(relay);
173 self.temp_dirs.push(dir);
174 Ok(url)
175 }
176
177 pub fn dht_bootstrap_nodes(&self) -> Vec<DomainPort> {
181 self.dht
182 .nodes
183 .iter()
184 .map(|node| {
185 let addr = node.info().local_addr();
186 DomainPort::from_str(&format!("{}:{}", addr.ip(), addr.port()))
187 .expect("boostrap nodes from the pkarr dht are always valid domain:port pairs")
188 })
189 .collect()
190 }
191
192 pub fn dht_relay_urls(&self) -> Vec<Url> {
194 self.pkarr_relays.iter().map(|r| r.local_url()).collect()
195 }
196
197 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
199 let relays = self.dht_relay_urls();
200
201 let mut builder = pubky::PubkyHttpClient::builder();
202 builder.pkarr(|builder| {
203 builder.no_default_network();
204 builder.bootstrap(&self.dht.bootstrap);
205 if relays.is_empty() {
206 builder.no_relays();
207 } else {
208 builder
209 .relays(&relays)
210 .expect("testnet relays should be valid urls");
211 }
212 builder.request_timeout(Duration::from_millis(100));
215 builder
216 });
217
218 builder
219 }
220
221 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
225 self.client_builder().build()
226 }
227
228 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
232 Ok(Pubky::with_client(self.client()?))
233 }
234
235 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
237 let relays = self.dht_relay_urls();
238 let mut builder = pkarr::Client::builder();
239 builder.no_default_network(); builder.bootstrap(&self.dht.bootstrap);
241 if !relays.is_empty() {
242 builder
243 .relays(&relays)
244 .expect("Testnet relays should be valid urls");
245 }
246
247 builder
248 }
249}
250
251#[cfg(test)]
252mod test {
253 use crate::Testnet;
254 use pubky::Keypair;
255
256 #[tokio::test]
258 #[crate::test]
259 async fn test_keep_relays_alive_even_when_dropped() {
260 let mut testnet = Testnet::new().await.unwrap();
261 {
262 let _relay = testnet.create_http_relay().await.unwrap();
263 }
264 assert_eq!(testnet.http_relays.len(), 1);
265 }
266
267 #[tokio::test]
269 #[crate::test]
270 async fn test_boostrap_node_conversion() {
271 let testnet = Testnet::new().await.unwrap();
272 let nodes = testnet.dht_bootstrap_nodes();
273 assert_eq!(nodes.len(), 2);
274 }
275
276 #[tokio::test]
279 #[crate::test]
280 async fn test_signup() {
281 let mut testnet = Testnet::new().await.unwrap();
282 testnet.create_homeserver().await.unwrap();
283
284 let hs = testnet.homeservers.first().unwrap();
285 let sdk = testnet.sdk().unwrap();
286
287 let signer = sdk.signer(Keypair::random());
288
289 let session = signer.signup(&hs.public_key(), None).await.unwrap();
290 assert_eq!(session.info().public_key(), &signer.public_key());
291 }
292
293 #[tokio::test]
294 async fn test_independent_dhts() {
295 let t1 = Testnet::new().await.unwrap();
296 let t2 = Testnet::new().await.unwrap();
297
298 assert_ne!(t1.dht.bootstrap, t2.dht.bootstrap);
299 }
300
301 #[tokio::test]
303 async fn test_homeserver_resolvable() {
304 let mut testnet = Testnet::new().await.unwrap();
305 let hs_pubky = testnet.create_homeserver().await.unwrap().public_key();
306
307 let pkarr_client = testnet.pkarr_client_builder().build().unwrap();
309 let _packet = pkarr_client.resolve(&hs_pubky).await.unwrap();
310
311 let pubkey = hs_pubky.z32();
313 let _endpoint = pkarr_client
314 .resolve_https_endpoint(pubkey.as_str())
315 .await
316 .unwrap();
317 }
318
319 #[tokio::test]
323 #[crate::test]
324 async fn test_pkarr_relay_resolvable() {
325 let mut testnet = Testnet::new().await.unwrap();
326 testnet.create_pkarr_relay().await.unwrap();
327
328 let keypair = Keypair::random();
329
330 let client = testnet.pkarr_client_builder().build().unwrap();
332 let signed = pkarr::SignedPacket::builder().sign(&keypair).unwrap();
333 client.publish(&signed, None).await.unwrap();
334
335 let client = testnet.pkarr_client_builder().no_relays().build().unwrap();
338 let packet = client.resolve(&keypair.public_key()).await;
339 assert!(
340 packet.is_some(),
341 "Published packet is not available over the DHT."
342 );
343
344 let client = testnet.pkarr_client_builder().no_dht().build().unwrap();
348 let packet = client.resolve(&keypair.public_key()).await;
349 assert!(
350 packet.is_some(),
351 "Published packet is not available over the relay only."
352 );
353 }
354}