Skip to main content

iroh_services/
client.rs

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::ApiSecret,
19    caps::Caps,
20    net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
21    protocol::{
22        ALPN, Auth, IrohServicesClient, NameEndpoint, Ping, Pong, PutMetrics,
23        PutNetworkDiagnostics, RemoteError,
24    },
25};
26
27/// Client is the main handle for interacting with iroh-services. It communicates with
28/// iroh-services entirely through an iroh endpoint, and is configured through a builder.
29/// Client requires either an Ssh Key or [`ApiSecret`]
30///
31/// ```no_run
32/// use iroh::{Endpoint, endpoint::presets};
33/// use iroh_services::Client;
34///
35/// async fn build_client() -> anyhow::Result<()> {
36///     let endpoint = Endpoint::bind(presets::N0).await?;
37///
38///     // needs IROH_SERVICES_API_SECRET set to an environment variable
39///     // client will now push endpoint metrics to iroh-services.
40///     let client = Client::builder(&endpoint)
41///         .api_secret_from_str("MY_API_SECRET")?
42///         .build()
43///         .await;
44///
45///     Ok(())
46/// }
47/// ```
48///
49/// [`ApiSecret`]: crate::api_secret::ApiSecret
50#[derive(Debug, Clone)]
51pub struct Client {
52    // owned clone of the endpoint for diagnostics, and for connection restarts on actor close
53    #[allow(dead_code)]
54    endpoint: Endpoint,
55    message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
56    _actor_task: Arc<AbortOnDropHandle<()>>,
57}
58
59/// ClientBuilder provides configures and builds a iroh-services client, typically
60/// created with [`Client::builder`]
61pub 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
72const DEFAULT_CAP_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 1 month
73pub const API_SECRET_ENV_VAR_NAME: &str = "IROH_SERVICES_API_SECRET";
74
75impl ClientBuilder {
76    pub fn new(endpoint: &Endpoint) -> Self {
77        let mut registry = Registry::default();
78        registry.register_all(endpoint.metrics());
79
80        Self {
81            cap: None,
82            cap_expiry: DEFAULT_CAP_EXPIRY,
83            endpoint: endpoint.clone(),
84            name: None,
85            metrics_interval: Some(Duration::from_secs(60)),
86            remote: None,
87            registry,
88        }
89    }
90
91    /// Register a metrics group to forward to iroh-services
92    ///
93    /// The default registered metrics uses only the endpoint
94    pub fn register_metrics_group(mut self, metrics_group: Arc<dyn MetricsGroup>) -> Self {
95        self.registry.register(metrics_group);
96        self
97    }
98
99    /// Set the metrics collection interval
100    ///
101    /// Defaults to enabled, every 60 seconds.
102    pub fn metrics_interval(mut self, interval: Duration) -> Self {
103        self.metrics_interval = Some(interval);
104        self
105    }
106
107    /// Disable metrics collection.
108    pub fn disable_metrics_interval(mut self) -> Self {
109        self.metrics_interval = None;
110        self
111    }
112
113    /// Set an optional human-readable name for the endpoint the client is
114    /// constructed with, making metrics from this endpoint easier to identify.
115    /// This is often used for associating with other services in your app,
116    /// like a database user id, machine name, permanent username, etc.
117    ///
118    /// When this builder method is called, the provided name is sent after the
119    /// client initially authenticates the endpoint server-side.
120    /// Errors will not interrupt client construction, instead producing a
121    /// warn-level log. For explicit error handling, use [`Client::set_name`].
122    ///
123    /// names can be any UTF-8 string, with a min length of 2 bytes, and
124    /// maximum length of 128 bytes. **name uniqueness is not enforced
125    /// server-side**, which means using the same name for different endpoints
126    /// will not produce an error
127    pub fn name(mut self, name: impl Into<String>) -> Result<Self> {
128        let name = name.into();
129        validate_name(&name).map_err(BuildError::InvalidName)?;
130        self.name = Some(name);
131        Ok(self)
132    }
133
134    /// Check IROH_SERVICES_API_SECRET environment variable for a valid API secret
135    pub fn api_secret_from_env(self) -> Result<Self> {
136        let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
137        self.api_secret(ticket)
138    }
139
140    /// set client API secret from an encoded string
141    pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
142        let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
143        self.api_secret(key)
144    }
145
146    /// Use a shared secret & remote iroh-services endpoint ID contained within a ticket
147    /// to construct a iroh-services client. The resulting client will have "Client"
148    /// capabilities.
149    ///
150    /// API secrets include remote details within them, and will set both the
151    /// remote and rcan values on the builder
152    pub fn api_secret(mut self, ticket: ApiSecret) -> Result<Self> {
153        let local_id = self.endpoint.id();
154        let rcan = crate::caps::create_api_token_from_secret_key(
155            ticket.secret,
156            local_id,
157            self.cap_expiry,
158            Caps::for_shared_secret(),
159        )?;
160
161        self.remote = Some(ticket.remote);
162        self.rcan(rcan)
163    }
164
165    /// Loads the private ssh key from the given path, and creates the needed capability.
166    #[cfg(not(target_arch = "wasm32"))]
167    pub async fn ssh_key_from_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self> {
168        let file_content = tokio::fs::read_to_string(path).await?;
169        let private_key = ssh_key::PrivateKey::from_openssh(&file_content)?;
170
171        self.ssh_key(&private_key)
172    }
173
174    /// Creates the capability from the provided private ssh key.
175    #[cfg(not(target_arch = "wasm32"))]
176    pub fn ssh_key(mut self, key: &ssh_key::PrivateKey) -> Result<Self> {
177        let local_id = self.endpoint.id();
178        let rcan = crate::caps::create_api_token_from_ssh_key(
179            key,
180            local_id,
181            self.cap_expiry,
182            Caps::all(),
183        )?;
184        self.cap.replace(rcan);
185
186        Ok(self)
187    }
188
189    /// Sets the rcan directly.
190    pub fn rcan(mut self, cap: Rcan<Caps>) -> Result<Self> {
191        ensure!(
192            EndpointId::from_verifying_key(*cap.audience()) == self.endpoint.id(),
193            "invalid audience"
194        );
195        self.cap.replace(cap);
196        Ok(self)
197    }
198
199    /// Sets the remote to dial, must be provided either directly by calling
200    /// this method, or through calling the api_secret builder methods.
201    pub fn remote(mut self, remote: impl Into<EndpointAddr>) -> Self {
202        self.remote = Some(remote.into());
203        self
204    }
205
206    /// Create a new client, connected to the provide service node
207    #[must_use = "dropping the client will silently cancel all client tasks"]
208    pub async fn build(self) -> Result<Client, BuildError> {
209        debug!("starting iroh-services client");
210        let remote = self.remote.ok_or(BuildError::MissingRemote)?;
211        let capabilities = self.cap.ok_or(BuildError::MissingCapability)?;
212
213        let conn = IrohLazyRemoteConnection::new(self.endpoint.clone(), remote, ALPN.to_vec());
214        let irpc_client = IrohServicesClient::boxed(conn);
215
216        let (tx, rx) = tokio::sync::mpsc::channel(1);
217        let actor_task = AbortOnDropHandle::new(n0_future::task::spawn(
218            ClientActor {
219                capabilities,
220                client: irpc_client,
221                name: self.name.clone(),
222                session_id: Uuid::new_v4(),
223                authorized: false,
224            }
225            .run(self.name, self.registry, 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
270/// Minimum length in bytes for an endpoint name.
271pub const CLIENT_NAME_MIN_LENGTH: usize = 2;
272/// Maximum length in bytes for an endpoint name.
273pub const CLIENT_NAME_MAX_LENGTH: usize = 128;
274
275/// Error returned when an endpoint name fails validation.
276#[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    /// Read the current endpoint name from the local client.
312    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    /// Name the active endpoint cloud-side.
324    ///
325    /// names can be any UTF-8 string, with a min length of 2 bytes, and
326    /// maximum length of 128 bytes. **name uniqueness is not enforced.**
327    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    /// Pings the remote node.
332    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    /// immediately send a single dump of metrics to iroh-services. It's not necessary
345    /// to call this function if you're using a non-zero metrics interval,
346    /// which will automatically propagate metrics on the set interval for you
347    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    /// Grant capabilities to a remote endpoint. Creates a signed RCAN token
360    /// and sends it to iroh-services for storage. The remote can then use this token
361    /// when dialing back to authorize its requests.
362    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    /// run local network status diagnostics, optionally uploading the results
389    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    // GrantCap is used by the `client_host` feature flag
418    #[allow(dead_code)]
419    GrantCap {
420        // boxed to avoid large enum variants
421        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}
444
445impl ClientActor {
446    async fn run(
447        mut self,
448        initial_name: Option<String>,
449        registry: Registry,
450        interval: Option<Duration>,
451        mut inbox: tokio::sync::mpsc::Receiver<ClientActorMessage>,
452    ) {
453        let registry = Arc::new(RwLock::new(registry));
454        let mut encoder = Encoder::new(registry);
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(&mut encoder).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(&mut encoder).await {
519                        debug!("failed to push metrics: {:#?}", err);
520                        self.authorized = false;
521                    }
522                },
523            }
524        }
525    }
526
527    // sends an authorization request to the server
528    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        Ok(())
542    }
543
544    async fn send_ping(&mut self) -> Result<Pong, RemoteError> {
545        trace!("client actor send ping");
546        self.auth().await?;
547
548        let req = rand::random();
549        self.client
550            .rpc(Ping { req_id: req })
551            .await
552            .inspect_err(|e| warn!("rpc ping error: {e}"))
553            .map_err(|_| RemoteError::InternalServerError)
554    }
555
556    async fn send_name_endpoint(&mut self, name: String) -> Result<(), RemoteError> {
557        trace!("client sending name endpoint request");
558        self.auth().await?;
559
560        self.client
561            .rpc(NameEndpoint { name: name.clone() })
562            .await
563            .inspect_err(|e| debug!("name endpoint error: {e}"))
564            .map_err(|_| RemoteError::InternalServerError)??;
565        self.name = Some(name);
566        Ok(())
567    }
568
569    async fn send_metrics(&mut self, encoder: &mut Encoder) -> Result<(), RemoteError> {
570        trace!("client actor send metrics");
571        self.auth().await?;
572
573        let update = encoder.export();
574        // let delta = update_delta(&self.latest_ackd_update, &update);
575        let req = PutMetrics {
576            session_id: self.session_id,
577            update,
578        };
579
580        self.client
581            .rpc(req)
582            .await
583            .map_err(|_| RemoteError::InternalServerError)??;
584
585        Ok(())
586    }
587
588    async fn grant_cap(&mut self, cap: Rcan<Caps>) -> Result<(), Error> {
589        trace!("client actor grant capability");
590        self.auth().await?;
591
592        self.client
593            .rpc(crate::protocol::GrantCap { cap })
594            .await
595            .map_err(|_| RemoteError::InternalServerError)??;
596
597        Ok(())
598    }
599
600    async fn put_network_diagnostics(
601        &mut self,
602        report: crate::net_diagnostics::DiagnosticsReport,
603    ) -> Result<(), Error> {
604        trace!("client actor publish network diagnostics");
605        self.auth().await?;
606
607        let req = PutNetworkDiagnostics { report };
608
609        self.client
610            .rpc(req)
611            .await
612            .map_err(|_| RemoteError::InternalServerError)??;
613
614        Ok(())
615    }
616}
617
618async fn set_name_inner(
619    message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
620    name: String,
621) -> Result<(), Error> {
622    validate_name(&name)?;
623    debug!(name_len = name.len(), "calling set name");
624    let (tx, rx) = oneshot::channel();
625    message_channel
626        .send(ClientActorMessage::NameEndpoint { name, done: tx })
627        .await
628        .map_err(|_| Error::Other(anyhow!("sending name endpoint request")))?;
629    rx.await
630        .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
631        .map_err(Error::Remote)
632}
633
634#[cfg(test)]
635mod tests {
636    use iroh::{Endpoint, EndpointAddr, SecretKey};
637    use temp_env_vars::temp_env_vars;
638
639    use crate::{
640        Client,
641        api_secret::ApiSecret,
642        caps::{Cap, Caps},
643        client::{API_SECRET_ENV_VAR_NAME, BuildError, ValidateNameError},
644    };
645
646    #[tokio::test]
647    #[temp_env_vars]
648    async fn test_api_key_from_env() {
649        use rand::SeedableRng;
650        // construct
651        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0);
652        let shared_secret = SecretKey::generate(&mut rng);
653        let fake_endpoint_id = SecretKey::generate(&mut rng).public();
654        let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
655        unsafe {
656            std::env::set_var(API_SECRET_ENV_VAR_NAME, api_secret.to_string());
657        };
658
659        let endpoint = Endpoint::empty_builder().bind().await.unwrap();
660
661        let builder = Client::builder(&endpoint).api_secret_from_env().unwrap();
662
663        let fake_endpoint_addr: EndpointAddr = fake_endpoint_id.into();
664        assert_eq!(builder.remote, Some(fake_endpoint_addr));
665
666        // Compare capability fields individually to avoid flaky timestamp
667        // mismatches between the builder's rcan and a freshly-created one.
668        let cap = builder.cap.as_ref().expect("expected capability to be set");
669        assert_eq!(cap.capability(), &Caps::new([Cap::Client]));
670        assert_eq!(cap.audience(), &endpoint.id().as_verifying_key());
671        assert_eq!(cap.issuer(), &shared_secret.public().as_verifying_key());
672    }
673
674    /// Assert that disabling metrics interval can manually send metrics without
675    /// panicking. Metrics sending itself is expected to fail.
676    #[tokio::test]
677    async fn test_no_metrics_interval() {
678        use rand::SeedableRng;
679        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1);
680        let shared_secret = SecretKey::generate(&mut rng);
681        let fake_endpoint_id = SecretKey::generate(&mut rng).public();
682        let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
683
684        let endpoint = Endpoint::empty_builder().bind().await.unwrap();
685
686        let client = Client::builder(&endpoint)
687            .disable_metrics_interval()
688            .api_secret(api_secret)
689            .unwrap()
690            .build()
691            .await
692            .unwrap();
693
694        let err = client.push_metrics().await;
695        assert!(err.is_err());
696    }
697
698    #[tokio::test]
699    async fn test_name() {
700        use rand::SeedableRng;
701        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0);
702        let shared_secret = SecretKey::generate(&mut rng);
703        let fake_endpoint_id = SecretKey::generate(&mut rng).public();
704        let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
705
706        let endpoint = Endpoint::empty_builder().bind().await.unwrap();
707
708        let builder = Client::builder(&endpoint)
709            .name("my-node 👋")
710            .unwrap()
711            .api_secret(api_secret)
712            .unwrap();
713
714        assert_eq!(builder.name, Some("my-node 👋".to_string()));
715
716        let Err(err) = Client::builder(&endpoint).name("a") else {
717            panic!("name should fail for strings under 2 bytes");
718        };
719        assert!(matches!(
720            err.downcast_ref::<BuildError>(),
721            Some(BuildError::InvalidName(ValidateNameError::TooShort))
722        ));
723
724        let too_long_name = "👋".repeat(129);
725        let Err(err) = Client::builder(&endpoint).name(&too_long_name) else {
726            panic!("name should fail for strings over 128 bytes");
727        };
728        assert!(matches!(
729            err.downcast_ref::<BuildError>(),
730            Some(BuildError::InvalidName(ValidateNameError::TooLong))
731        ));
732    }
733}