1use std::{
2 str::FromStr,
3 sync::{Arc, RwLock},
4};
5
6use anyhow::{Result, anyhow, ensure};
7use iroh::{Endpoint, EndpointAddr, EndpointId, endpoint::ConnectError};
8use iroh_metrics::{MetricsGroup, Registry, encoding::Encoder};
9use irpc_iroh::IrohLazyRemoteConnection;
10use n0_error::StackResultExt;
11use n0_future::{task::AbortOnDropHandle, time::Duration};
12use rcan::Rcan;
13use tokio::sync::oneshot;
14use tracing::{debug, trace, warn};
15use uuid::Uuid;
16
17use crate::{
18 api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
19 caps::{Caps, DEFAULT_CAP_EXPIRY},
20 net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
21 protocol::{
22 ALPN, Auth, IrohServicesClient, NameEndpoint, Ping, Pong, PutMetrics,
23 PutNetworkDiagnostics, RemoteError,
24 },
25};
26
27#[derive(Debug, Clone)]
51pub struct Client {
52 #[allow(dead_code)]
54 endpoint: Endpoint,
55 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
56 _actor_task: Arc<AbortOnDropHandle<()>>,
57}
58
59pub struct ClientBuilder {
62 #[allow(dead_code)]
63 cap_expiry: Duration,
64 cap: Option<Rcan<Caps>>,
65 endpoint: Endpoint,
66 name: Option<String>,
67 metrics_interval: Option<Duration>,
68 remote: Option<EndpointAddr>,
69 registry: Registry,
70}
71
72impl ClientBuilder {
73 pub fn new(endpoint: &Endpoint) -> Self {
74 let mut registry = Registry::default();
75 registry.register_all(endpoint.metrics());
76
77 Self {
78 cap: None,
79 cap_expiry: DEFAULT_CAP_EXPIRY,
80 endpoint: endpoint.clone(),
81 name: None,
82 metrics_interval: Some(Duration::from_secs(60)),
83 remote: None,
84 registry,
85 }
86 }
87
88 pub fn register_metrics_group(mut self, metrics_group: Arc<dyn MetricsGroup>) -> Self {
92 self.registry.register(metrics_group);
93 self
94 }
95
96 pub fn metrics_interval(mut self, interval: Duration) -> Self {
100 self.metrics_interval = Some(interval);
101 self
102 }
103
104 pub fn disable_metrics_interval(mut self) -> Self {
106 self.metrics_interval = None;
107 self
108 }
109
110 pub fn name(mut self, name: impl Into<String>) -> Result<Self> {
125 let name = name.into();
126 validate_name(&name).map_err(BuildError::InvalidName)?;
127 self.name = Some(name);
128 Ok(self)
129 }
130
131 pub fn api_secret_from_env(self) -> Result<Self> {
133 let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
134 self.api_secret(ticket)
135 }
136
137 pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
139 let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
140 self.api_secret(key)
141 }
142
143 pub fn api_secret(mut self, ticket: ApiSecret) -> Result<Self> {
150 let local_id = self.endpoint.id();
151 let rcan = crate::caps::create_api_token_from_secret_key(
152 ticket.secret,
153 local_id,
154 self.cap_expiry,
155 Caps::for_shared_secret(),
156 )?;
157
158 self.remote = Some(ticket.remote);
159 self.rcan(rcan)
160 }
161
162 #[cfg(not(wasm_browser))]
166 pub async fn ssh_key_from_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self> {
167 let file_content = tokio::fs::read_to_string(path).await?;
168 self.ssh_key(&file_content)
169 }
170
171 #[cfg(not(wasm_browser))]
173 pub fn ssh_key(mut self, pem: &str) -> Result<Self> {
174 let local_id = self.endpoint.id();
175 let rcan = crate::caps::create_api_token_from_openssh_pem(
176 pem,
177 local_id,
178 self.cap_expiry,
179 Caps::all(),
180 )?;
181 self.cap.replace(rcan);
182
183 Ok(self)
184 }
185
186 pub fn rcan(mut self, cap: Rcan<Caps>) -> Result<Self> {
188 ensure!(
189 EndpointId::from_verifying_key(*cap.audience()) == self.endpoint.id(),
190 "invalid audience"
191 );
192 self.cap.replace(cap);
193 Ok(self)
194 }
195
196 pub fn remote(mut self, remote: impl Into<EndpointAddr>) -> Self {
199 self.remote = Some(remote.into());
200 self
201 }
202
203 #[must_use = "dropping the client will silently cancel all client tasks"]
205 pub async fn build(self) -> Result<Client, BuildError> {
206 debug!("starting iroh-services client");
207 let remote = self.remote.ok_or(BuildError::MissingRemote)?;
208 let capabilities = self.cap.ok_or(BuildError::MissingCapability)?;
209
210 let conn = IrohLazyRemoteConnection::new(self.endpoint.clone(), remote, ALPN.to_vec());
211 let irpc_client = IrohServicesClient::boxed(conn);
212
213 let registry = Arc::new(RwLock::new(self.registry));
214 let (tx, rx) = tokio::sync::mpsc::channel(1);
215 let actor_task = AbortOnDropHandle::new(n0_future::task::spawn(
216 ClientActor {
217 capabilities,
218 client: irpc_client,
219 name: self.name.clone(),
220 session_id: Uuid::new_v4(),
221 authorized: false,
222 encoder: Encoder::new(registry.clone()),
223 registry,
224 }
225 .run(self.name, self.metrics_interval, rx),
226 ));
227
228 Ok(Client {
229 endpoint: self.endpoint,
230 message_channel: tx,
231 _actor_task: Arc::new(actor_task),
232 })
233 }
234}
235
236#[derive(thiserror::Error, Debug)]
237pub enum BuildError {
238 #[error("Missing remote endpoint to dial")]
239 MissingRemote,
240 #[error("Missing capability")]
241 MissingCapability,
242 #[error("Unauthorized")]
243 Unauthorized,
244 #[error("Remote error: {0}")]
245 Remote(#[from] RemoteError),
246 #[error("Rpc connection error: {0}")]
247 Rpc(irpc::Error),
248 #[error("Connection error: {0}")]
249 Connect(ConnectError),
250 #[error("Invalid endpoint name: {0}")]
251 InvalidName(#[from] ValidateNameError),
252}
253
254impl From<irpc::Error> for BuildError {
255 fn from(value: irpc::Error) -> Self {
256 match value {
257 irpc::Error::Request {
258 source:
259 irpc::RequestError::Connection {
260 source: iroh::endpoint::ConnectionError::ApplicationClosed(frame),
261 ..
262 },
263 ..
264 } if frame.error_code == 401u32.into() => Self::Unauthorized,
265 value => Self::Rpc(value),
266 }
267 }
268}
269
270pub const CLIENT_NAME_MIN_LENGTH: usize = 2;
272pub const CLIENT_NAME_MAX_LENGTH: usize = 128;
274
275#[derive(Debug, thiserror::Error)]
277pub enum ValidateNameError {
278 #[error("Name is too long (must be no more than {CLIENT_NAME_MAX_LENGTH} characters).")]
279 TooLong,
280 #[error("Name is too short (must be at least {CLIENT_NAME_MIN_LENGTH} characters).")]
281 TooShort,
282}
283
284fn validate_name(name: &str) -> Result<(), ValidateNameError> {
285 if name.len() < CLIENT_NAME_MIN_LENGTH {
286 Err(ValidateNameError::TooShort)
287 } else if name.len() > CLIENT_NAME_MAX_LENGTH {
288 Err(ValidateNameError::TooLong)
289 } else {
290 Ok(())
291 }
292}
293
294#[derive(thiserror::Error, Debug)]
295pub enum Error {
296 #[error("Invalid endpoint name: {0}")]
297 InvalidName(#[from] ValidateNameError),
298 #[error("Remote error: {0}")]
299 Remote(#[from] RemoteError),
300 #[error("Connection error: {0}")]
301 Rpc(#[from] irpc::Error),
302 #[error(transparent)]
303 Other(#[from] anyhow::Error),
304}
305
306impl Client {
307 pub fn builder(endpoint: &Endpoint) -> ClientBuilder {
308 ClientBuilder::new(endpoint)
309 }
310
311 pub async fn name(&self) -> Result<Option<String>, Error> {
313 let (tx, rx) = oneshot::channel();
314 self.message_channel
315 .send(ClientActorMessage::ReadName { done: tx })
316 .await
317 .map_err(|_| Error::Other(anyhow!("sending name read request")))?;
318
319 rx.await
320 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))
321 }
322
323 pub async fn set_name(&self, name: impl Into<String>) -> Result<(), Error> {
328 set_name_inner(self.message_channel.clone(), name.into()).await
329 }
330
331 pub async fn ping(&self) -> Result<Pong, Error> {
333 let (tx, rx) = oneshot::channel();
334 self.message_channel
335 .send(ClientActorMessage::Ping { done: tx })
336 .await
337 .map_err(|_| Error::Other(anyhow!("sending ping request")))?;
338
339 rx.await
340 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
341 .map_err(Error::Remote)
342 }
343
344 pub async fn push_metrics(&self) -> Result<(), Error> {
348 let (tx, rx) = oneshot::channel();
349 self.message_channel
350 .send(ClientActorMessage::SendMetrics { done: tx })
351 .await
352 .map_err(|_| Error::Other(anyhow!("sending metrics")))?;
353
354 rx.await
355 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
356 .map_err(Error::Remote)
357 }
358
359 pub async fn grant_capability(
363 &self,
364 remote_id: EndpointId,
365 caps: impl IntoIterator<Item = impl Into<crate::caps::Cap>>,
366 ) -> Result<(), Error> {
367 let cap = crate::caps::create_grant_token(
368 self.endpoint.secret_key().clone(),
369 remote_id,
370 DEFAULT_CAP_EXPIRY,
371 Caps::new(caps),
372 )
373 .map_err(Error::Other)?;
374
375 let (tx, rx) = oneshot::channel();
376 self.message_channel
377 .send(ClientActorMessage::GrantCap {
378 cap: Box::new(cap),
379 done: tx,
380 })
381 .await
382 .map_err(|_| Error::Other(anyhow!("granting capability")))?;
383
384 rx.await
385 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
386 }
387
388 pub async fn net_diagnostics(&self, send: bool) -> Result<DiagnosticsReport, Error> {
390 let report = run_diagnostics(&self.endpoint).await?;
391 if send {
392 let (tx, rx) = oneshot::channel();
393 self.message_channel
394 .send(ClientActorMessage::PutNetworkDiagnostics {
395 done: tx,
396 report: Box::new(report.clone()),
397 })
398 .await
399 .map_err(|_| Error::Other(anyhow!("sending network diagnostics report")))?;
400
401 let _ = rx
402 .await
403 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?;
404 }
405
406 Ok(report)
407 }
408}
409
410enum ClientActorMessage {
411 SendMetrics {
412 done: oneshot::Sender<Result<(), RemoteError>>,
413 },
414 Ping {
415 done: oneshot::Sender<Result<Pong, RemoteError>>,
416 },
417 #[allow(dead_code)]
419 GrantCap {
420 cap: Box<Rcan<Caps>>,
422 done: oneshot::Sender<Result<(), Error>>,
423 },
424 PutNetworkDiagnostics {
425 report: Box<DiagnosticsReport>,
426 done: oneshot::Sender<Result<(), Error>>,
427 },
428 ReadName {
429 done: oneshot::Sender<Option<String>>,
430 },
431 NameEndpoint {
432 name: String,
433 done: oneshot::Sender<Result<(), RemoteError>>,
434 },
435}
436
437struct ClientActor {
438 capabilities: Rcan<Caps>,
439 client: IrohServicesClient,
440 name: Option<String>,
441 session_id: Uuid,
442 authorized: bool,
443 encoder: Encoder,
444 registry: Arc<RwLock<Registry>>,
446}
447
448impl ClientActor {
449 async fn run(
450 mut self,
451 initial_name: Option<String>,
452 interval: Option<Duration>,
453 mut inbox: tokio::sync::mpsc::Receiver<ClientActorMessage>,
454 ) {
455 let mut metrics_timer = interval.map(|interval| n0_future::time::interval(interval));
456 trace!("starting client actor");
457
458 if let Some(name) = initial_name
459 && let Err(err) = self.send_name_endpoint(name).await
460 {
461 warn!(err = %err, "failed setting endpoint name on startup");
462 }
463
464 loop {
465 trace!("client actor tick");
466 tokio::select! {
467 biased;
468 Some(msg) = inbox.recv() => {
469 match msg {
470 ClientActorMessage::Ping{ done } => {
471 let res = self.send_ping().await;
472 if let Err(err) = done.send(res) {
473 debug!("failed to send ping: {:#?}", err);
474 self.authorized = false;
475 }
476 },
477 ClientActorMessage::SendMetrics{ done } => {
478 trace!("sending metrics manually triggered");
479 let res = self.send_metrics().await;
480 if let Err(err) = done.send(res) {
481 debug!("failed to push metrics: {:#?}", err);
482 self.authorized = false;
483 }
484 }
485 ClientActorMessage::GrantCap{ cap, done } => {
486 let res = self.grant_cap(*cap).await;
487 if let Err(err) = done.send(res) {
488 warn!("failed to grant capability: {:#?}", err);
489 }
490 }
491 ClientActorMessage::ReadName{ done } => {
492 if let Err(err) = done.send(self.name.clone()) {
493 warn!("sending name value: {:#?}", err);
494 }
495 }
496 ClientActorMessage::NameEndpoint{ name, done } => {
497 let res = self.send_name_endpoint(name).await;
498 if let Err(err) = done.send(res) {
499 warn!("failed to name endpoint: {:#?}", err);
500 }
501 }
502 ClientActorMessage::PutNetworkDiagnostics{ report, done } => {
503 let res = self.put_network_diagnostics(*report).await;
504 if let Err(err) = done.send(res) {
505 warn!("failed to publish network diagnostics: {:#?}", err);
506 }
507 }
508 }
509 }
510 _ = async {
511 if let Some(ref mut timer) = metrics_timer {
512 timer.tick().await;
513 } else {
514 std::future::pending::<()>().await;
515 }
516 } => {
517 trace!("metrics send tick");
518 if let Err(err) = self.send_metrics().await {
519 debug!("failed to push metrics: {:#?}", err);
520 self.authorized = false;
521 }
522 },
523 }
524 }
525 }
526
527 async fn auth(&mut self) -> Result<(), RemoteError> {
529 if self.authorized {
530 return Ok(());
531 }
532 trace!("client authorizing");
533 self.client
534 .rpc(Auth {
535 caps: self.capabilities.clone(),
536 })
537 .await
538 .inspect_err(|e| debug!("authorization failed: {:?}", e))
539 .map_err(|e| RemoteError::AuthError(e.to_string()))?;
540 self.authorized = true;
541 self.encoder = Encoder::new(self.registry.clone());
547 Ok(())
548 }
549
550 fn track_rpc<T>(&mut self, res: Result<T, irpc::Error>) -> Result<T, irpc::Error> {
556 if res.is_err() {
557 self.authorized = false;
558 }
559 res
560 }
561
562 async fn send_ping(&mut self) -> Result<Pong, RemoteError> {
563 trace!("client actor send ping");
564 self.auth().await?;
565
566 let req = rand::random();
567 let res = self.client.rpc(Ping { req_id: req }).await;
568 self.track_rpc(res)
569 .inspect_err(|e| warn!("rpc ping error: {e}"))
570 .map_err(|_| RemoteError::InternalServerError)
571 }
572
573 async fn send_name_endpoint(&mut self, name: String) -> Result<(), RemoteError> {
574 trace!("client sending name endpoint request");
575 self.auth().await?;
576
577 let res = self.client.rpc(NameEndpoint { name: name.clone() }).await;
578 self.track_rpc(res)
579 .inspect_err(|e| debug!("name endpoint error: {e}"))
580 .map_err(|_| RemoteError::InternalServerError)??;
581 self.name = Some(name);
582 Ok(())
583 }
584
585 async fn send_metrics(&mut self) -> Result<(), RemoteError> {
586 trace!("client actor send metrics");
587 self.auth().await?;
588
589 let update = self.encoder.export();
590 let req = PutMetrics {
592 session_id: self.session_id,
593 update,
594 };
595
596 let res = self.client.rpc(req).await;
597 self.track_rpc(res)
598 .map_err(|_| RemoteError::InternalServerError)??;
599
600 Ok(())
601 }
602
603 async fn grant_cap(&mut self, cap: Rcan<Caps>) -> Result<(), Error> {
604 trace!("client actor grant capability");
605 self.auth().await?;
606
607 let res = self.client.rpc(crate::protocol::GrantCap { cap }).await;
608 self.track_rpc(res)
609 .map_err(|_| RemoteError::InternalServerError)??;
610
611 Ok(())
612 }
613
614 async fn put_network_diagnostics(
615 &mut self,
616 report: crate::net_diagnostics::DiagnosticsReport,
617 ) -> Result<(), Error> {
618 trace!("client actor publish network diagnostics");
619 self.auth().await?;
620
621 let req = PutNetworkDiagnostics { report };
622
623 let res = self.client.rpc(req).await;
624 self.track_rpc(res)
625 .map_err(|_| RemoteError::InternalServerError)??;
626
627 Ok(())
628 }
629}
630
631async fn set_name_inner(
632 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
633 name: String,
634) -> Result<(), Error> {
635 validate_name(&name)?;
636 debug!(name_len = name.len(), "calling set name");
637 let (tx, rx) = oneshot::channel();
638 message_channel
639 .send(ClientActorMessage::NameEndpoint { name, done: tx })
640 .await
641 .map_err(|_| Error::Other(anyhow!("sending name endpoint request")))?;
642 rx.await
643 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
644 .map_err(Error::Remote)
645}
646
647#[cfg(test)]
648mod tests {
649 use std::sync::{
650 Arc, RwLock,
651 atomic::{AtomicBool, Ordering},
652 };
653
654 use iroh::{
655 Endpoint, EndpointAddr, SecretKey,
656 endpoint::{Connection, presets},
657 protocol::{AcceptError, ProtocolHandler, Router},
658 };
659 use iroh_metrics::{
660 Registry,
661 encoding::{Decoder, Encoder},
662 };
663 use irpc::WithChannels;
664 use irpc_iroh::read_request;
665 use n0_error::AnyError;
666 use n0_future::time::Duration;
667 use rand::{RngExt, SeedableRng};
668 use temp_env_vars::temp_env_vars;
669
670 use crate::{
671 Client,
672 api_secret::ApiSecret,
673 caps::{Cap, Caps, create_api_token_from_secret_key},
674 client::{API_SECRET_ENV_VAR_NAME, BuildError, ValidateNameError},
675 protocol::{ALPN, IrohServicesProtocol, ServicesMessage},
676 };
677
678 #[derive(Debug)]
680 struct SeenUpdate {
681 has_schema: bool,
682 decoded_items: usize,
683 }
684
685 #[derive(Debug)]
690 struct RecordingServer {
691 seen: tokio::sync::mpsc::UnboundedSender<SeenUpdate>,
692 drop_next: Arc<AtomicBool>,
693 }
694
695 impl ProtocolHandler for RecordingServer {
696 async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
697 self.handle_connection(connection).await.map_err(|e| {
698 let boxed: Box<dyn std::error::Error + Send + Sync> = e.into();
699 AcceptError::from(AnyError::from(boxed))
700 })
701 }
702 }
703
704 impl RecordingServer {
705 async fn handle_connection(&self, connection: Connection) -> anyhow::Result<()> {
706 let mut decoder = Decoder::default();
707 let mut authed = false;
708 loop {
709 let Some(request) = read_request::<IrohServicesProtocol>(&connection).await? else {
710 return Ok(());
711 };
712 if self.drop_next.swap(false, Ordering::SeqCst) {
713 connection.close(500u32.into(), b"test restart");
716 return Ok(());
717 }
718 match request {
719 ServicesMessage::Auth(WithChannels { tx, .. }) => {
720 authed = true;
721 tx.send(()).await?;
722 }
723 ServicesMessage::PutMetrics(WithChannels { inner, tx, .. }) if authed => {
724 let has_schema = inner.update.schema.is_some();
725 decoder.import(inner.update);
726 let decoded_items = decoder.iter().count();
727 let _ = self.seen.send(SeenUpdate {
728 has_schema,
729 decoded_items,
730 });
731 tx.send(Ok(())).await?;
732 }
733 _ => {
734 connection.close(400u32.into(), b"Expected initial auth message");
735 return Ok(());
736 }
737 }
738 }
739 }
740 }
741
742 #[tokio::test]
747 async fn test_metrics_schema_resent_after_reconnect() {
748 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(2);
749 let server_ep = Endpoint::builder(presets::Minimal).bind().await.unwrap();
750 let client_ep = Endpoint::builder(presets::Minimal).bind().await.unwrap();
751
752 let (seen_tx, mut seen_rx) = tokio::sync::mpsc::unbounded_channel();
753 let drop_next = Arc::new(AtomicBool::new(false));
754 let server = RecordingServer {
755 seen: seen_tx,
756 drop_next: drop_next.clone(),
757 };
758 let router = Router::builder(server_ep.clone())
759 .accept(ALPN, server)
760 .spawn();
761
762 let shared_secret = SecretKey::from_bytes(&rng.random());
764 let cap = create_api_token_from_secret_key(
765 shared_secret,
766 client_ep.id(),
767 Duration::from_secs(3600),
768 Caps::for_shared_secret(),
769 )
770 .unwrap();
771
772 let client = Client::builder(&client_ep)
773 .disable_metrics_interval()
774 .remote(server_ep.addr())
775 .rcan(cap)
776 .unwrap()
777 .build()
778 .await
779 .unwrap();
780
781 client.push_metrics().await.unwrap();
783 let first = seen_rx.recv().await.unwrap();
784 assert!(first.has_schema);
785 assert!(first.decoded_items > 0);
786
787 client.push_metrics().await.unwrap();
790 let second = seen_rx.recv().await.unwrap();
791 assert!(!second.has_schema);
792 assert!(second.decoded_items > 0);
793
794 drop_next.store(true, Ordering::SeqCst);
796 assert!(client.push_metrics().await.is_err());
797
798 client.push_metrics().await.unwrap();
801 let third = seen_rx.recv().await.unwrap();
802 assert!(third.has_schema, "schema must be re-sent after a reconnect");
803 assert!(third.decoded_items > 0);
804
805 router.shutdown().await.unwrap();
806 client_ep.close().await;
807 }
808
809 #[tokio::test]
811 async fn test_fresh_encoder_resends_schema() {
812 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
813 let mut registry = Registry::default();
814 registry.register_all(endpoint.metrics());
815 let registry = Arc::new(RwLock::new(registry));
816
817 let mut encoder = Encoder::new(registry.clone());
818 let first = encoder.export();
819 assert!(first.schema.is_some());
820
821 let schemaless = encoder.export();
823 assert!(schemaless.schema.is_none());
824
825 let mut fresh_decoder = Decoder::default();
828 fresh_decoder.import(schemaless);
829 assert_eq!(fresh_decoder.iter().count(), 0);
830
831 let mut encoder = Encoder::new(registry);
834 let resent = encoder.export();
835 assert!(resent.schema.is_some());
836 let mut fresh_decoder = Decoder::default();
837 fresh_decoder.import(resent);
838 assert!(fresh_decoder.iter().count() > 0);
839 }
840
841 #[tokio::test]
842 #[temp_env_vars]
843 async fn test_api_key_from_env() {
844 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
846 let shared_secret = SecretKey::from_bytes(&rng.random());
847 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
848 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
849 unsafe {
850 std::env::set_var(API_SECRET_ENV_VAR_NAME, api_secret.to_string());
851 };
852
853 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
854
855 let builder = Client::builder(&endpoint).api_secret_from_env().unwrap();
856
857 let fake_endpoint_addr: EndpointAddr = fake_endpoint_id.into();
858 assert_eq!(builder.remote, Some(fake_endpoint_addr));
859
860 let cap = builder.cap.as_ref().expect("expected capability to be set");
863 assert_eq!(cap.capability(), &Caps::new([Cap::Client]));
864 assert_eq!(cap.audience(), &endpoint.id().as_verifying_key());
865 assert_eq!(cap.issuer(), &shared_secret.public().as_verifying_key());
866 }
867
868 #[tokio::test]
871 async fn test_no_metrics_interval() {
872 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(1);
873 let shared_secret = SecretKey::from_bytes(&rng.random());
874 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
875 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
876
877 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
878
879 let client = Client::builder(&endpoint)
880 .disable_metrics_interval()
881 .api_secret(api_secret)
882 .unwrap()
883 .build()
884 .await
885 .unwrap();
886
887 let err = client.push_metrics().await;
888 assert!(err.is_err());
889 }
890
891 #[tokio::test]
892 async fn test_name() {
893 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
894 let shared_secret = SecretKey::from_bytes(&rng.random());
895 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
896 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
897
898 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
899
900 let builder = Client::builder(&endpoint)
901 .name("my-node 👋")
902 .unwrap()
903 .api_secret(api_secret)
904 .unwrap();
905
906 assert_eq!(builder.name, Some("my-node 👋".to_string()));
907
908 let Err(err) = Client::builder(&endpoint).name("a") else {
909 panic!("name should fail for strings under 2 bytes");
910 };
911 assert!(matches!(
912 err.downcast_ref::<BuildError>(),
913 Some(BuildError::InvalidName(ValidateNameError::TooShort))
914 ));
915
916 let too_long_name = "👋".repeat(129);
917 let Err(err) = Client::builder(&endpoint).name(&too_long_name) else {
918 panic!("name should fail for strings over 128 bytes");
919 };
920 assert!(matches!(
921 err.downcast_ref::<BuildError>(),
922 Some(BuildError::InvalidName(ValidateNameError::TooLong))
923 ));
924 }
925}