1use std::{
2 collections::BTreeMap,
3 fmt::Display,
4 str::FromStr,
5 sync::{Arc, RwLock},
6};
7
8use anyhow::{Result, ensure};
9use iroh::{
10 Endpoint, EndpointAddr, EndpointId,
11 endpoint::{ConnectError, Connection},
12};
13use iroh_metrics::{MetricsGroup, Registry, encoding::Encoder};
14use irpc::{Channels, RpcMessage, WithChannels, channel::none::NoReceiver};
15use irpc_iroh::IrohRemoteConnection;
16use n0_error::StackResultExt;
17use n0_future::{
18 task::{self, AbortOnDropHandle},
19 time::{self, Duration},
20};
21use rcan::Rcan;
22use tokio::sync::oneshot;
23use tokio_util::sync::CancellationToken;
24use tracing::{debug, trace, warn};
25use uuid::Uuid;
26
27use crate::{
28 api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
29 caps::{Caps, DEFAULT_CAP_EXPIRY},
30 net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
31 protocol::{
32 ALPN, Auth, GrantCap, IrohServicesClient, IrohServicesProtocol, NameEndpoint, Ping, Pong,
33 PutMetrics, PutNetworkDiagnostics, RemoteError, ServicesMessage, SetAttributes, SetGroup,
34 },
35};
36
37#[derive(Debug, Clone)]
61pub struct Client {
62 endpoint: Endpoint,
64 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
65 shutdown: CancellationToken,
67 _actor_task: Arc<AbortOnDropHandle<()>>,
68}
69
70pub struct ClientBuilder {
73 cap_expiry: Duration,
74 cap: Option<Rcan<Caps>>,
75 endpoint: Endpoint,
76 name: Option<String>,
77 group: Option<String>,
78 attributes: Option<BTreeMap<String, String>>,
79 metrics_interval: Option<Duration>,
80 remote: Option<EndpointAddr>,
81 registry: Registry,
82}
83
84impl ClientBuilder {
85 pub fn new(endpoint: &Endpoint) -> Self {
86 let mut registry = Registry::default();
87 registry.register_all(endpoint.metrics());
88
89 Self {
90 cap: None,
91 cap_expiry: DEFAULT_CAP_EXPIRY,
92 endpoint: endpoint.clone(),
93 name: None,
94 group: None,
95 attributes: None,
96 metrics_interval: Some(Duration::from_secs(60)),
97 remote: None,
98 registry,
99 }
100 }
101
102 pub fn register_metrics_group(mut self, metrics_group: Arc<dyn MetricsGroup>) -> Self {
106 self.registry.register(metrics_group);
107 self
108 }
109
110 pub fn metrics_interval(mut self, interval: Duration) -> Self {
114 self.metrics_interval = Some(interval);
115 self
116 }
117
118 pub fn disable_metrics_interval(mut self) -> Self {
120 self.metrics_interval = None;
121 self
122 }
123
124 pub fn name(mut self, name: impl Into<String>) -> Result<Self> {
136 let name = name.into();
137 validate_name(&name).map_err(BuildError::InvalidName)?;
138 self.name = Some(name);
139 Ok(self)
140 }
141
142 pub fn group(mut self, group: impl Into<String>) -> Result<Self> {
151 let group = group.into();
152 validate_name(&group).map_err(BuildError::InvalidGroup)?;
153 self.group = Some(group);
154 Ok(self)
155 }
156
157 pub fn attributes<I, K, V>(mut self, attrs: I) -> Result<Self>
175 where
176 I: IntoIterator<Item = (K, V)>,
177 K: Into<String>,
178 V: Into<String>,
179 {
180 let collected: BTreeMap<String, String> = attrs
181 .into_iter()
182 .map(|(k, v)| (k.into(), v.into()))
183 .collect();
184 validate_attributes(&collected).map_err(BuildError::InvalidAttributes)?;
185 self.attributes = Some(collected);
186 Ok(self)
187 }
188
189 pub fn api_secret_from_env(self) -> Result<Self> {
191 let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
192 self.api_secret(ticket)
193 }
194
195 pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
197 let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
198 self.api_secret(key)
199 }
200
201 pub fn api_secret(mut self, ticket: ApiSecret) -> Result<Self> {
208 let local_id = self.endpoint.id();
209 let rcan = crate::caps::create_api_token_from_secret_key(
210 ticket.secret,
211 local_id,
212 self.cap_expiry,
213 Caps::for_shared_secret(),
214 )?;
215
216 self.remote = Some(ticket.remote);
217 self.rcan(rcan)
218 }
219
220 #[cfg(not(wasm_browser))]
224 pub async fn ssh_key_from_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self> {
225 let file_content = tokio::fs::read_to_string(path).await?;
226 self.ssh_key(&file_content)
227 }
228
229 #[cfg(not(wasm_browser))]
231 pub fn ssh_key(mut self, pem: &str) -> Result<Self> {
232 let local_id = self.endpoint.id();
233 let rcan = crate::caps::create_api_token_from_openssh_pem(
234 pem,
235 local_id,
236 self.cap_expiry,
237 Caps::all(),
238 )?;
239 self.cap.replace(rcan);
240
241 Ok(self)
242 }
243
244 pub fn rcan(mut self, cap: Rcan<Caps>) -> Result<Self> {
246 ensure!(
247 EndpointId::from_verifying_key(*cap.audience()) == self.endpoint.id(),
248 "invalid audience"
249 );
250 self.cap.replace(cap);
251 Ok(self)
252 }
253
254 pub fn remote(mut self, remote: impl Into<EndpointAddr>) -> Self {
257 self.remote = Some(remote.into());
258 self
259 }
260
261 #[must_use = "dropping the client will silently cancel all client tasks"]
263 pub async fn build(self) -> Result<Client, BuildError> {
264 debug!("starting iroh-services client");
265 let remote = self.remote.ok_or(BuildError::MissingRemote)?;
266 let capabilities = self.cap.ok_or(BuildError::MissingCapability)?;
267
268 let registry = Arc::new(RwLock::new(self.registry));
269 let (tx, rx) = tokio::sync::mpsc::channel(1);
270 let shutdown = CancellationToken::new();
271 let actor_task = AbortOnDropHandle::new(task::spawn(
272 ClientActor {
273 capabilities,
274 endpoint: self.endpoint.clone(),
275 remote,
276 client: None,
277 name: self.name.clone(),
278 group: self.group.clone(),
279 attributes: self.attributes.clone().unwrap_or_default(),
280 session_id: Uuid::new_v4(),
281 encoder: Encoder::new(registry.clone()),
282 registry,
283 }
284 .run(self.metrics_interval, rx, shutdown.clone()),
285 ));
286
287 Ok(Client {
288 endpoint: self.endpoint,
289 message_channel: tx,
290 shutdown,
291 _actor_task: Arc::new(actor_task),
292 })
293 }
294}
295
296#[derive(thiserror::Error, Debug)]
297#[non_exhaustive]
298pub enum BuildError {
299 #[error("Missing remote endpoint to dial")]
300 MissingRemote,
301 #[error("Missing capability")]
302 MissingCapability,
303 #[error("Unauthorized")]
304 Unauthorized,
305 #[error("Remote error: {0}")]
306 Remote(#[from] RemoteError),
307 #[error("Rpc connection error: {0}")]
308 Rpc(irpc::Error),
309 #[error("Connection error: {0}")]
310 Connect(ConnectError),
311 #[error("Invalid endpoint name: {0}")]
312 InvalidName(#[from] ValidateNameError),
313 #[error("Invalid endpoint group: {0}")]
314 InvalidGroup(ValidateNameError),
315 #[error("Invalid endpoint attributes: {0}")]
316 InvalidAttributes(#[from] ValidateAttributesError),
317}
318
319impl From<irpc::Error> for BuildError {
320 fn from(value: irpc::Error) -> Self {
321 match value {
322 irpc::Error::Request {
323 source:
324 irpc::RequestError::Connection {
325 source: iroh::endpoint::ConnectionError::ApplicationClosed(frame),
326 ..
327 },
328 ..
329 } if frame.error_code == 401u32.into() => Self::Unauthorized,
330 value => Self::Rpc(value),
331 }
332 }
333}
334
335const SHUTDOWN_GRACE: Duration = Duration::from_secs(3);
344
345const SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
351
352pub const CLIENT_NAME_MIN_LENGTH: usize = 2;
354pub const CLIENT_NAME_MAX_LENGTH: usize = 128;
356
357#[derive(Debug, thiserror::Error)]
359pub enum ValidateNameError {
360 #[error("Name is too long (must be no more than {CLIENT_NAME_MAX_LENGTH} bytes).")]
361 TooLong,
362 #[error("Name is too short (must be at least {CLIENT_NAME_MIN_LENGTH} bytes).")]
363 TooShort,
364}
365
366fn validate_name(name: &str) -> Result<(), ValidateNameError> {
367 if name.len() < CLIENT_NAME_MIN_LENGTH {
368 Err(ValidateNameError::TooShort)
369 } else if name.len() > CLIENT_NAME_MAX_LENGTH {
370 Err(ValidateNameError::TooLong)
371 } else {
372 Ok(())
373 }
374}
375
376pub const CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH: usize = 128;
378pub const CLIENT_ATTRIBUTES_MAX_COUNT: usize = 128;
380
381#[derive(Debug, thiserror::Error)]
383pub enum ValidateAttributesError {
384 #[error("Too many attributes (must be no more than {CLIENT_ATTRIBUTES_MAX_COUNT}).")]
385 TooManyEntries,
386 #[error("Invalid attribute key: {0}")]
387 InvalidKey(#[from] ValidateNameError),
388 #[error(
389 "Attribute value too long (must be no more than {CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH} bytes)."
390 )]
391 ValueTooLong,
392}
393
394fn validate_attributes(attrs: &BTreeMap<String, String>) -> Result<(), ValidateAttributesError> {
395 if attrs.len() > CLIENT_ATTRIBUTES_MAX_COUNT {
396 return Err(ValidateAttributesError::TooManyEntries);
397 }
398 for (k, v) in attrs {
399 validate_name(k)?;
400 if v.len() > CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH {
401 return Err(ValidateAttributesError::ValueTooLong);
402 }
403 }
404 Ok(())
405}
406
407#[derive(thiserror::Error, Debug)]
408#[non_exhaustive]
409pub enum Error {
410 #[error("Invalid endpoint name: {0}")]
411 InvalidName(#[from] ValidateNameError),
412 #[error("Invalid endpoint group: {0}")]
413 InvalidGroup(ValidateNameError),
414 #[error("Invalid endpoint attributes: {0}")]
415 InvalidAttributes(#[from] ValidateAttributesError),
416 #[error("Remote error: {0}")]
417 Remote(#[from] RemoteError),
418 #[error("Connection error: {0}")]
419 Connect(#[from] ConnectError),
420 #[error("Rpc error: {0}")]
421 Rpc(#[from] irpc::Error),
422 #[error(transparent)]
423 Other(#[from] anyhow::Error),
424 #[error("Local client actor is stopped, cannot send requests")]
425 ActorStopped,
426}
427
428impl From<tokio::sync::mpsc::error::SendError<ClientActorMessage>> for Error {
429 fn from(_value: tokio::sync::mpsc::error::SendError<ClientActorMessage>) -> Self {
430 Error::ActorStopped
431 }
432}
433
434impl From<tokio::sync::oneshot::error::RecvError> for Error {
435 fn from(_value: tokio::sync::oneshot::error::RecvError) -> Self {
436 Error::ActorStopped
437 }
438}
439
440impl Client {
441 pub fn builder(endpoint: &Endpoint) -> ClientBuilder {
442 ClientBuilder::new(endpoint)
443 }
444
445 pub async fn name(&self) -> Result<Option<String>, Error> {
447 let (tx, rx) = oneshot::channel();
448 self.message_channel
449 .send(ClientActorMessage::ReadName { done: tx })
450 .await?;
451 rx.await.map_err(Into::into)
452 }
453
454 pub async fn group(&self) -> Result<Option<String>, Error> {
456 let (tx, rx) = oneshot::channel();
457 self.message_channel
458 .send(ClientActorMessage::ReadGroup { done: tx })
459 .await?;
460 rx.await.map_err(Into::into)
461 }
462
463 pub async fn set_name(&self, name: impl Into<String>) -> Result<(), Error> {
468 let name = name.into();
469 validate_name(&name)?;
470 debug!(name_len = name.len(), "calling set name");
471 let (tx, rx) = oneshot::channel();
472 self.message_channel
473 .send(ClientActorMessage::NameEndpoint { name, done: tx })
474 .await?;
475 rx.await?
476 }
477
478 pub async fn set_group(&self, group: impl Into<String>) -> Result<(), Error> {
482 let group: String = group.into();
483 validate_name(&group).map_err(Error::InvalidGroup)?;
484 debug!(%group, "calling set group");
485 let (tx, rx) = oneshot::channel();
486 self.message_channel
487 .send(ClientActorMessage::SetGroup { group, done: tx })
488 .await?;
489 rx.await?
490 }
491
492 pub async fn set_attributes<I, K, V>(&self, attrs: I) -> Result<(), Error>
510 where
511 I: IntoIterator<Item = (K, V)>,
512 K: Into<String>,
513 V: Into<String>,
514 {
515 let collected: BTreeMap<String, String> = attrs
516 .into_iter()
517 .map(|(k, v)| (k.into(), v.into()))
518 .collect();
519 validate_attributes(&collected)?;
520 debug!(attr_count = collected.len(), "calling set attributes");
521 let (tx, rx) = oneshot::channel();
522 self.message_channel
523 .send(ClientActorMessage::SetAttributes {
524 attributes: collected,
525 done: tx,
526 })
527 .await?;
528 rx.await?
529 }
530
531 pub async fn set_attribute(
538 &self,
539 key: impl Into<String>,
540 value: impl Into<String>,
541 ) -> Result<(), Error> {
542 let (tx, rx) = oneshot::channel();
547 self.message_channel
548 .send(ClientActorMessage::SetAttribute {
549 key: key.into(),
550 value: value.into(),
551 done: tx,
552 })
553 .await?;
554 rx.await?
555 }
556
557 pub async fn ping(&self) -> Result<Pong, Error> {
559 let (tx, rx) = oneshot::channel();
560 self.message_channel
561 .send(ClientActorMessage::Ping { done: tx })
562 .await?;
563 rx.await?
564 }
565
566 pub async fn push_metrics(&self) -> Result<(), Error> {
570 let (tx, rx) = oneshot::channel();
571 self.message_channel
572 .send(ClientActorMessage::SendMetrics { done: tx })
573 .await?;
574 rx.await?
575 }
576
577 pub async fn shutdown(&self) {
587 self.shutdown.cancel();
588 self.message_channel.closed().await;
590 }
591
592 pub async fn grant_capability(
596 &self,
597 remote_id: EndpointId,
598 caps: impl IntoIterator<Item = impl Into<crate::caps::Cap>>,
599 ) -> Result<(), Error> {
600 let cap = crate::caps::create_grant_token(
601 self.endpoint.secret_key().clone(),
602 remote_id,
603 DEFAULT_CAP_EXPIRY,
604 Caps::new(caps),
605 )
606 .map_err(Error::Other)?;
607
608 let (tx, rx) = oneshot::channel();
609 self.message_channel
610 .send(ClientActorMessage::GrantCap {
611 cap: Box::new(cap),
612 done: tx,
613 })
614 .await?;
615 rx.await?
616 }
617
618 pub async fn net_diagnostics(&self, send: bool) -> Result<DiagnosticsReport, Error> {
620 let report = run_diagnostics(&self.endpoint).await?;
621 if send {
622 let (tx, rx) = oneshot::channel();
623 self.message_channel
624 .send(ClientActorMessage::PutNetworkDiagnostics {
625 done: tx,
626 report: Box::new(report.clone()),
627 })
628 .await?;
629 rx.await??;
630 }
631
632 Ok(report)
633 }
634}
635
636enum ClientActorMessage {
637 SendMetrics {
638 done: oneshot::Sender<Result<(), Error>>,
639 },
640 Ping {
641 done: oneshot::Sender<Result<Pong, Error>>,
642 },
643 GrantCap {
644 cap: Box<Rcan<Caps>>,
646 done: oneshot::Sender<Result<(), Error>>,
647 },
648 PutNetworkDiagnostics {
649 report: Box<DiagnosticsReport>,
650 done: oneshot::Sender<Result<(), Error>>,
651 },
652 ReadName {
653 done: oneshot::Sender<Option<String>>,
654 },
655 ReadGroup {
656 done: oneshot::Sender<Option<String>>,
657 },
658 NameEndpoint {
659 name: String,
660 done: oneshot::Sender<Result<(), Error>>,
661 },
662 SetGroup {
663 group: String,
664 done: oneshot::Sender<Result<(), Error>>,
665 },
666 SetAttributes {
667 attributes: BTreeMap<String, String>,
668 done: oneshot::Sender<Result<(), Error>>,
669 },
670 SetAttribute {
671 key: String,
672 value: String,
673 done: oneshot::Sender<Result<(), Error>>,
677 },
678}
679
680fn is_connection_lost(err: &irpc::Error) -> bool {
688 !matches!(
689 err,
690 irpc::Error::Send {
691 source: irpc::channel::SendError::MaxMessageSizeExceeded { .. },
692 ..
693 }
694 )
695}
696
697struct RpcClient {
704 connection: Connection,
706 irpc: IrohServicesClient,
707}
708
709impl RpcClient {
710 async fn connect(
712 endpoint: &Endpoint,
713 remote: EndpointAddr,
714 caps: Rcan<Caps>,
715 ) -> Result<Self, Error> {
716 trace!("client connecting and authorizing");
717 let connection = endpoint
718 .connect(remote, ALPN)
719 .await
720 .inspect_err(|err| debug!("connect failed: {err:?}"))?;
721 let irpc = IrohServicesClient::boxed(IrohRemoteConnection::new(connection.clone()));
722 irpc.rpc(Auth { caps })
723 .await
724 .inspect_err(|err| debug!("authorization failed: {err:?}"))
725 .map_err(|err| RemoteError::AuthError(err.to_string()))?;
726 Ok(Self { connection, irpc })
727 }
728}
729
730struct ClientActor {
731 capabilities: Rcan<Caps>,
732 endpoint: Endpoint,
733 remote: EndpointAddr,
734 client: Option<RpcClient>,
743 name: Option<String>,
744 group: Option<String>,
745 attributes: BTreeMap<String, String>,
746 session_id: Uuid,
747 encoder: Encoder,
748 registry: Arc<RwLock<Registry>>,
750}
751
752impl ClientActor {
753 async fn run(
760 mut self,
761 interval: Option<Duration>,
762 mut inbox: tokio::sync::mpsc::Receiver<ClientActorMessage>,
763 shutdown: CancellationToken,
764 ) {
765 let metrics_enabled = interval.is_some();
766 let shutdown_and_grace_period_expired = async {
767 shutdown.cancelled().await;
768 time::sleep(SHUTDOWN_GRACE).await;
769 };
770
771 let clean_shutdown = tokio::select! {
772 () = self.run_inner(interval, &mut inbox, &shutdown) => true,
773 () = shutdown_and_grace_period_expired => {
774 debug!("shutdown grace elapsed, dropping the request in flight");
775 false
776 }
777 };
778
779 if clean_shutdown && metrics_enabled && self.is_connected() {
783 match time::timeout(SHUTDOWN_FLUSH_TIMEOUT, self.send_metrics()).await {
784 Ok(Ok(())) => trace!("pushed final metrics on shutdown"),
785 Ok(Err(err)) => debug!(%err, "failed to push final metrics on shutdown"),
786 Err(_) => debug!("final metrics push on shutdown timed out"),
787 }
788 }
789 debug!("client actor shut down");
790 }
791
792 async fn run_inner(
793 &mut self,
794 interval: Option<Duration>,
795 inbox: &mut tokio::sync::mpsc::Receiver<ClientActorMessage>,
796 shutdown: &CancellationToken,
797 ) {
798 let mut metrics_timer = interval.map(|interval| time::interval(interval));
799 trace!("starting client actor");
800
801 if let Some(name) = self.name.clone()
804 && let Err(err) = self.send_name_endpoint(name).await
805 {
806 warn!(err = %err, "failed setting endpoint name on startup");
807 }
808
809 if let Some(group) = self.group.clone()
810 && let Err(err) = self.send_set_group(group).await
811 {
812 warn!(err = %err, "failed setting endpoint group on startup");
813 }
814
815 if !self.attributes.is_empty()
816 && let Err(err) = self.send_set_attributes(self.attributes.clone()).await
817 {
818 warn!(err = %err, "failed setting endpoint attributes on startup");
819 }
820
821 loop {
822 trace!("client actor tick");
823 tokio::select! {
824 biased;
825 () = shutdown.cancelled() => {
828 trace!("client actor observed shutdown between requests");
829 break;
830 }
831 Some(msg) = inbox.recv() => {
832 match msg {
833 ClientActorMessage::Ping { done } => {
834 let res = self.send_ping().await;
835 done.send(res).ok();
836 },
837 ClientActorMessage::SendMetrics { done } => {
838 trace!("sending metrics manually triggered");
839 let res = self.send_metrics().await;
840 done.send(res).ok();
841 }
842 ClientActorMessage::GrantCap { cap, done } => {
843 let res = self.grant_cap(*cap).await;
844 done.send(res).ok();
845 }
846 ClientActorMessage::ReadName { done } => {
847 done.send(self.name.clone()).ok();
848 }
849 ClientActorMessage::ReadGroup { done } => {
850 done.send(self.group.clone()).ok();
851 }
852 ClientActorMessage::NameEndpoint { name, done } => {
853 let res = self.send_name_endpoint(name).await;
854 done.send(res).ok();
855 }
856 ClientActorMessage::SetGroup { group, done } => {
857 let res = self.send_set_group(group).await;
858 done.send(res).ok();
859 }
860 ClientActorMessage::SetAttributes { attributes, done } => {
861 let res = self.send_set_attributes(attributes).await;
862 done.send(res).ok();
863 }
864 ClientActorMessage::SetAttribute { key, value, done } => {
865 let mut merged = self.attributes.clone();
870 merged.insert(key, value);
871 let res = match validate_attributes(&merged) {
872 Ok(()) => self.send_set_attributes(merged).await,
873 Err(err) => Err(Error::from(err)),
874 };
875 done.send(res).ok();
876 }
877 ClientActorMessage::PutNetworkDiagnostics { report, done } => {
878 let res = self.put_network_diagnostics(*report).await;
879 done.send(res).ok();
880 }
881 }
882 }
883 _ = async {
884 if let Some(ref mut timer) = metrics_timer {
885 timer.tick().await;
886 } else {
887 std::future::pending::<()>().await;
888 }
889 } => {
890 trace!("metrics send tick");
891 if let Err(err) = self.send_metrics().await {
892 debug!("failed to push metrics: {:#?}", err);
893 }
894 },
895 }
896 }
897 }
898
899 fn is_connected(&self) -> bool {
904 self.client
905 .as_ref()
906 .is_some_and(|client| client.connection.close_reason().is_none())
907 }
908
909 async fn connect(&mut self) -> Result<&IrohServicesClient, Error> {
922 if let Some(client) = &self.client
925 && let Some(reason) = client.connection.close_reason()
926 {
927 debug!(%reason, "connection closed by remote, reconnecting");
928 self.client = None;
929 }
930 let client = match self.client.take() {
935 Some(client) => client,
936 None => {
937 let client = RpcClient::connect(
938 &self.endpoint,
939 self.remote.clone(),
940 self.capabilities.clone(),
941 )
942 .await?;
943 self.encoder = Encoder::new(self.registry.clone());
945 client
946 }
947 };
948 Ok(&self.client.insert(client).irpc)
949 }
950
951 async fn rpc<Req, Res>(&mut self, msg: Req) -> Result<Res, Error>
952 where
953 IrohServicesProtocol: From<Req>,
954 ServicesMessage: From<WithChannels<Req, IrohServicesProtocol>>,
955 Req: Channels<
956 IrohServicesProtocol,
957 Tx = irpc::channel::oneshot::Sender<Res>,
958 Rx = NoReceiver,
959 > + Display,
960 Res: RpcMessage,
961 {
962 trace!(request = %msg, "client actor send request");
963 let client = self.connect().await?;
964 let res = client.rpc(msg).await;
965
966 if let Err(err) = &res
967 && is_connection_lost(err)
968 {
969 self.client = None;
972 }
973
974 res.inspect_err(|err| warn!("rpc error: {err}"))
975 .map_err(Error::from)
976 }
977
978 async fn send_ping(&mut self) -> Result<Pong, Error> {
979 let req = rand::random();
980 self.rpc(Ping { req_id: req }).await
981 }
982
983 async fn send_name_endpoint(&mut self, name: String) -> Result<(), Error> {
984 self.rpc(NameEndpoint { name: name.clone() }).await??;
985 self.name = Some(name);
986 Ok(())
987 }
988
989 async fn send_set_group(&mut self, group: String) -> Result<(), Error> {
990 self.rpc(SetGroup {
991 group: group.clone(),
992 })
993 .await??;
994 self.group = Some(group);
995 Ok(())
996 }
997
998 async fn send_set_attributes(
999 &mut self,
1000 attributes: BTreeMap<String, String>,
1001 ) -> Result<(), Error> {
1002 self.rpc(SetAttributes {
1003 attributes: attributes.clone(),
1004 })
1005 .await??;
1006 self.attributes = attributes;
1007 Ok(())
1008 }
1009
1010 async fn send_metrics(&mut self) -> Result<(), Error> {
1011 self.connect().await?;
1015 let update = self.encoder.export();
1016 let req = PutMetrics {
1018 session_id: self.session_id,
1019 update,
1020 };
1021 self.rpc(req).await??;
1022 Ok(())
1023 }
1024
1025 async fn grant_cap(&mut self, cap: Rcan<Caps>) -> Result<(), Error> {
1026 self.rpc(GrantCap { cap }).await??;
1027 Ok(())
1028 }
1029
1030 async fn put_network_diagnostics(&mut self, report: DiagnosticsReport) -> Result<(), Error> {
1031 self.rpc(PutNetworkDiagnostics { report }).await??;
1032 Ok(())
1033 }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use std::{
1039 collections::HashMap,
1040 sync::{
1041 Arc, RwLock,
1042 atomic::{AtomicBool, Ordering},
1043 },
1044 };
1045
1046 use iroh::{
1047 Endpoint, EndpointAddr, SecretKey,
1048 endpoint::{Connection, presets},
1049 protocol::{AcceptError, ProtocolHandler, Router},
1050 };
1051 use iroh_metrics::{
1052 Registry,
1053 encoding::{Decoder, Encoder},
1054 };
1055 use irpc::WithChannels;
1056 use irpc_iroh::read_request;
1057 use n0_error::AnyError;
1058 use n0_future::{
1059 task,
1060 time::{self, Duration},
1061 };
1062 use rand::{RngExt, SeedableRng};
1063 use temp_env_vars::temp_env_vars;
1064
1065 use crate::{
1066 Client, ClientBuilder,
1067 api_secret::ApiSecret,
1068 caps::{Cap, Caps, create_api_token_from_secret_key},
1069 client::{
1070 API_SECRET_ENV_VAR_NAME, BuildError, CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH,
1071 CLIENT_ATTRIBUTES_MAX_COUNT, CLIENT_NAME_MAX_LENGTH, Error, ValidateAttributesError,
1072 ValidateNameError, is_connection_lost,
1073 },
1074 protocol::{ALPN, IrohServicesProtocol, Pong, ServicesMessage},
1075 };
1076
1077 #[derive(Debug)]
1079 struct SeenUpdate {
1080 has_schema: bool,
1081 decoded_items: usize,
1082 }
1083
1084 #[derive(Debug)]
1086 enum Seen {
1087 Metrics(SeenUpdate),
1088 PingAnswered,
1090 }
1091
1092 #[derive(Debug)]
1100 struct TestServer {
1101 seen: tokio::sync::mpsc::UnboundedSender<Seen>,
1102 drop_next: Arc<AtomicBool>,
1104 ping_delay: Duration,
1106 }
1107
1108 impl TestServer {
1109 fn new(seen: tokio::sync::mpsc::UnboundedSender<Seen>) -> Self {
1110 Self {
1111 seen,
1112 drop_next: Arc::new(AtomicBool::new(false)),
1113 ping_delay: Duration::ZERO,
1114 }
1115 }
1116
1117 fn ping_delay(mut self, delay: Duration) -> Self {
1118 self.ping_delay = delay;
1119 self
1120 }
1121
1122 async fn handle_connection(&self, connection: Connection) -> anyhow::Result<()> {
1123 let Some(first_request) = read_request::<IrohServicesProtocol>(&connection).await?
1124 else {
1125 return Ok(());
1126 };
1127 let ServicesMessage::Auth(WithChannels { tx, .. }) = first_request else {
1128 connection.close(400u32.into(), b"Expected initial auth message");
1129 return Ok(());
1130 };
1131 tx.send(()).await?;
1132
1133 let mut decoder = Decoder::default();
1134 loop {
1135 let Ok(Some(request)) = read_request::<IrohServicesProtocol>(&connection).await
1136 else {
1137 return Ok(());
1138 };
1139 if self.drop_next.swap(false, Ordering::SeqCst) {
1140 connection.close(500u32.into(), b"test restart");
1143 return Ok(());
1144 }
1145 match request {
1146 ServicesMessage::Auth(_) => {
1147 connection.close(400u32.into(), b"Unexpected auth message");
1148 anyhow::bail!("client re-sent auth on a live connection");
1149 }
1150 ServicesMessage::Ping(WithChannels { inner, tx, .. }) => {
1151 time::sleep(self.ping_delay).await;
1152 tx.send(Pong {
1155 req_id: inner.req_id,
1156 })
1157 .await?;
1158 let _ = self.seen.send(Seen::PingAnswered);
1159 }
1160 ServicesMessage::PutMetrics(WithChannels { inner, tx, .. }) => {
1161 let has_schema = inner.update.schema.is_some();
1162 decoder.import(inner.update);
1163 let _ = self.seen.send(Seen::Metrics(SeenUpdate {
1164 has_schema,
1165 decoded_items: decoder.iter().count(),
1166 }));
1167 tx.send(Ok(())).await?;
1168 }
1169 _ => {
1170 connection.close(400u32.into(), b"Unexpected message in test");
1171 anyhow::bail!("unexpected message in test");
1172 }
1173 }
1174 }
1175 }
1176 }
1177
1178 impl ProtocolHandler for TestServer {
1179 async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
1180 self.handle_connection(connection).await.map_err(|e| {
1181 let boxed: Box<dyn std::error::Error + Send + Sync> = e.into();
1182 AcceptError::from(AnyError::from(boxed))
1183 })
1184 }
1185 }
1186
1187 async fn spawn_test_server(seed: u64, server: TestServer) -> (Router, Endpoint, ClientBuilder) {
1194 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(seed);
1195 let server_ep = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1196 let client_ep = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1197 let router = Router::builder(server_ep.clone())
1198 .accept(ALPN, server)
1199 .spawn();
1200
1201 let shared_secret = SecretKey::from_bytes(&rng.random());
1202 let cap = create_api_token_from_secret_key(
1203 shared_secret,
1204 client_ep.id(),
1205 Duration::from_secs(3600),
1206 Caps::for_shared_secret(),
1207 )
1208 .unwrap();
1209
1210 let builder = Client::builder(&client_ep)
1211 .remote(server_ep.addr())
1212 .rcan(cap)
1213 .unwrap();
1214 (router, client_ep, builder)
1215 }
1216
1217 async fn next_metrics(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Seen>) -> SeenUpdate {
1219 match rx.recv().await.expect("server dropped the record channel") {
1220 Seen::Metrics(update) => update,
1221 other => panic!("expected a metrics push, recorded {other:?}"),
1222 }
1223 }
1224
1225 fn recorded_so_far(rx: &mut tokio::sync::mpsc::UnboundedReceiver<Seen>) -> Vec<Seen> {
1227 let mut seen = Vec::new();
1228 while let Ok(record) = rx.try_recv() {
1229 seen.push(record);
1230 }
1231 seen
1232 }
1233
1234 #[tokio::test]
1239 async fn test_metrics_schema_resent_after_reconnect() {
1240 let (seen_tx, mut seen_rx) = tokio::sync::mpsc::unbounded_channel();
1241 let server = TestServer::new(seen_tx);
1242 let drop_next = server.drop_next.clone();
1243 let (router, client_ep, builder) = spawn_test_server(2, server).await;
1244
1245 let client = builder.disable_metrics_interval().build().await.unwrap();
1246
1247 client.push_metrics().await.unwrap();
1249 let first = next_metrics(&mut seen_rx).await;
1250 assert!(first.has_schema);
1251 assert!(first.decoded_items > 0);
1252
1253 let mut settled = false;
1258 for _ in 0..20 {
1259 client.push_metrics().await.unwrap();
1260 let seen = next_metrics(&mut seen_rx).await;
1261 assert!(seen.decoded_items > 0);
1262 if !seen.has_schema {
1263 settled = true;
1264 break;
1265 }
1266 }
1267 assert!(settled, "schema must stop being sent once it is unchanged");
1268
1269 drop_next.store(true, Ordering::SeqCst);
1271 assert!(client.push_metrics().await.is_err());
1272
1273 client.push_metrics().await.unwrap();
1276 let third = next_metrics(&mut seen_rx).await;
1277 assert!(third.has_schema, "schema must be re-sent after a reconnect");
1278 assert!(third.decoded_items > 0);
1279
1280 router.shutdown().await.unwrap();
1281 client_ep.close().await;
1282 }
1283
1284 #[tokio::test]
1286 async fn test_fresh_encoder_resends_schema() {
1287 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1288 let mut registry = Registry::default();
1289 registry.register_all(endpoint.metrics());
1290 let registry = Arc::new(RwLock::new(registry));
1291
1292 let mut encoder = Encoder::new(registry.clone());
1293 let first = encoder.export();
1294 assert!(first.schema.is_some());
1295
1296 let schemaless = encoder.export();
1298 assert!(schemaless.schema.is_none());
1299
1300 let mut fresh_decoder = Decoder::default();
1303 fresh_decoder.import(schemaless);
1304 assert_eq!(fresh_decoder.iter().count(), 0);
1305
1306 let mut encoder = Encoder::new(registry);
1309 let resent = encoder.export();
1310 assert!(resent.schema.is_some());
1311 let mut fresh_decoder = Decoder::default();
1312 fresh_decoder.import(resent);
1313 assert!(fresh_decoder.iter().count() > 0);
1314 }
1315
1316 #[tokio::test]
1317 #[temp_env_vars]
1318 async fn test_api_key_from_env() {
1319 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1321 let shared_secret = SecretKey::from_bytes(&rng.random());
1322 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1323 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1324 unsafe {
1325 std::env::set_var(API_SECRET_ENV_VAR_NAME, api_secret.to_string());
1326 };
1327
1328 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1329
1330 let builder = Client::builder(&endpoint).api_secret_from_env().unwrap();
1331
1332 let fake_endpoint_addr: EndpointAddr = fake_endpoint_id.into();
1333 assert_eq!(builder.remote, Some(fake_endpoint_addr));
1334
1335 let cap = builder.cap.as_ref().expect("expected capability to be set");
1338 assert_eq!(cap.capability(), &Caps::new([Cap::Client]));
1339 assert_eq!(cap.audience(), &endpoint.id().as_verifying_key());
1340 assert_eq!(cap.issuer(), &shared_secret.public().as_verifying_key());
1341 }
1342
1343 #[tokio::test]
1346 async fn test_no_metrics_interval() {
1347 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(1);
1348 let shared_secret = SecretKey::from_bytes(&rng.random());
1349 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1350 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1351
1352 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1353
1354 let client = Client::builder(&endpoint)
1355 .disable_metrics_interval()
1356 .api_secret(api_secret)
1357 .unwrap()
1358 .build()
1359 .await
1360 .unwrap();
1361
1362 let err = client.push_metrics().await;
1363 assert!(err.is_err());
1364 }
1365
1366 #[tokio::test]
1369 async fn test_shutdown_stops_actor() {
1370 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(2);
1371 let shared_secret = SecretKey::from_bytes(&rng.random());
1372 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1373 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1374
1375 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1376
1377 let client = Client::builder(&endpoint)
1378 .api_secret(api_secret)
1379 .unwrap()
1380 .build()
1381 .await
1382 .unwrap();
1383
1384 client.shutdown().await;
1385
1386 let err = client.push_metrics().await;
1388 assert!(err.is_err());
1389 }
1390
1391 #[test]
1395 fn test_oversized_message_keeps_the_connection() {
1396 let send_error = |source| irpc::Error::Send {
1397 source,
1398 meta: Default::default(),
1399 };
1400
1401 assert!(!is_connection_lost(&send_error(
1402 irpc::channel::SendError::MaxMessageSizeExceeded {
1403 meta: Default::default(),
1404 }
1405 )));
1406 assert!(is_connection_lost(&send_error(
1407 irpc::channel::SendError::ReceiverClosed {
1408 meta: Default::default(),
1409 }
1410 )));
1411 }
1412
1413 #[tokio::test]
1420 async fn test_shutdown_drains_request_in_flight() {
1421 let (seen_tx, mut seen_rx) = tokio::sync::mpsc::unbounded_channel();
1422 let (router, client_ep, builder) = spawn_test_server(
1423 9,
1424 TestServer::new(seen_tx).ping_delay(Duration::from_millis(300)),
1426 )
1427 .await;
1428
1429 let client = builder
1430 .metrics_interval(Duration::from_secs(3600))
1432 .build()
1433 .await
1434 .unwrap();
1435
1436 next_metrics(&mut seen_rx).await;
1438
1439 let pinging = client.clone();
1441 let ping = task::spawn(async move { pinging.ping().await });
1442 time::sleep(Duration::from_millis(100)).await;
1443 client.shutdown().await;
1444
1445 let recorded = recorded_so_far(&mut seen_rx);
1446 assert!(
1447 recorded
1448 .iter()
1449 .any(|seen| matches!(seen, Seen::PingAnswered)),
1450 "the ping in flight was dropped, so the server saw its stream fail"
1451 );
1452 assert!(
1453 recorded.iter().any(|seen| matches!(seen, Seen::Metrics(_))),
1454 "the final metrics push did not reach the server"
1455 );
1456
1457 let _ = ping.await;
1458 router.shutdown().await.unwrap();
1459 client_ep.close().await;
1460 }
1461
1462 #[tokio::test]
1469 async fn test_shutdown_cancels_dial_in_flight() {
1470 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(3);
1471 let shared_secret = SecretKey::from_bytes(&rng.random());
1472 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1473 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1474
1475 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1476
1477 let client = Client::builder(&endpoint)
1478 .api_secret(api_secret)
1479 .unwrap()
1480 .remote(
1483 EndpointAddr::new(fake_endpoint_id).with_ip_addr("192.0.2.1:1234".parse().unwrap()),
1484 )
1485 .build()
1486 .await
1487 .unwrap();
1488
1489 time::sleep(Duration::from_millis(200)).await;
1491
1492 time::timeout(Duration::from_secs(5), client.shutdown())
1493 .await
1494 .expect("shutdown blocked on the dial in flight");
1495 }
1496
1497 #[tokio::test]
1498 async fn test_name() {
1499 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1500 let shared_secret = SecretKey::from_bytes(&rng.random());
1501 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1502 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1503
1504 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1505
1506 let builder = Client::builder(&endpoint)
1507 .name("my-node 👋")
1508 .unwrap()
1509 .api_secret(api_secret)
1510 .unwrap();
1511
1512 assert_eq!(builder.name, Some("my-node 👋".to_string()));
1513
1514 let Err(err) = Client::builder(&endpoint).name("a") else {
1515 panic!("name should fail for strings under 2 bytes");
1516 };
1517 assert!(matches!(
1518 err.downcast_ref::<BuildError>(),
1519 Some(BuildError::InvalidName(ValidateNameError::TooShort))
1520 ));
1521
1522 let too_long_name = "👋".repeat(129);
1523 let Err(err) = Client::builder(&endpoint).name(&too_long_name) else {
1524 panic!("name should fail for strings over 128 bytes");
1525 };
1526 assert!(matches!(
1527 err.downcast_ref::<BuildError>(),
1528 Some(BuildError::InvalidName(ValidateNameError::TooLong))
1529 ));
1530 }
1531
1532 #[tokio::test]
1533 async fn test_group() {
1534 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1535 let shared_secret = SecretKey::from_bytes(&rng.random());
1536 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1537 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1538
1539 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1540
1541 let builder = Client::builder(&endpoint)
1542 .group("staging")
1543 .unwrap()
1544 .api_secret(api_secret)
1545 .unwrap();
1546
1547 assert_eq!(builder.group, Some("staging".to_string()));
1548
1549 let Err(err) = Client::builder(&endpoint).group("a") else {
1550 panic!("group should fail for strings under 2 bytes");
1551 };
1552 assert!(matches!(
1553 err.downcast_ref::<BuildError>(),
1554 Some(BuildError::InvalidGroup(ValidateNameError::TooShort))
1555 ));
1556
1557 let too_long_group = "👋".repeat(129);
1558 let Err(err) = Client::builder(&endpoint).group(&too_long_group) else {
1559 panic!("group should fail for strings over 128 bytes");
1560 };
1561 assert!(matches!(
1562 err.downcast_ref::<BuildError>(),
1563 Some(BuildError::InvalidGroup(ValidateNameError::TooLong))
1564 ));
1565 }
1566
1567 #[tokio::test]
1568 async fn test_attributes() {
1569 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1570
1571 let builder = Client::builder(&endpoint)
1573 .attributes(std::iter::empty::<(String, String)>())
1574 .unwrap();
1575 assert_eq!(builder.attributes.as_ref().map(|m| m.len()), Some(0));
1576
1577 let builder = Client::builder(&endpoint)
1579 .attributes([("env", "prod"), ("region", "us-west")])
1580 .unwrap();
1581 let attrs = builder.attributes.as_ref().expect("attributes set");
1582 assert_eq!(attrs.get("env").map(String::as_str), Some("prod"));
1583 assert_eq!(attrs.get("region").map(String::as_str), Some("us-west"));
1584
1585 let mut map: HashMap<String, String> = HashMap::new();
1587 map.insert("k1".into(), "v1".into());
1588 map.insert("k2".into(), "".into()); let builder = Client::builder(&endpoint).attributes(map).unwrap();
1590 let attrs = builder.attributes.as_ref().expect("attributes set");
1591 assert_eq!(attrs.get("k2").map(String::as_str), Some(""));
1592
1593 let too_long_value = "x".repeat(129);
1595 let Err(err) = Client::builder(&endpoint).attributes([("ok", too_long_value.as_str())])
1596 else {
1597 panic!("attributes should fail for value over 128 bytes");
1598 };
1599 assert!(matches!(
1600 err.downcast_ref::<BuildError>(),
1601 Some(BuildError::InvalidAttributes(
1602 ValidateAttributesError::ValueTooLong
1603 ))
1604 ));
1605
1606 let Err(err) = Client::builder(&endpoint).attributes([("a", "v")]) else {
1608 panic!("attributes should fail for key under 2 bytes");
1609 };
1610 assert!(matches!(
1611 err.downcast_ref::<BuildError>(),
1612 Some(BuildError::InvalidAttributes(
1613 ValidateAttributesError::InvalidKey(ValidateNameError::TooShort)
1614 ))
1615 ));
1616
1617 let big: Vec<(String, String)> = (0..(CLIENT_ATTRIBUTES_MAX_COUNT + 1))
1619 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1620 .collect();
1621 let Err(err) = Client::builder(&endpoint).attributes(big) else {
1622 panic!("attributes should fail for more than 128 entries");
1623 };
1624 assert!(matches!(
1625 err.downcast_ref::<BuildError>(),
1626 Some(BuildError::InvalidAttributes(
1627 ValidateAttributesError::TooManyEntries
1628 ))
1629 ));
1630 }
1631
1632 async fn build_serverless_client(seed: u64) -> Client {
1636 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(seed);
1637 let shared_secret = SecretKey::from_bytes(&rng.random());
1638 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1639 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1640
1641 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1642
1643 Client::builder(&endpoint)
1644 .disable_metrics_interval()
1645 .api_secret(api_secret)
1646 .unwrap()
1647 .build()
1648 .await
1649 .unwrap()
1650 }
1651
1652 #[tokio::test]
1655 async fn test_set_group_runtime_validation() {
1656 let client = build_serverless_client(2).await;
1657
1658 let err = client
1659 .set_group("a")
1660 .await
1661 .expect_err("too-short group should fail validation");
1662 assert!(matches!(
1663 err,
1664 Error::InvalidGroup(ValidateNameError::TooShort)
1665 ));
1666
1667 let too_long = "x".repeat(CLIENT_NAME_MAX_LENGTH + 1);
1668 let err = client
1669 .set_group(too_long)
1670 .await
1671 .expect_err("too-long group should fail validation");
1672 assert!(matches!(
1673 err,
1674 Error::InvalidGroup(ValidateNameError::TooLong)
1675 ));
1676 }
1677
1678 #[tokio::test]
1681 async fn test_set_attributes_runtime_validation() {
1682 let client = build_serverless_client(3).await;
1683
1684 let err = client
1686 .set_attributes([("a", "v")])
1687 .await
1688 .expect_err("too-short attribute key should fail validation");
1689 assert!(matches!(
1690 err,
1691 Error::InvalidAttributes(ValidateAttributesError::InvalidKey(
1692 ValidateNameError::TooShort
1693 ))
1694 ));
1695
1696 let too_long_value = "x".repeat(CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH + 1);
1698 let err = client
1699 .set_attributes([("ok", too_long_value.as_str())])
1700 .await
1701 .expect_err("too-long attribute value should fail validation");
1702 assert!(matches!(
1703 err,
1704 Error::InvalidAttributes(ValidateAttributesError::ValueTooLong)
1705 ));
1706
1707 let big: Vec<(String, String)> = (0..(CLIENT_ATTRIBUTES_MAX_COUNT + 1))
1709 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1710 .collect();
1711 let err = client
1712 .set_attributes(big)
1713 .await
1714 .expect_err("too many attributes should fail validation");
1715 assert!(matches!(
1716 err,
1717 Error::InvalidAttributes(ValidateAttributesError::TooManyEntries)
1718 ));
1719 }
1720
1721 #[tokio::test]
1722 async fn test_set_attribute_runtime_validation() {
1723 let client = build_serverless_client(7).await;
1724
1725 let err = client
1727 .set_attribute("a", "v")
1728 .await
1729 .expect_err("too-short attribute key should fail validation");
1730 assert!(matches!(
1731 err,
1732 Error::InvalidAttributes(ValidateAttributesError::InvalidKey(
1733 ValidateNameError::TooShort
1734 ))
1735 ));
1736
1737 let err = client
1741 .set_attribute("firmware", "2.1.0")
1742 .await
1743 .expect_err("no server: remote call must fail after validation passes");
1744 assert!(matches!(err, Error::Connect(_)), "got {err:?}");
1745 }
1746
1747 #[tokio::test]
1748 async fn test_set_attribute_merge_over_limit_rejected() {
1749 let full: Vec<(String, String)> = (0..CLIENT_ATTRIBUTES_MAX_COUNT)
1751 .map(|i| (format!("key_{i:04}"), "v".to_string()))
1752 .collect();
1753
1754 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(9);
1755 let shared_secret = SecretKey::from_bytes(&rng.random());
1756 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1757 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1758 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1759 let client = Client::builder(&endpoint)
1760 .disable_metrics_interval()
1761 .attributes(full)
1762 .unwrap()
1763 .api_secret(api_secret)
1764 .unwrap()
1765 .build()
1766 .await
1767 .unwrap();
1768
1769 let err = client
1773 .set_attribute("one-too-many", "v")
1774 .await
1775 .expect_err("merging past the attribute limit must fail");
1776 assert!(
1777 matches!(
1778 err,
1779 Error::InvalidAttributes(ValidateAttributesError::TooManyEntries)
1780 ),
1781 "expected TooManyEntries, got {err:?}"
1782 );
1783 }
1784
1785 #[tokio::test]
1790 async fn test_set_attributes_runtime_boundary_accepted() {
1791 let client = build_serverless_client(4).await;
1792
1793 let max_value = "x".repeat(CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH);
1795 let err = client
1796 .set_attributes([("ok".to_string(), max_value)])
1797 .await
1798 .expect_err("no server: remote call must fail after validation passes");
1799 assert!(
1800 matches!(err, Error::Connect(_)),
1801 "expected a connect error (validation accepted), got {err:?}"
1802 );
1803
1804 let max_entries: Vec<(String, String)> = (0..CLIENT_ATTRIBUTES_MAX_COUNT)
1806 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1807 .collect();
1808 let err = client
1809 .set_attributes(max_entries)
1810 .await
1811 .expect_err("no server: remote call must fail after validation passes");
1812 assert!(
1813 matches!(err, Error::Connect(_)),
1814 "expected a connect error (validation accepted), got {err:?}"
1815 );
1816 }
1817}