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: 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 = mainline::Testnet::builder(2).seeded(seeded).build()?;
35
36 let testnet = Self {
37 dht,
38 pkarr_relays: vec![],
39 http_relays: vec![],
40 homeservers: vec![],
41 temp_dirs: vec![],
42 postgres_connection_string: None,
43 };
44
45 Ok(testnet)
46 }
47
48 pub async fn new() -> Result<Self> {
50 Self::new_inner(true)
51 }
52
53 pub async fn new_unseeded() -> Result<Self> {
55 Self::new_inner(false)
56 }
57
58 pub async fn new_with_custom_postgres(
62 postgres_connection_string: ConnectionString,
63 ) -> Result<Self> {
64 let dht = mainline::Testnet::builder(2).build()?;
65 let testnet: Testnet = Self {
66 dht,
67 pkarr_relays: vec![],
68 http_relays: vec![],
69 homeservers: vec![],
70 temp_dirs: vec![],
71 postgres_connection_string: Some(postgres_connection_string),
72 };
73
74 Ok(testnet)
75 }
76
77 pub async fn create_homeserver(&mut self) -> Result<&HomeserverApp> {
82 let mut config = ConfigToml::default_test_config();
83 config.general.database_url = self.postgres_connection_string.clone();
84 let mock_dir = MockDataDir::new(config, Some(crate::common::testnet_keypair()))?;
85 self.create_homeserver_app_with_mock(mock_dir).await
86 }
87
88 pub async fn create_random_homeserver(&mut self) -> Result<&HomeserverApp> {
93 let mut config = ConfigToml::default_test_config();
94 config.general.database_url = self.postgres_connection_string.clone();
95 let mock_dir = MockDataDir::new(config, Some(Keypair::random()))?;
96 self.create_homeserver_app_with_mock(mock_dir).await
97 }
98
99 pub async fn create_homeserver_app_with_mock(
103 &mut self,
104 mut mock_dir: MockDataDir,
105 ) -> Result<&HomeserverApp> {
106 mock_dir.config_toml.pkdns.dht_bootstrap_nodes = Some(self.dht_bootstrap_nodes());
107 if !self.dht_relay_urls().is_empty() {
108 mock_dir.config_toml.pkdns.dht_relay_nodes = Some(self.dht_relay_urls().to_vec());
109 }
110 mock_dir.config_toml.storage.backend = StorageConfigToml::InMemory;
111 let homeserver = HomeserverApp::start_with_mock_data_dir(mock_dir).await?;
112 self.homeservers.push(homeserver);
113 Ok(self
114 .homeservers
115 .last()
116 .expect("homeservers should be non-empty"))
117 }
118
119 pub async fn create_http_relay(&mut self) -> Result<&HttpRelay> {
121 let relay = HttpRelay::builder()
122 .http_port(0) .cors_allow_all(true)
124 .run()
125 .await?;
126 self.http_relays.push(relay);
127 Ok(self
128 .http_relays
129 .last()
130 .expect("http relays should be non-empty"))
131 }
132
133 pub async fn create_pkarr_relay(&mut self) -> Result<Url> {
137 let dir = tempfile::tempdir()?;
138 let mut builder = pkarr_relay::Relay::builder();
139 builder
140 .disable_rate_limiter()
141 .http_port(0)
142 .storage(dir.path().to_path_buf())
143 .report_policy(pkarr::dht::ReportPolicy::testnet())
144 .dht(|config| {
145 config.bootstrap = Some(
146 self.dht
147 .bootstrap
148 .iter()
149 .map(|address| address.parse().expect("testnet bootstrap address is valid"))
150 .collect(),
151 );
152 config
153 });
154 let relay = unsafe { builder.run().await? };
155 let url = relay.local_url();
156 self.pkarr_relays.push(relay);
157 self.temp_dirs.push(dir);
158 Ok(url)
159 }
160
161 pub fn dht_bootstrap_nodes(&self) -> Vec<DomainPort> {
165 self.dht
166 .bootstrap
167 .iter()
168 .map(|address| {
169 DomainPort::from_str(address)
170 .expect("bootstrap nodes from the pkarr dht are always valid domain:port pairs")
171 })
172 .collect()
173 }
174
175 pub fn dht_relay_urls(&self) -> Vec<Url> {
177 self.pkarr_relays.iter().map(|r| r.local_url()).collect()
178 }
179
180 pub fn client_builder(&self) -> pubky::PubkyHttpClientBuilder {
182 let relays = self.dht_relay_urls();
183
184 let mut builder = pubky::PubkyHttpClient::builder();
185 builder.pkarr(|builder| {
186 builder
187 .no_default_network()
188 .bootstrap(&self.dht.bootstrap)
189 .dht_report_policy(pkarr::dht::ReportPolicy::testnet())
190 .request_timeout(Duration::from_millis(100));
193 if relays.is_empty() {
194 builder.no_relays()
195 } else {
196 builder
197 .relays(&relays)
198 .expect("testnet relays should be valid urls")
199 }
200 });
201
202 builder
203 }
204
205 pub fn client(&self) -> Result<pubky::PubkyHttpClient, pubky::BuildError> {
209 self.client_builder().build()
210 }
211
212 pub fn sdk(&self) -> Result<Pubky, pubky::BuildError> {
216 Ok(Pubky::with_client(self.client()?))
217 }
218
219 pub fn pkarr_client_builder(&self) -> pkarr::ClientBuilder {
221 let relays = self.dht_relay_urls();
222 let mut builder = pkarr::Client::builder();
223 builder.no_default_network(); builder
225 .bootstrap(&self.dht.bootstrap)
226 .dht_report_policy(pkarr::dht::ReportPolicy::testnet());
227 if !relays.is_empty() {
228 builder
229 .relays(&relays)
230 .expect("Testnet relays should be valid urls");
231 }
232
233 builder
234 }
235}
236
237#[cfg(test)]
238mod test {
239 use crate::Testnet;
240 use pubky::Keypair;
241 use pubky_common::auth::jws::ClientId;
242
243 #[tokio::test]
245 #[crate::test]
246 async fn test_keep_relays_alive_even_when_dropped() {
247 let mut testnet = Testnet::new().await.unwrap();
248 {
249 let _relay = testnet.create_http_relay().await.unwrap();
250 }
251 assert_eq!(testnet.http_relays.len(), 1);
252 }
253
254 #[tokio::test]
256 #[crate::test]
257 async fn test_boostrap_node_conversion() {
258 let testnet = Testnet::new().await.unwrap();
259 let nodes = testnet.dht_bootstrap_nodes();
260 assert_eq!(nodes.len(), 2);
261 }
262
263 #[tokio::test]
266 #[crate::test]
267 async fn test_signup() {
268 let mut testnet = Testnet::new().await.unwrap();
269 testnet.create_homeserver().await.unwrap();
270
271 let hs = testnet.homeservers.first().unwrap();
272 let sdk = testnet.sdk().unwrap();
273
274 let signer = sdk.signer(Keypair::random());
275
276 signer.signup(&hs.public_key(), None).await.unwrap();
277 let session = signer.signin(ClientId::new("test").unwrap()).await.unwrap();
278 assert_eq!(session.info().public_key(), &signer.public_key());
279 }
280
281 #[tokio::test]
282 async fn test_independent_dhts() {
283 let t1 = Testnet::new().await.unwrap();
284 let t2 = Testnet::new().await.unwrap();
285
286 assert_ne!(t1.dht.bootstrap, t2.dht.bootstrap);
287 }
288
289 #[tokio::test]
291 #[crate::test]
292 async fn test_homeserver_resolvable() {
293 let mut testnet = Testnet::new().await.unwrap();
294 let hs_pubky = testnet.create_homeserver().await.unwrap().public_key();
295
296 let pkarr_client = testnet.pkarr_client_builder().build().unwrap();
298 let _packet = pkarr_client
299 .resolve(&hs_pubky, pkarr::ResolvePolicy::CacheFirst)
300 .await
301 .unwrap();
302
303 let pubkey = hs_pubky.z32();
305 let _endpoint = pkarr_client
306 .resolve_https_endpoint(pubkey.as_str())
307 .await
308 .unwrap();
309 }
310
311 #[tokio::test]
315 #[crate::test]
316 async fn test_pkarr_relay_resolvable() {
317 let mut testnet = Testnet::new().await.unwrap();
318 testnet.create_pkarr_relay().await.unwrap();
319
320 let keypair = Keypair::random();
321
322 let client = testnet.pkarr_client_builder().build().unwrap();
324 let signed = pkarr::SignedPacket::builder().sign(&keypair).unwrap();
325 client.publish(&signed).await.unwrap();
326
327 let client = testnet.pkarr_client_builder().no_relays().build().unwrap();
330 let packet = client
331 .resolve(&keypair.public_key(), pkarr::ResolvePolicy::CacheFirst)
332 .await;
333 assert!(
334 packet.is_ok(),
335 "Published packet is not available over the DHT."
336 );
337
338 let client = testnet.pkarr_client_builder().no_dht().build().unwrap();
342 let packet = client
343 .resolve(&keypair.public_key(), pkarr::ResolvePolicy::CacheFirst)
344 .await;
345 assert!(
346 packet.is_ok(),
347 "Published packet is not available over the relay only."
348 );
349 }
350}