1use std::{
2 collections::BTreeMap,
3 fmt::Display,
4 str::FromStr,
5 sync::{Arc, RwLock},
6};
7
8use anyhow::Result;
9use iroh::{
10 Endpoint, EndpointAddr, EndpointId,
11 endpoint::{ConnectError, Connection},
12};
13use iroh_metrics::{MetricsGroup, Registry, encoding::Encoder};
14use iroh_services_proto::{
15 ATTRIBUTE_VALUE_MAX_LENGTH, ATTRIBUTES_MAX_COUNT, Auth, GrantCap, IrohServicesClient,
16 IrohServicesProtocol, NameEndpoint, Ping, Pong as ProtoPong, PutMetrics, PutNetworkDiagnostics,
17 RemoteError as ProtoRemoteError, ServicesMessage, SetAttributes, SetGroup,
18 caps::Caps as ProtoCaps,
19};
20use irpc::{Channels, RpcMessage, WithChannels, channel::none::NoReceiver};
21use irpc_iroh::IrohRemoteConnection;
22use n0_error::StackResultExt;
23use n0_future::{
24 task::{self, AbortOnDropHandle},
25 time::{self, Duration},
26};
27use rcan::Rcan;
28use serde::{Deserialize, Serialize};
29use tokio::sync::oneshot;
30use tokio_util::sync::CancellationToken;
31use tracing::{debug, trace, warn};
32use uuid::Uuid;
33
34use crate::{
35 ALPN,
36 api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
37 caps::{Caps, DEFAULT_CAP_EXPIRY},
38 net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
39};
40
41#[derive(Debug, Clone)]
65pub struct Client {
66 endpoint: Endpoint,
68 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
69 shutdown: CancellationToken,
71 _actor_task: Arc<AbortOnDropHandle<()>>,
72}
73
74pub struct ClientBuilder {
77 cap_expiry: Duration,
78 cap: Option<Rcan<ProtoCaps>>,
79 endpoint: Endpoint,
80 name: Option<String>,
81 group: Option<String>,
82 attributes: Option<BTreeMap<String, String>>,
83 metrics_interval: Option<Duration>,
84 remote: Option<EndpointAddr>,
85 registry: Registry,
86}
87
88#[derive(Debug, Serialize, Deserialize)]
90pub struct Pong {
91 pub req_id: [u8; 16],
92}
93
94#[derive(Clone, Serialize, Deserialize, thiserror::Error, Debug)]
96#[non_exhaustive]
97pub enum RemoteError {
98 #[error("Missing capability: {}", _0.0.to_strings().join(", "))]
99 MissingCapability(#[serde(with = "missing_capability_serde")] Caps),
100 #[error("Unauthorized: {0}")]
101 AuthError(String),
102 #[error("Internal server error")]
103 InternalServerError,
104 #[error("Invalid input: {0}")]
105 InvalidInput(String),
106 #[error("Rate limit exceeded")]
107 RateLimited,
108}
109
110mod missing_capability_serde {
111 use serde::{Deserialize, Serialize};
112
113 use super::{Caps, ProtoCaps};
114
115 pub(super) fn serialize<S>(caps: &Caps, serializer: S) -> Result<S::Ok, S::Error>
116 where
117 S: serde::Serializer,
118 {
119 caps.0.serialize(serializer)
120 }
121
122 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Caps, D::Error>
123 where
124 D: serde::Deserializer<'de>,
125 {
126 ProtoCaps::deserialize(deserializer).map(Caps)
127 }
128}
129
130impl RemoteError {
131 fn from_proto(error: ProtoRemoteError) -> Self {
132 match error {
133 ProtoRemoteError::MissingCapability(caps) => Self::MissingCapability(Caps(caps)),
134 ProtoRemoteError::AuthError(error) => Self::AuthError(error),
135 ProtoRemoteError::InternalServerError => Self::InternalServerError,
136 ProtoRemoteError::InvalidInput(error) => Self::InvalidInput(error),
137 ProtoRemoteError::RateLimited => Self::RateLimited,
138 _ => Self::InternalServerError,
139 }
140 }
141}
142
143impl ClientBuilder {
144 pub fn new(endpoint: &Endpoint) -> Self {
145 let mut registry = Registry::default();
146 registry.register_all(endpoint.metrics());
147
148 Self {
149 cap: None,
150 cap_expiry: DEFAULT_CAP_EXPIRY,
151 endpoint: endpoint.clone(),
152 name: None,
153 group: None,
154 attributes: None,
155 metrics_interval: Some(Duration::from_secs(60)),
156 remote: None,
157 registry,
158 }
159 }
160
161 pub fn register_metrics_group(mut self, metrics_group: Arc<dyn MetricsGroup>) -> Self {
165 self.registry.register(metrics_group);
166 self
167 }
168
169 pub fn metrics_interval(mut self, interval: Duration) -> Self {
173 self.metrics_interval = Some(interval);
174 self
175 }
176
177 pub fn disable_metrics_interval(mut self) -> Self {
179 self.metrics_interval = None;
180 self
181 }
182
183 pub fn name(mut self, name: impl Into<String>) -> Result<Self> {
195 let name = name.into();
196 validate_name(&name).map_err(BuildError::InvalidName)?;
197 self.name = Some(name);
198 Ok(self)
199 }
200
201 pub fn group(mut self, group: impl Into<String>) -> Result<Self> {
210 let group = group.into();
211 validate_name(&group).map_err(BuildError::InvalidGroup)?;
212 self.group = Some(group);
213 Ok(self)
214 }
215
216 pub fn attributes<I, K, V>(mut self, attrs: I) -> Result<Self>
234 where
235 I: IntoIterator<Item = (K, V)>,
236 K: Into<String>,
237 V: Into<String>,
238 {
239 let collected: BTreeMap<String, String> = attrs
240 .into_iter()
241 .map(|(k, v)| (k.into(), v.into()))
242 .collect();
243 validate_attributes(&collected).map_err(BuildError::InvalidAttributes)?;
244 self.attributes = Some(collected);
245 Ok(self)
246 }
247
248 pub fn api_secret_from_env(self) -> Result<Self> {
250 let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
251 self.api_secret(ticket)
252 }
253
254 pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
256 let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
257 self.api_secret(key)
258 }
259
260 pub fn api_secret(mut self, ticket: ApiSecret) -> Result<Self> {
267 let local_id = self.endpoint.id();
268 let token = crate::caps::create_api_token_from_secret_key(
269 ticket.secret,
270 local_id,
271 self.cap_expiry,
272 Caps::client(),
273 )?;
274
275 self.remote = Some(ticket.remote);
276 self.cap.replace(token.into_rcan());
277 Ok(self)
278 }
279
280 #[cfg(not(wasm_browser))]
284 pub async fn ssh_key_from_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self> {
285 let file_content = tokio::fs::read_to_string(path).await?;
286 self.ssh_key(&file_content)
287 }
288
289 #[cfg(not(wasm_browser))]
291 pub fn ssh_key(mut self, pem: &str) -> Result<Self> {
292 let local_id = self.endpoint.id();
293 let token = crate::caps::create_api_token_from_openssh_pem(
294 pem,
295 local_id,
296 self.cap_expiry,
297 Caps(ProtoCaps::all()),
298 )?;
299 self.cap.replace(token.into_rcan());
300
301 Ok(self)
302 }
303
304 pub fn remote(mut self, remote: impl Into<EndpointAddr>) -> Self {
307 self.remote = Some(remote.into());
308 self
309 }
310
311 #[must_use = "dropping the client will silently cancel all client tasks"]
313 pub async fn build(self) -> Result<Client, BuildError> {
314 debug!("starting iroh-services client");
315 let remote = self.remote.ok_or(BuildError::MissingRemote)?;
316 let capabilities = self.cap.ok_or(BuildError::MissingCapability)?;
317
318 let registry = Arc::new(RwLock::new(self.registry));
319 let (tx, rx) = tokio::sync::mpsc::channel(1);
320 let shutdown = CancellationToken::new();
321 let actor_task = AbortOnDropHandle::new(task::spawn(
322 ClientActor {
323 capabilities,
324 endpoint: self.endpoint.clone(),
325 remote,
326 client: None,
327 name: self.name.clone(),
328 group: self.group.clone(),
329 attributes: self.attributes.clone().unwrap_or_default(),
330 session_id: Uuid::new_v4(),
331 encoder: Encoder::new(registry.clone()),
332 registry,
333 }
334 .run(self.metrics_interval, rx, shutdown.clone()),
335 ));
336
337 Ok(Client {
338 endpoint: self.endpoint,
339 message_channel: tx,
340 shutdown,
341 _actor_task: Arc::new(actor_task),
342 })
343 }
344}
345
346#[derive(thiserror::Error, Debug)]
347#[non_exhaustive]
348pub enum BuildError {
349 #[error("Missing remote endpoint to dial")]
350 MissingRemote,
351 #[error("Missing capability")]
352 MissingCapability,
353 #[error("Unauthorized")]
354 Unauthorized,
355 #[error("Remote error: {0}")]
356 Remote(#[from] RemoteError),
357 #[error("Rpc connection error: {0}")]
358 Rpc(irpc::Error),
359 #[error("Connection error: {0}")]
360 Connect(ConnectError),
361 #[error("Invalid endpoint name: {0}")]
362 InvalidName(#[from] ValidateNameError),
363 #[error("Invalid endpoint group: {0}")]
364 InvalidGroup(ValidateNameError),
365 #[error("Invalid endpoint attributes: {0}")]
366 InvalidAttributes(#[from] ValidateAttributesError),
367}
368
369impl From<irpc::Error> for BuildError {
370 fn from(value: irpc::Error) -> Self {
371 match value {
372 irpc::Error::Request {
373 source:
374 irpc::RequestError::Connection {
375 source: iroh::endpoint::ConnectionError::ApplicationClosed(frame),
376 ..
377 },
378 ..
379 } if frame.error_code == 401u32.into() => Self::Unauthorized,
380 value => Self::Rpc(value),
381 }
382 }
383}
384
385const SHUTDOWN_GRACE: Duration = Duration::from_secs(3);
394
395const SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
401
402pub const CLIENT_NAME_MIN_LENGTH: usize = 2;
404pub const CLIENT_NAME_MAX_LENGTH: usize = 128;
406
407#[derive(Debug, thiserror::Error)]
409pub enum ValidateNameError {
410 #[error("Name is too long (must be no more than {CLIENT_NAME_MAX_LENGTH} bytes).")]
411 TooLong,
412 #[error("Name is too short (must be at least {CLIENT_NAME_MIN_LENGTH} bytes).")]
413 TooShort,
414}
415
416fn validate_name(name: &str) -> Result<(), ValidateNameError> {
417 if name.len() < CLIENT_NAME_MIN_LENGTH {
418 Err(ValidateNameError::TooShort)
419 } else if name.len() > CLIENT_NAME_MAX_LENGTH {
420 Err(ValidateNameError::TooLong)
421 } else {
422 Ok(())
423 }
424}
425
426#[derive(Debug, thiserror::Error)]
428pub enum ValidateAttributesError {
429 #[error("Too many attributes (must be no more than {ATTRIBUTES_MAX_COUNT}).")]
430 TooManyEntries,
431 #[error("Invalid attribute key: {0}")]
432 InvalidKey(#[from] ValidateNameError),
433 #[error("Attribute value too long (must be no more than {ATTRIBUTE_VALUE_MAX_LENGTH} bytes).")]
434 ValueTooLong,
435}
436
437fn validate_attributes(attrs: &BTreeMap<String, String>) -> Result<(), ValidateAttributesError> {
438 if attrs.len() > ATTRIBUTES_MAX_COUNT {
439 return Err(ValidateAttributesError::TooManyEntries);
440 }
441 for (k, v) in attrs {
442 validate_name(k)?;
443 if v.len() > ATTRIBUTE_VALUE_MAX_LENGTH {
444 return Err(ValidateAttributesError::ValueTooLong);
445 }
446 }
447 Ok(())
448}
449
450#[derive(thiserror::Error, Debug)]
451#[non_exhaustive]
452pub enum Error {
453 #[error("Invalid endpoint name: {0}")]
454 InvalidName(#[from] ValidateNameError),
455 #[error("Invalid endpoint group: {0}")]
456 InvalidGroup(ValidateNameError),
457 #[error("Invalid endpoint attributes: {0}")]
458 InvalidAttributes(#[from] ValidateAttributesError),
459 #[error("Remote error: {0}")]
460 Remote(#[from] RemoteError),
461 #[error("Connection error: {0}")]
462 Connect(#[from] ConnectError),
463 #[error("Rpc error: {0}")]
464 Rpc(#[from] irpc::Error),
465 #[error(transparent)]
466 Other(#[from] anyhow::Error),
467 #[error("Local client actor is stopped, cannot send requests")]
468 ActorStopped,
469}
470
471impl From<tokio::sync::mpsc::error::SendError<ClientActorMessage>> for Error {
472 fn from(_value: tokio::sync::mpsc::error::SendError<ClientActorMessage>) -> Self {
473 Error::ActorStopped
474 }
475}
476
477impl From<tokio::sync::oneshot::error::RecvError> for Error {
478 fn from(_value: tokio::sync::oneshot::error::RecvError) -> Self {
479 Error::ActorStopped
480 }
481}
482
483impl Client {
484 pub fn builder(endpoint: &Endpoint) -> ClientBuilder {
485 ClientBuilder::new(endpoint)
486 }
487
488 pub async fn name(&self) -> Result<Option<String>, Error> {
490 let (tx, rx) = oneshot::channel();
491 self.message_channel
492 .send(ClientActorMessage::ReadName { done: tx })
493 .await?;
494 rx.await.map_err(Into::into)
495 }
496
497 pub async fn group(&self) -> Result<Option<String>, Error> {
499 let (tx, rx) = oneshot::channel();
500 self.message_channel
501 .send(ClientActorMessage::ReadGroup { done: tx })
502 .await?;
503 rx.await.map_err(Into::into)
504 }
505
506 pub async fn set_name(&self, name: impl Into<String>) -> Result<(), Error> {
511 let name = name.into();
512 validate_name(&name)?;
513 debug!(name_len = name.len(), "calling set name");
514 let (tx, rx) = oneshot::channel();
515 self.message_channel
516 .send(ClientActorMessage::NameEndpoint { name, done: tx })
517 .await?;
518 rx.await?
519 }
520
521 pub async fn set_group(&self, group: impl Into<String>) -> Result<(), Error> {
525 let group: String = group.into();
526 validate_name(&group).map_err(Error::InvalidGroup)?;
527 debug!(%group, "calling set group");
528 let (tx, rx) = oneshot::channel();
529 self.message_channel
530 .send(ClientActorMessage::SetGroup { group, done: tx })
531 .await?;
532 rx.await?
533 }
534
535 pub async fn set_attributes<I, K, V>(&self, attrs: I) -> Result<(), Error>
553 where
554 I: IntoIterator<Item = (K, V)>,
555 K: Into<String>,
556 V: Into<String>,
557 {
558 let collected: BTreeMap<String, String> = attrs
559 .into_iter()
560 .map(|(k, v)| (k.into(), v.into()))
561 .collect();
562 validate_attributes(&collected)?;
563 debug!(attr_count = collected.len(), "calling set attributes");
564 let (tx, rx) = oneshot::channel();
565 self.message_channel
566 .send(ClientActorMessage::SetAttributes {
567 attributes: collected,
568 done: tx,
569 })
570 .await?;
571 rx.await?
572 }
573
574 pub async fn set_attribute(
581 &self,
582 key: impl Into<String>,
583 value: impl Into<String>,
584 ) -> Result<(), Error> {
585 let (tx, rx) = oneshot::channel();
590 self.message_channel
591 .send(ClientActorMessage::SetAttribute {
592 key: key.into(),
593 value: value.into(),
594 done: tx,
595 })
596 .await?;
597 rx.await?
598 }
599
600 pub async fn ping(&self) -> Result<Pong, Error> {
602 let (tx, rx) = oneshot::channel();
603 self.message_channel
604 .send(ClientActorMessage::Ping { done: tx })
605 .await?;
606 rx.await?
607 }
608
609 pub async fn push_metrics(&self) -> Result<(), Error> {
613 let (tx, rx) = oneshot::channel();
614 self.message_channel
615 .send(ClientActorMessage::SendMetrics { done: tx })
616 .await?;
617 rx.await?
618 }
619
620 pub async fn shutdown(&self) {
630 self.shutdown.cancel();
631 self.message_channel.closed().await;
633 }
634
635 pub async fn grant_capability(&self, remote_id: EndpointId, caps: Caps) -> Result<(), Error> {
639 let cap = crate::caps::create_grant_token(
640 self.endpoint.secret_key().clone(),
641 remote_id,
642 DEFAULT_CAP_EXPIRY,
643 caps,
644 )
645 .map_err(Error::Other)?
646 .into_rcan();
647
648 let (tx, rx) = oneshot::channel();
649 self.message_channel
650 .send(ClientActorMessage::GrantCap {
651 cap: Box::new(cap),
652 done: tx,
653 })
654 .await?;
655 rx.await?
656 }
657
658 pub async fn net_diagnostics(&self, send: bool) -> Result<DiagnosticsReport, Error> {
660 let report = run_diagnostics(&self.endpoint).await?;
661 if send {
662 let (tx, rx) = oneshot::channel();
663 self.message_channel
664 .send(ClientActorMessage::PutNetworkDiagnostics {
665 done: tx,
666 report: Box::new(report.clone()),
667 })
668 .await?;
669 rx.await??;
670 }
671
672 Ok(report)
673 }
674}
675
676enum ClientActorMessage {
677 SendMetrics {
678 done: oneshot::Sender<Result<(), Error>>,
679 },
680 Ping {
681 done: oneshot::Sender<Result<Pong, Error>>,
682 },
683 GrantCap {
684 cap: Box<Rcan<ProtoCaps>>,
686 done: oneshot::Sender<Result<(), Error>>,
687 },
688 PutNetworkDiagnostics {
689 report: Box<DiagnosticsReport>,
690 done: oneshot::Sender<Result<(), Error>>,
691 },
692 ReadName {
693 done: oneshot::Sender<Option<String>>,
694 },
695 ReadGroup {
696 done: oneshot::Sender<Option<String>>,
697 },
698 NameEndpoint {
699 name: String,
700 done: oneshot::Sender<Result<(), Error>>,
701 },
702 SetGroup {
703 group: String,
704 done: oneshot::Sender<Result<(), Error>>,
705 },
706 SetAttributes {
707 attributes: BTreeMap<String, String>,
708 done: oneshot::Sender<Result<(), Error>>,
709 },
710 SetAttribute {
711 key: String,
712 value: String,
713 done: oneshot::Sender<Result<(), Error>>,
717 },
718}
719
720fn is_connection_lost(err: &irpc::Error) -> bool {
728 !matches!(
729 err,
730 irpc::Error::Send {
731 source: irpc::channel::SendError::MaxMessageSizeExceeded { .. },
732 ..
733 }
734 )
735}
736
737struct RpcClient {
744 connection: Connection,
746 irpc: IrohServicesClient,
747}
748
749impl RpcClient {
750 async fn connect(
752 endpoint: &Endpoint,
753 remote: EndpointAddr,
754 caps: Rcan<ProtoCaps>,
755 ) -> Result<Self, Error> {
756 trace!("client connecting and authorizing");
757 let connection = endpoint
758 .connect(remote, ALPN)
759 .await
760 .inspect_err(|err| debug!("connect failed: {err:?}"))?;
761 let irpc = IrohServicesClient::boxed(IrohRemoteConnection::new(connection.clone()));
762 irpc.rpc(Auth { caps })
763 .await
764 .inspect_err(|err| debug!("authorization failed: {err:?}"))
765 .map_err(|err| Error::Remote(RemoteError::AuthError(err.to_string())))?;
766 Ok(Self { connection, irpc })
767 }
768}
769
770struct ClientActor {
771 capabilities: Rcan<ProtoCaps>,
772 endpoint: Endpoint,
773 remote: EndpointAddr,
774 client: Option<RpcClient>,
783 name: Option<String>,
784 group: Option<String>,
785 attributes: BTreeMap<String, String>,
786 session_id: Uuid,
787 encoder: Encoder,
788 registry: Arc<RwLock<Registry>>,
790}
791
792impl ClientActor {
793 async fn run(
800 mut self,
801 interval: Option<Duration>,
802 mut inbox: tokio::sync::mpsc::Receiver<ClientActorMessage>,
803 shutdown: CancellationToken,
804 ) {
805 let metrics_enabled = interval.is_some();
806 let shutdown_and_grace_period_expired = async {
807 shutdown.cancelled().await;
808 time::sleep(SHUTDOWN_GRACE).await;
809 };
810
811 let clean_shutdown = tokio::select! {
812 () = self.run_inner(interval, &mut inbox, &shutdown) => true,
813 () = shutdown_and_grace_period_expired => {
814 debug!("shutdown grace elapsed, dropping the request in flight");
815 false
816 }
817 };
818
819 if clean_shutdown && metrics_enabled && self.is_connected() {
823 match time::timeout(SHUTDOWN_FLUSH_TIMEOUT, self.send_metrics()).await {
824 Ok(Ok(())) => trace!("pushed final metrics on shutdown"),
825 Ok(Err(err)) => debug!(%err, "failed to push final metrics on shutdown"),
826 Err(_) => debug!("final metrics push on shutdown timed out"),
827 }
828 }
829 debug!("client actor shut down");
830 }
831
832 async fn run_inner(
833 &mut self,
834 interval: Option<Duration>,
835 inbox: &mut tokio::sync::mpsc::Receiver<ClientActorMessage>,
836 shutdown: &CancellationToken,
837 ) {
838 let mut metrics_timer = interval.map(|interval| time::interval(interval));
839 trace!("starting client actor");
840
841 if let Some(name) = self.name.clone()
844 && let Err(err) = self.send_name_endpoint(name).await
845 {
846 warn!(err = %err, "failed setting endpoint name on startup");
847 }
848
849 if let Some(group) = self.group.clone()
850 && let Err(err) = self.send_set_group(group).await
851 {
852 warn!(err = %err, "failed setting endpoint group on startup");
853 }
854
855 if !self.attributes.is_empty()
856 && let Err(err) = self.send_set_attributes(self.attributes.clone()).await
857 {
858 warn!(err = %err, "failed setting endpoint attributes on startup");
859 }
860
861 loop {
862 trace!("client actor tick");
863 tokio::select! {
864 biased;
865 () = shutdown.cancelled() => {
868 trace!("client actor observed shutdown between requests");
869 break;
870 }
871 Some(msg) = inbox.recv() => {
872 match msg {
873 ClientActorMessage::Ping { done } => {
874 let res = self.send_ping().await;
875 done.send(res).ok();
876 },
877 ClientActorMessage::SendMetrics { done } => {
878 trace!("sending metrics manually triggered");
879 let res = self.send_metrics().await;
880 done.send(res).ok();
881 }
882 ClientActorMessage::GrantCap { cap, done } => {
883 let res = self.grant_cap(*cap).await;
884 done.send(res).ok();
885 }
886 ClientActorMessage::ReadName { done } => {
887 done.send(self.name.clone()).ok();
888 }
889 ClientActorMessage::ReadGroup { done } => {
890 done.send(self.group.clone()).ok();
891 }
892 ClientActorMessage::NameEndpoint { name, done } => {
893 let res = self.send_name_endpoint(name).await;
894 done.send(res).ok();
895 }
896 ClientActorMessage::SetGroup { group, done } => {
897 let res = self.send_set_group(group).await;
898 done.send(res).ok();
899 }
900 ClientActorMessage::SetAttributes { attributes, done } => {
901 let res = self.send_set_attributes(attributes).await;
902 done.send(res).ok();
903 }
904 ClientActorMessage::SetAttribute { key, value, done } => {
905 let mut merged = self.attributes.clone();
910 merged.insert(key, value);
911 let res = match validate_attributes(&merged) {
912 Ok(()) => self.send_set_attributes(merged).await,
913 Err(err) => Err(Error::from(err)),
914 };
915 done.send(res).ok();
916 }
917 ClientActorMessage::PutNetworkDiagnostics { report, done } => {
918 let res = self.put_network_diagnostics(*report).await;
919 done.send(res).ok();
920 }
921 }
922 }
923 _ = async {
924 if let Some(ref mut timer) = metrics_timer {
925 timer.tick().await;
926 } else {
927 std::future::pending::<()>().await;
928 }
929 } => {
930 trace!("metrics send tick");
931 if let Err(err) = self.send_metrics().await {
932 debug!("failed to push metrics: {:#?}", err);
933 }
934 },
935 }
936 }
937 }
938
939 fn is_connected(&self) -> bool {
944 self.client
945 .as_ref()
946 .is_some_and(|client| client.connection.close_reason().is_none())
947 }
948
949 async fn connect(&mut self) -> Result<&IrohServicesClient, Error> {
962 if let Some(client) = &self.client
965 && let Some(reason) = client.connection.close_reason()
966 {
967 debug!(%reason, "connection closed by remote, reconnecting");
968 self.client = None;
969 }
970 let client = match self.client.take() {
975 Some(client) => client,
976 None => {
977 let client = RpcClient::connect(
978 &self.endpoint,
979 self.remote.clone(),
980 self.capabilities.clone(),
981 )
982 .await?;
983 self.encoder = Encoder::new(self.registry.clone());
985 client
986 }
987 };
988 Ok(&self.client.insert(client).irpc)
989 }
990
991 async fn rpc<Req, Res>(&mut self, msg: Req) -> Result<Res, Error>
992 where
993 IrohServicesProtocol: From<Req>,
994 ServicesMessage: From<WithChannels<Req, IrohServicesProtocol>>,
995 Req: Channels<
996 IrohServicesProtocol,
997 Tx = irpc::channel::oneshot::Sender<Res>,
998 Rx = NoReceiver,
999 > + Display,
1000 Res: RpcMessage,
1001 {
1002 trace!(request = %msg, "client actor send request");
1003 let client = self.connect().await?;
1004 let res = client.rpc(msg).await;
1005
1006 if let Err(err) = &res
1007 && is_connection_lost(err)
1008 {
1009 self.client = None;
1012 }
1013
1014 res.inspect_err(|err| warn!("rpc error: {err}"))
1015 .map_err(Error::from)
1016 }
1017
1018 async fn send_ping(&mut self) -> Result<Pong, Error> {
1019 let req = rand::random();
1020 let pong: ProtoPong = self.rpc(Ping { req_id: req }).await?;
1021 Ok(Pong {
1022 req_id: pong.req_id,
1023 })
1024 }
1025
1026 async fn send_name_endpoint(&mut self, name: String) -> Result<(), Error> {
1027 self.rpc(NameEndpoint { name: name.clone() })
1028 .await?
1029 .map_err(RemoteError::from_proto)?;
1030 self.name = Some(name);
1031 Ok(())
1032 }
1033
1034 async fn send_set_group(&mut self, group: String) -> Result<(), Error> {
1035 self.rpc(SetGroup {
1036 group: group.clone(),
1037 })
1038 .await?
1039 .map_err(RemoteError::from_proto)?;
1040 self.group = Some(group);
1041 Ok(())
1042 }
1043
1044 async fn send_set_attributes(
1045 &mut self,
1046 attributes: BTreeMap<String, String>,
1047 ) -> Result<(), Error> {
1048 self.rpc(SetAttributes {
1049 attributes: attributes.clone(),
1050 })
1051 .await?
1052 .map_err(RemoteError::from_proto)?;
1053 self.attributes = attributes;
1054 Ok(())
1055 }
1056
1057 async fn send_metrics(&mut self) -> Result<(), Error> {
1058 self.connect().await?;
1062 let update = self.encoder.export();
1063 let req = PutMetrics {
1065 session_id: self.session_id,
1066 update,
1067 };
1068 self.rpc(req).await?.map_err(RemoteError::from_proto)?;
1069 Ok(())
1070 }
1071
1072 async fn grant_cap(&mut self, cap: Rcan<ProtoCaps>) -> Result<(), Error> {
1073 self.rpc(GrantCap { cap })
1074 .await?
1075 .map_err(RemoteError::from_proto)?;
1076 Ok(())
1077 }
1078
1079 async fn put_network_diagnostics(&mut self, report: DiagnosticsReport) -> Result<(), Error> {
1080 self.rpc(PutNetworkDiagnostics {
1081 report: report.into_proto(),
1082 })
1083 .await?
1084 .map_err(RemoteError::from_proto)?;
1085 Ok(())
1086 }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use std::{
1092 collections::HashMap,
1093 sync::{
1094 Arc, RwLock,
1095 atomic::{AtomicBool, Ordering},
1096 },
1097 };
1098
1099 use iroh::{
1100 Endpoint, EndpointAddr, SecretKey,
1101 endpoint::{Connection, presets},
1102 protocol::{AcceptError, ProtocolHandler, Router},
1103 };
1104 use iroh_metrics::{
1105 Registry,
1106 encoding::{Decoder, Encoder},
1107 };
1108 use iroh_services_proto::{IrohServicesProtocol, Pong, ServicesMessage};
1109 use irpc::WithChannels;
1110 use irpc_iroh::read_request;
1111 use n0_error::AnyError;
1112 use n0_future::{
1113 task,
1114 time::{self, Duration},
1115 };
1116 use rand::{RngExt, SeedableRng};
1117 use temp_env_vars::temp_env_vars;
1118
1119 use crate::{
1120 Client, ClientBuilder,
1121 api_secret::ApiSecret,
1122 caps::Caps,
1123 client::{
1124 API_SECRET_ENV_VAR_NAME, ATTRIBUTE_VALUE_MAX_LENGTH, ATTRIBUTES_MAX_COUNT, BuildError,
1125 CLIENT_NAME_MAX_LENGTH, Error, ValidateAttributesError, ValidateNameError,
1126 is_connection_lost,
1127 },
1128 };
1129
1130 #[derive(Debug)]
1132 struct SeenUpdate {
1133 has_schema: bool,
1134 decoded_items: usize,
1135 }
1136
1137 #[derive(Debug)]
1139 enum Seen {
1140 Metrics(SeenUpdate),
1141 PingAnswered,
1143 }
1144
1145 #[derive(Debug)]
1153 struct TestServer {
1154 seen: tokio::sync::mpsc::UnboundedSender<Seen>,
1155 drop_next: Arc<AtomicBool>,
1157 ping_delay: Duration,
1159 }
1160
1161 impl TestServer {
1162 fn new(seen: tokio::sync::mpsc::UnboundedSender<Seen>) -> Self {
1163 Self {
1164 seen,
1165 drop_next: Arc::new(AtomicBool::new(false)),
1166 ping_delay: Duration::ZERO,
1167 }
1168 }
1169
1170 fn ping_delay(mut self, delay: Duration) -> Self {
1171 self.ping_delay = delay;
1172 self
1173 }
1174
1175 async fn handle_connection(&self, connection: Connection) -> anyhow::Result<()> {
1176 let Some(first_request) = read_request::<IrohServicesProtocol>(&connection).await?
1177 else {
1178 return Ok(());
1179 };
1180 let ServicesMessage::Auth(WithChannels { tx, .. }) = first_request else {
1181 connection.close(400u32.into(), b"Expected initial auth message");
1182 return Ok(());
1183 };
1184 tx.send(()).await?;
1185
1186 let mut decoder = Decoder::default();
1187 loop {
1188 let Ok(Some(request)) = read_request::<IrohServicesProtocol>(&connection).await
1189 else {
1190 return Ok(());
1191 };
1192 if self.drop_next.swap(false, Ordering::SeqCst) {
1193 connection.close(500u32.into(), b"test restart");
1196 return Ok(());
1197 }
1198 match request {
1199 ServicesMessage::Auth(_) => {
1200 connection.close(400u32.into(), b"Unexpected auth message");
1201 anyhow::bail!("client re-sent auth on a live connection");
1202 }
1203 ServicesMessage::Ping(WithChannels { inner, tx, .. }) => {
1204 time::sleep(self.ping_delay).await;
1205 tx.send(Pong {
1208 req_id: inner.req_id,
1209 })
1210 .await?;
1211 let _ = self.seen.send(Seen::PingAnswered);
1212 }
1213 ServicesMessage::PutMetrics(WithChannels { inner, tx, .. }) => {
1214 let has_schema = inner.update.schema.is_some();
1215 decoder.import(inner.update);
1216 let _ = self.seen.send(Seen::Metrics(SeenUpdate {
1217 has_schema,
1218 decoded_items: decoder.iter().count(),
1219 }));
1220 tx.send(Ok(())).await?;
1221 }
1222 _ => {
1223 connection.close(400u32.into(), b"Unexpected message in test");
1224 anyhow::bail!("unexpected message in test");
1225 }
1226 }
1227 }
1228 }
1229 }
1230
1231 impl ProtocolHandler for TestServer {
1232 async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
1233 self.handle_connection(connection).await.map_err(|e| {
1234 let boxed: Box<dyn std::error::Error + Send + Sync> = e.into();
1235 AcceptError::from(AnyError::from(boxed))
1236 })
1237 }
1238 }
1239
1240 async fn spawn_test_server(seed: u64, server: TestServer) -> (Router, Endpoint, ClientBuilder) {
1247 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(seed);
1248 let server_ep = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1249 let client_ep = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1250 let router = Router::builder(server_ep.clone())
1251 .accept(crate::ALPN, server)
1252 .spawn();
1253
1254 let shared_secret = SecretKey::from_bytes(&rng.random());
1255 let api_secret = ApiSecret::new(shared_secret, server_ep.id());
1256 let builder = Client::builder(&client_ep)
1257 .api_secret(api_secret)
1258 .unwrap()
1259 .remote(server_ep.addr());
1260 (router, client_ep, builder)
1261 }
1262
1263 async fn next_metrics(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Seen>) -> SeenUpdate {
1265 match rx.recv().await.expect("server dropped the record channel") {
1266 Seen::Metrics(update) => update,
1267 other => panic!("expected a metrics push, recorded {other:?}"),
1268 }
1269 }
1270
1271 fn recorded_so_far(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Seen>) -> Vec<Seen> {
1273 let mut seen = Vec::new();
1274 while let Ok(record) = rx.try_recv() {
1275 seen.push(record);
1276 }
1277 seen
1278 }
1279
1280 #[tokio::test]
1285 async fn test_metrics_schema_resent_after_reconnect() {
1286 let (seen_tx, mut seen_rx) = tokio::sync::mpsc::unbounded_channel();
1287 let server = TestServer::new(seen_tx);
1288 let drop_next = server.drop_next.clone();
1289 let (router, client_ep, builder) = spawn_test_server(2, server).await;
1290
1291 let client = builder.disable_metrics_interval().build().await.unwrap();
1292
1293 client.push_metrics().await.unwrap();
1295 let first = next_metrics(&mut seen_rx).await;
1296 assert!(first.has_schema);
1297 assert!(first.decoded_items > 0);
1298
1299 let mut settled = false;
1304 for _ in 0..20 {
1305 client.push_metrics().await.unwrap();
1306 let seen = next_metrics(&mut seen_rx).await;
1307 assert!(seen.decoded_items > 0);
1308 if !seen.has_schema {
1309 settled = true;
1310 break;
1311 }
1312 }
1313 assert!(settled, "schema must stop being sent once it is unchanged");
1314
1315 drop_next.store(true, Ordering::SeqCst);
1317 assert!(client.push_metrics().await.is_err());
1318
1319 client.push_metrics().await.unwrap();
1322 let third = next_metrics(&mut seen_rx).await;
1323 assert!(third.has_schema, "schema must be re-sent after a reconnect");
1324 assert!(third.decoded_items > 0);
1325
1326 router.shutdown().await.unwrap();
1327 client_ep.close().await;
1328 }
1329
1330 #[tokio::test]
1332 async fn test_fresh_encoder_resends_schema() {
1333 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1334 let mut registry = Registry::default();
1335 registry.register_all(endpoint.metrics());
1336 let registry = Arc::new(RwLock::new(registry));
1337
1338 let mut encoder = Encoder::new(registry.clone());
1339 let first = encoder.export();
1340 assert!(first.schema.is_some());
1341
1342 let schemaless = encoder.export();
1344 assert!(schemaless.schema.is_none());
1345
1346 let mut fresh_decoder = Decoder::default();
1349 fresh_decoder.import(schemaless);
1350 assert_eq!(fresh_decoder.iter().count(), 0);
1351
1352 let mut encoder = Encoder::new(registry);
1355 let resent = encoder.export();
1356 assert!(resent.schema.is_some());
1357 let mut fresh_decoder = Decoder::default();
1358 fresh_decoder.import(resent);
1359 assert!(fresh_decoder.iter().count() > 0);
1360 }
1361
1362 #[tokio::test]
1363 #[temp_env_vars]
1364 async fn test_api_key_from_env() {
1365 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1367 let shared_secret = SecretKey::from_bytes(&rng.random());
1368 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1369 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1370 unsafe {
1371 std::env::set_var(API_SECRET_ENV_VAR_NAME, api_secret.to_string());
1372 };
1373
1374 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1375
1376 let builder = Client::builder(&endpoint).api_secret_from_env().unwrap();
1377
1378 let fake_endpoint_addr: EndpointAddr = fake_endpoint_id.into();
1379 assert_eq!(builder.remote, Some(fake_endpoint_addr));
1380
1381 let cap = builder.cap.as_ref().expect("expected capability to be set");
1384 assert_eq!(cap.capability(), &Caps::client().0);
1385 assert_eq!(cap.audience(), &endpoint.id().as_verifying_key());
1386 assert_eq!(cap.issuer(), &shared_secret.public().as_verifying_key());
1387 }
1388
1389 #[tokio::test]
1392 async fn test_no_metrics_interval() {
1393 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(1);
1394 let shared_secret = SecretKey::from_bytes(&rng.random());
1395 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1396 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1397
1398 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1399
1400 let client = Client::builder(&endpoint)
1401 .disable_metrics_interval()
1402 .api_secret(api_secret)
1403 .unwrap()
1404 .build()
1405 .await
1406 .unwrap();
1407
1408 let err = client.push_metrics().await;
1409 assert!(err.is_err());
1410 }
1411
1412 #[tokio::test]
1415 async fn test_shutdown_stops_actor() {
1416 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(2);
1417 let shared_secret = SecretKey::from_bytes(&rng.random());
1418 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1419 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1420
1421 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1422
1423 let client = Client::builder(&endpoint)
1424 .api_secret(api_secret)
1425 .unwrap()
1426 .build()
1427 .await
1428 .unwrap();
1429
1430 client.shutdown().await;
1431
1432 let err = client.push_metrics().await;
1434 assert!(err.is_err());
1435 }
1436
1437 #[test]
1441 fn test_oversized_message_keeps_the_connection() {
1442 let send_error = |source| irpc::Error::Send {
1443 source,
1444 meta: Default::default(),
1445 };
1446
1447 assert!(!is_connection_lost(&send_error(
1448 irpc::channel::SendError::MaxMessageSizeExceeded {
1449 meta: Default::default(),
1450 }
1451 )));
1452 assert!(is_connection_lost(&send_error(
1453 irpc::channel::SendError::ReceiverClosed {
1454 meta: Default::default(),
1455 }
1456 )));
1457 }
1458
1459 #[tokio::test]
1466 async fn test_shutdown_drains_request_in_flight() {
1467 let (seen_tx, mut seen_rx) = tokio::sync::mpsc::unbounded_channel();
1468 let (router, client_ep, builder) = spawn_test_server(
1469 9,
1470 TestServer::new(seen_tx).ping_delay(Duration::from_millis(300)),
1472 )
1473 .await;
1474
1475 let client = builder
1476 .metrics_interval(Duration::from_secs(3600))
1478 .build()
1479 .await
1480 .unwrap();
1481
1482 next_metrics(&mut seen_rx).await;
1484
1485 let pinging = client.clone();
1487 let ping = task::spawn(async move { pinging.ping().await });
1488 time::sleep(Duration::from_millis(100)).await;
1489 client.shutdown().await;
1490
1491 let recorded = recorded_so_far(&mut seen_rx);
1492 assert!(
1493 recorded
1494 .iter()
1495 .any(|seen| matches!(seen, Seen::PingAnswered)),
1496 "the ping in flight was dropped, so the server saw its stream fail"
1497 );
1498 assert!(
1499 recorded.iter().any(|seen| matches!(seen, Seen::Metrics(_))),
1500 "the final metrics push did not reach the server"
1501 );
1502
1503 let _ = ping.await;
1504 router.shutdown().await.unwrap();
1505 client_ep.close().await;
1506 }
1507
1508 #[tokio::test]
1515 async fn test_shutdown_cancels_dial_in_flight() {
1516 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(3);
1517 let shared_secret = SecretKey::from_bytes(&rng.random());
1518 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1519 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1520
1521 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1522
1523 let client = Client::builder(&endpoint)
1524 .api_secret(api_secret)
1525 .unwrap()
1526 .remote(
1529 EndpointAddr::new(fake_endpoint_id).with_ip_addr("192.0.2.1:1234".parse().unwrap()),
1530 )
1531 .build()
1532 .await
1533 .unwrap();
1534
1535 time::sleep(Duration::from_millis(200)).await;
1537
1538 time::timeout(Duration::from_secs(5), client.shutdown())
1539 .await
1540 .expect("shutdown blocked on the dial in flight");
1541 }
1542
1543 #[tokio::test]
1544 async fn test_name() {
1545 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1546 let shared_secret = SecretKey::from_bytes(&rng.random());
1547 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1548 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1549
1550 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1551
1552 let builder = Client::builder(&endpoint)
1553 .name("my-node 👋")
1554 .unwrap()
1555 .api_secret(api_secret)
1556 .unwrap();
1557
1558 assert_eq!(builder.name, Some("my-node 👋".to_string()));
1559
1560 let Err(err) = Client::builder(&endpoint).name("a") else {
1561 panic!("name should fail for strings under 2 bytes");
1562 };
1563 assert!(matches!(
1564 err.downcast_ref::<BuildError>(),
1565 Some(BuildError::InvalidName(ValidateNameError::TooShort))
1566 ));
1567
1568 let too_long_name = "👋".repeat(129);
1569 let Err(err) = Client::builder(&endpoint).name(&too_long_name) else {
1570 panic!("name should fail for strings over 128 bytes");
1571 };
1572 assert!(matches!(
1573 err.downcast_ref::<BuildError>(),
1574 Some(BuildError::InvalidName(ValidateNameError::TooLong))
1575 ));
1576 }
1577
1578 #[tokio::test]
1579 async fn test_group() {
1580 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1581 let shared_secret = SecretKey::from_bytes(&rng.random());
1582 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1583 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1584
1585 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1586
1587 let builder = Client::builder(&endpoint)
1588 .group("staging")
1589 .unwrap()
1590 .api_secret(api_secret)
1591 .unwrap();
1592
1593 assert_eq!(builder.group, Some("staging".to_string()));
1594
1595 let Err(err) = Client::builder(&endpoint).group("a") else {
1596 panic!("group should fail for strings under 2 bytes");
1597 };
1598 assert!(matches!(
1599 err.downcast_ref::<BuildError>(),
1600 Some(BuildError::InvalidGroup(ValidateNameError::TooShort))
1601 ));
1602
1603 let too_long_group = "👋".repeat(129);
1604 let Err(err) = Client::builder(&endpoint).group(&too_long_group) else {
1605 panic!("group should fail for strings over 128 bytes");
1606 };
1607 assert!(matches!(
1608 err.downcast_ref::<BuildError>(),
1609 Some(BuildError::InvalidGroup(ValidateNameError::TooLong))
1610 ));
1611 }
1612
1613 #[tokio::test]
1614 async fn test_attributes() {
1615 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1616
1617 let builder = Client::builder(&endpoint)
1619 .attributes(std::iter::empty::<(String, String)>())
1620 .unwrap();
1621 assert_eq!(builder.attributes.as_ref().map(|m| m.len()), Some(0));
1622
1623 let builder = Client::builder(&endpoint)
1625 .attributes([("env", "prod"), ("region", "us-west")])
1626 .unwrap();
1627 let attrs = builder.attributes.as_ref().expect("attributes set");
1628 assert_eq!(attrs.get("env").map(String::as_str), Some("prod"));
1629 assert_eq!(attrs.get("region").map(String::as_str), Some("us-west"));
1630
1631 let mut map: HashMap<String, String> = HashMap::new();
1633 map.insert("k1".into(), "v1".into());
1634 map.insert("k2".into(), "".into()); let builder = Client::builder(&endpoint).attributes(map).unwrap();
1636 let attrs = builder.attributes.as_ref().expect("attributes set");
1637 assert_eq!(attrs.get("k2").map(String::as_str), Some(""));
1638
1639 let too_long_value = "x".repeat(129);
1641 let Err(err) = Client::builder(&endpoint).attributes([("ok", too_long_value.as_str())])
1642 else {
1643 panic!("attributes should fail for value over 128 bytes");
1644 };
1645 assert!(matches!(
1646 err.downcast_ref::<BuildError>(),
1647 Some(BuildError::InvalidAttributes(
1648 ValidateAttributesError::ValueTooLong
1649 ))
1650 ));
1651
1652 let Err(err) = Client::builder(&endpoint).attributes([("a", "v")]) else {
1654 panic!("attributes should fail for key under 2 bytes");
1655 };
1656 assert!(matches!(
1657 err.downcast_ref::<BuildError>(),
1658 Some(BuildError::InvalidAttributes(
1659 ValidateAttributesError::InvalidKey(ValidateNameError::TooShort)
1660 ))
1661 ));
1662
1663 let big: Vec<(String, String)> = (0..(ATTRIBUTES_MAX_COUNT + 1))
1665 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1666 .collect();
1667 let Err(err) = Client::builder(&endpoint).attributes(big) else {
1668 panic!("attributes should fail for more than 128 entries");
1669 };
1670 assert!(matches!(
1671 err.downcast_ref::<BuildError>(),
1672 Some(BuildError::InvalidAttributes(
1673 ValidateAttributesError::TooManyEntries
1674 ))
1675 ));
1676 }
1677
1678 async fn build_serverless_client(seed: u64) -> Client {
1682 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(seed);
1683 let shared_secret = SecretKey::from_bytes(&rng.random());
1684 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1685 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1686
1687 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1688
1689 Client::builder(&endpoint)
1690 .disable_metrics_interval()
1691 .api_secret(api_secret)
1692 .unwrap()
1693 .build()
1694 .await
1695 .unwrap()
1696 }
1697
1698 #[tokio::test]
1701 async fn test_set_group_runtime_validation() {
1702 let client = build_serverless_client(2).await;
1703
1704 let err = client
1705 .set_group("a")
1706 .await
1707 .expect_err("too-short group should fail validation");
1708 assert!(matches!(
1709 err,
1710 Error::InvalidGroup(ValidateNameError::TooShort)
1711 ));
1712
1713 let too_long = "x".repeat(CLIENT_NAME_MAX_LENGTH + 1);
1714 let err = client
1715 .set_group(too_long)
1716 .await
1717 .expect_err("too-long group should fail validation");
1718 assert!(matches!(
1719 err,
1720 Error::InvalidGroup(ValidateNameError::TooLong)
1721 ));
1722 }
1723
1724 #[tokio::test]
1727 async fn test_set_attributes_runtime_validation() {
1728 let client = build_serverless_client(3).await;
1729
1730 let err = client
1732 .set_attributes([("a", "v")])
1733 .await
1734 .expect_err("too-short attribute key should fail validation");
1735 assert!(matches!(
1736 err,
1737 Error::InvalidAttributes(ValidateAttributesError::InvalidKey(
1738 ValidateNameError::TooShort
1739 ))
1740 ));
1741
1742 let too_long_value = "x".repeat(ATTRIBUTE_VALUE_MAX_LENGTH + 1);
1744 let err = client
1745 .set_attributes([("ok", too_long_value.as_str())])
1746 .await
1747 .expect_err("too-long attribute value should fail validation");
1748 assert!(matches!(
1749 err,
1750 Error::InvalidAttributes(ValidateAttributesError::ValueTooLong)
1751 ));
1752
1753 let big: Vec<(String, String)> = (0..(ATTRIBUTES_MAX_COUNT + 1))
1755 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1756 .collect();
1757 let err = client
1758 .set_attributes(big)
1759 .await
1760 .expect_err("too many attributes should fail validation");
1761 assert!(matches!(
1762 err,
1763 Error::InvalidAttributes(ValidateAttributesError::TooManyEntries)
1764 ));
1765 }
1766
1767 #[tokio::test]
1768 async fn test_set_attribute_runtime_validation() {
1769 let client = build_serverless_client(7).await;
1770
1771 let err = client
1773 .set_attribute("a", "v")
1774 .await
1775 .expect_err("too-short attribute key should fail validation");
1776 assert!(matches!(
1777 err,
1778 Error::InvalidAttributes(ValidateAttributesError::InvalidKey(
1779 ValidateNameError::TooShort
1780 ))
1781 ));
1782
1783 let err = client
1787 .set_attribute("firmware", "2.1.0")
1788 .await
1789 .expect_err("no server: remote call must fail after validation passes");
1790 assert!(matches!(err, Error::Connect(_)), "got {err:?}");
1791 }
1792
1793 #[tokio::test]
1794 async fn test_set_attribute_merge_over_limit_rejected() {
1795 let full: Vec<(String, String)> = (0..ATTRIBUTES_MAX_COUNT)
1797 .map(|i| (format!("key_{i:04}"), "v".to_string()))
1798 .collect();
1799
1800 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(9);
1801 let shared_secret = SecretKey::from_bytes(&rng.random());
1802 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1803 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1804 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1805 let client = Client::builder(&endpoint)
1806 .disable_metrics_interval()
1807 .attributes(full)
1808 .unwrap()
1809 .api_secret(api_secret)
1810 .unwrap()
1811 .build()
1812 .await
1813 .unwrap();
1814
1815 let err = client
1819 .set_attribute("one-too-many", "v")
1820 .await
1821 .expect_err("merging past the attribute limit must fail");
1822 assert!(
1823 matches!(
1824 err,
1825 Error::InvalidAttributes(ValidateAttributesError::TooManyEntries)
1826 ),
1827 "expected TooManyEntries, got {err:?}"
1828 );
1829 }
1830
1831 #[tokio::test]
1836 async fn test_set_attributes_runtime_boundary_accepted() {
1837 let client = build_serverless_client(4).await;
1838
1839 let max_value = "x".repeat(ATTRIBUTE_VALUE_MAX_LENGTH);
1841 let err = client
1842 .set_attributes([("ok".to_string(), max_value)])
1843 .await
1844 .expect_err("no server: remote call must fail after validation passes");
1845 assert!(
1846 matches!(err, Error::Connect(_)),
1847 "expected a connect error (validation accepted), got {err:?}"
1848 );
1849
1850 let max_entries: Vec<(String, String)> = (0..ATTRIBUTES_MAX_COUNT)
1852 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1853 .collect();
1854 let err = client
1855 .set_attributes(max_entries)
1856 .await
1857 .expect_err("no server: remote call must fail after validation passes");
1858 assert!(
1859 matches!(err, Error::Connect(_)),
1860 "expected a connect error (validation accepted), got {err:?}"
1861 );
1862 }
1863}