1use std::{cell::RefCell, error::Error, fmt::Display, marker::PhantomData, rc::Rc};
2
3use multiaddr::Multiaddr;
4use serde::{ser::SerializeStruct, Deserialize, Serialize};
5
6use super::{
7 errors::FieldError,
8 helpers::{
9 ensure_port_unique, ensure_value_is_not_empty, generate_unique_node_name, merge_errors,
10 merge_errors_vecs,
11 },
12 macros::states,
13 resources::ResourcesBuilder,
14 types::{AssetLocation, ChainDefaultContext, Command, Image, ValidationContext, U128},
15};
16use crate::{
17 shared::{
18 resources::Resources,
19 types::{Arg, Port},
20 },
21 utils::{default_as_true, default_initial_balance},
22};
23
24states! {
25 Buildable,
26 Initial
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct EnvVar {
49 pub name: String,
51
52 pub value: String,
54}
55
56impl From<(&str, &str)> for EnvVar {
57 fn from((name, value): (&str, &str)) -> Self {
58 Self {
59 name: name.to_owned(),
60 value: value.to_owned(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Deserialize)]
67pub struct NodeConfig {
68 pub(crate) name: String,
69 pub(crate) image: Option<Image>,
70 pub(crate) command: Option<Command>,
71 pub(crate) subcommand: Option<Command>,
72 #[serde(default)]
73 args: Vec<Arg>,
74 #[serde(alias = "validator", default = "default_as_true")]
75 pub(crate) is_validator: bool,
76 #[serde(alias = "invulnerable", default = "default_as_true")]
77 pub(crate) is_invulnerable: bool,
78 #[serde(alias = "bootnode", default)]
79 pub(crate) is_bootnode: bool,
80 #[serde(alias = "balance")]
81 #[serde(default = "default_initial_balance")]
82 initial_balance: U128,
83 #[serde(default)]
84 env: Vec<EnvVar>,
85 #[serde(default)]
86 bootnodes_addresses: Vec<Multiaddr>,
87 pub(crate) resources: Option<Resources>,
88 ws_port: Option<Port>,
89 rpc_port: Option<Port>,
90 prometheus_port: Option<Port>,
91 p2p_port: Option<Port>,
92 p2p_cert_hash: Option<String>,
93 pub(crate) db_snapshot: Option<AssetLocation>,
94 #[serde(default)]
95 pub(crate) chain_context: ChainDefaultContext,
97}
98
99impl Serialize for NodeConfig {
100 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
101 where
102 S: serde::Serializer,
103 {
104 let mut state = serializer.serialize_struct("NodeConfig", 18)?;
105 state.serialize_field("name", &self.name)?;
106
107 if self.image == self.chain_context.default_image {
108 state.skip_field("image")?;
109 } else {
110 state.serialize_field("image", &self.image)?;
111 }
112
113 if self.command == self.chain_context.default_command {
114 state.skip_field("command")?;
115 } else {
116 state.serialize_field("command", &self.command)?;
117 }
118
119 if self.subcommand.is_none() {
120 state.skip_field("subcommand")?;
121 } else {
122 state.serialize_field("subcommand", &self.subcommand)?;
123 }
124
125 if self.args.is_empty() || self.args == self.chain_context.default_args {
126 state.skip_field("args")?;
127 } else {
128 state.serialize_field("args", &self.args)?;
129 }
130
131 state.serialize_field("validator", &self.is_validator)?;
132 state.serialize_field("invulnerable", &self.is_invulnerable)?;
133 state.serialize_field("bootnode", &self.is_bootnode)?;
134 state.serialize_field("balance", &self.initial_balance)?;
135
136 if self.env.is_empty() {
137 state.skip_field("env")?;
138 } else {
139 state.serialize_field("env", &self.env)?;
140 }
141
142 if self.bootnodes_addresses.is_empty() {
143 state.skip_field("bootnodes_addresses")?;
144 } else {
145 state.serialize_field("bootnodes_addresses", &self.bootnodes_addresses)?;
146 }
147
148 if self.resources == self.chain_context.default_resources {
149 state.skip_field("resources")?;
150 } else {
151 state.serialize_field("resources", &self.resources)?;
152 }
153
154 state.serialize_field("ws_port", &self.ws_port)?;
155 state.serialize_field("rpc_port", &self.rpc_port)?;
156 state.serialize_field("prometheus_port", &self.prometheus_port)?;
157 state.serialize_field("p2p_port", &self.p2p_port)?;
158 state.serialize_field("p2p_cert_hash", &self.p2p_cert_hash)?;
159
160 if self.db_snapshot == self.chain_context.default_db_snapshot {
161 state.skip_field("db_snapshot")?;
162 } else {
163 state.serialize_field("db_snapshot", &self.db_snapshot)?;
164 }
165
166 state.skip_field("chain_context")?;
167 state.end()
168 }
169}
170
171impl NodeConfig {
172 pub fn name(&self) -> &str {
174 &self.name
175 }
176
177 pub fn image(&self) -> Option<&Image> {
179 self.image.as_ref()
180 }
181
182 pub fn command(&self) -> Option<&Command> {
184 self.command.as_ref()
185 }
186
187 pub fn subcommand(&self) -> Option<&Command> {
189 self.subcommand.as_ref()
190 }
191
192 pub fn args(&self) -> Vec<&Arg> {
194 self.args.iter().collect()
195 }
196
197 pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
199 self.args = args;
200 }
201
202 pub fn is_validator(&self) -> bool {
204 self.is_validator
205 }
206
207 pub fn is_invulnerable(&self) -> bool {
209 self.is_invulnerable
210 }
211
212 pub fn is_bootnode(&self) -> bool {
214 self.is_bootnode
215 }
216
217 pub fn initial_balance(&self) -> u128 {
219 self.initial_balance.0
220 }
221
222 pub fn env(&self) -> Vec<&EnvVar> {
224 self.env.iter().collect()
225 }
226
227 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
229 self.bootnodes_addresses.iter().collect()
230 }
231
232 pub fn resources(&self) -> Option<&Resources> {
234 self.resources.as_ref()
235 }
236
237 pub fn ws_port(&self) -> Option<u16> {
239 self.ws_port
240 }
241
242 pub fn rpc_port(&self) -> Option<u16> {
244 self.rpc_port
245 }
246
247 pub fn prometheus_port(&self) -> Option<u16> {
249 self.prometheus_port
250 }
251
252 pub fn p2p_port(&self) -> Option<u16> {
254 self.p2p_port
255 }
256
257 pub fn p2p_cert_hash(&self) -> Option<&str> {
259 self.p2p_cert_hash.as_deref()
260 }
261
262 pub fn db_snapshot(&self) -> Option<&AssetLocation> {
264 self.db_snapshot.as_ref()
265 }
266}
267
268pub struct NodeConfigBuilder<S> {
270 config: NodeConfig,
271 validation_context: Rc<RefCell<ValidationContext>>,
272 errors: Vec<anyhow::Error>,
273 _state: PhantomData<S>,
274}
275
276impl Default for NodeConfigBuilder<Initial> {
277 fn default() -> Self {
278 Self {
279 config: NodeConfig {
280 name: "".into(),
281 image: None,
282 command: None,
283 subcommand: None,
284 args: vec![],
285 is_validator: true,
286 is_invulnerable: true,
287 is_bootnode: false,
288 initial_balance: 2_000_000_000_000.into(),
289 env: vec![],
290 bootnodes_addresses: vec![],
291 resources: None,
292 ws_port: None,
293 rpc_port: None,
294 prometheus_port: None,
295 p2p_port: None,
296 p2p_cert_hash: None,
297 db_snapshot: None,
298 chain_context: Default::default(),
299 },
300 validation_context: Default::default(),
301 errors: vec![],
302 _state: PhantomData,
303 }
304 }
305}
306
307impl<A> NodeConfigBuilder<A> {
308 fn transition<B>(
309 config: NodeConfig,
310 validation_context: Rc<RefCell<ValidationContext>>,
311 errors: Vec<anyhow::Error>,
312 ) -> NodeConfigBuilder<B> {
313 NodeConfigBuilder {
314 config,
315 validation_context,
316 errors,
317 _state: PhantomData,
318 }
319 }
320}
321
322impl NodeConfigBuilder<Initial> {
323 pub fn new(
324 chain_context: ChainDefaultContext,
325 validation_context: Rc<RefCell<ValidationContext>>,
326 ) -> Self {
327 Self::transition(
328 NodeConfig {
329 command: chain_context.default_command.clone(),
330 image: chain_context.default_image.clone(),
331 resources: chain_context.default_resources.clone(),
332 db_snapshot: chain_context.default_db_snapshot.clone(),
333 args: chain_context.default_args.clone(),
334 chain_context,
335 ..Self::default().config
336 },
337 validation_context,
338 vec![],
339 )
340 }
341
342 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> NodeConfigBuilder<Buildable> {
344 let name: String = generate_unique_node_name(name, self.validation_context.clone());
345
346 match ensure_value_is_not_empty(&name) {
347 Ok(_) => Self::transition(
348 NodeConfig {
349 name,
350 ..self.config
351 },
352 self.validation_context,
353 self.errors,
354 ),
355 Err(e) => Self::transition(
356 NodeConfig {
357 name,
359 ..self.config
360 },
361 self.validation_context,
362 merge_errors(self.errors, FieldError::Name(e).into()),
363 ),
364 }
365 }
366}
367
368impl NodeConfigBuilder<Buildable> {
369 pub fn with_command<T>(self, command: T) -> Self
371 where
372 T: TryInto<Command>,
373 T::Error: Error + Send + Sync + 'static,
374 {
375 match command.try_into() {
376 Ok(command) => Self::transition(
377 NodeConfig {
378 command: Some(command),
379 ..self.config
380 },
381 self.validation_context,
382 self.errors,
383 ),
384 Err(error) => Self::transition(
385 self.config,
386 self.validation_context,
387 merge_errors(self.errors, FieldError::Command(error.into()).into()),
388 ),
389 }
390 }
391
392 pub fn with_subcommand<T>(self, subcommand: T) -> Self
394 where
395 T: TryInto<Command>,
396 T::Error: Error + Send + Sync + 'static,
397 {
398 match subcommand.try_into() {
399 Ok(subcommand) => Self::transition(
400 NodeConfig {
401 subcommand: Some(subcommand),
402 ..self.config
403 },
404 self.validation_context,
405 self.errors,
406 ),
407 Err(error) => Self::transition(
408 self.config,
409 self.validation_context,
410 merge_errors(self.errors, FieldError::Command(error.into()).into()),
411 ),
412 }
413 }
414
415 pub fn with_image<T>(self, image: T) -> Self
417 where
418 T: TryInto<Image>,
419 T::Error: Error + Send + Sync + 'static,
420 {
421 match image.try_into() {
422 Ok(image) => Self::transition(
423 NodeConfig {
424 image: Some(image),
425 ..self.config
426 },
427 self.validation_context,
428 self.errors,
429 ),
430 Err(error) => Self::transition(
431 self.config,
432 self.validation_context,
433 merge_errors(self.errors, FieldError::Image(error.into()).into()),
434 ),
435 }
436 }
437
438 pub fn with_args(self, args: Vec<Arg>) -> Self {
440 Self::transition(
441 NodeConfig {
442 args,
443 ..self.config
444 },
445 self.validation_context,
446 self.errors,
447 )
448 }
449
450 pub fn validator(self, choice: bool) -> Self {
452 Self::transition(
453 NodeConfig {
454 is_validator: choice,
455 ..self.config
456 },
457 self.validation_context,
458 self.errors,
459 )
460 }
461
462 pub fn invulnerable(self, choice: bool) -> Self {
464 Self::transition(
465 NodeConfig {
466 is_invulnerable: choice,
467 ..self.config
468 },
469 self.validation_context,
470 self.errors,
471 )
472 }
473
474 pub fn bootnode(self, choice: bool) -> Self {
476 Self::transition(
477 NodeConfig {
478 is_bootnode: choice,
479 ..self.config
480 },
481 self.validation_context,
482 self.errors,
483 )
484 }
485
486 pub fn with_initial_balance(self, initial_balance: u128) -> Self {
488 Self::transition(
489 NodeConfig {
490 initial_balance: initial_balance.into(),
491 ..self.config
492 },
493 self.validation_context,
494 self.errors,
495 )
496 }
497
498 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
500 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
501
502 Self::transition(
503 NodeConfig { env, ..self.config },
504 self.validation_context,
505 self.errors,
506 )
507 }
508
509 pub fn with_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
511 where
512 T: TryInto<Multiaddr> + Display + Copy,
513 T::Error: Error + Send + Sync + 'static,
514 {
515 let mut addrs = vec![];
516 let mut errors = vec![];
517
518 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
519 match addr.try_into() {
520 Ok(addr) => addrs.push(addr),
521 Err(error) => errors.push(
522 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
523 ),
524 }
525 }
526
527 Self::transition(
528 NodeConfig {
529 bootnodes_addresses: addrs,
530 ..self.config
531 },
532 self.validation_context,
533 merge_errors_vecs(self.errors, errors),
534 )
535 }
536
537 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
539 match f(ResourcesBuilder::new()).build() {
540 Ok(resources) => Self::transition(
541 NodeConfig {
542 resources: Some(resources),
543 ..self.config
544 },
545 self.validation_context,
546 self.errors,
547 ),
548 Err(errors) => Self::transition(
549 self.config,
550 self.validation_context,
551 merge_errors_vecs(
552 self.errors,
553 errors
554 .into_iter()
555 .map(|error| FieldError::Resources(error).into())
556 .collect::<Vec<_>>(),
557 ),
558 ),
559 }
560 }
561
562 pub fn with_ws_port(self, ws_port: Port) -> Self {
564 match ensure_port_unique(ws_port, self.validation_context.clone()) {
565 Ok(_) => Self::transition(
566 NodeConfig {
567 ws_port: Some(ws_port),
568 ..self.config
569 },
570 self.validation_context,
571 self.errors,
572 ),
573 Err(error) => Self::transition(
574 self.config,
575 self.validation_context,
576 merge_errors(self.errors, FieldError::WsPort(error).into()),
577 ),
578 }
579 }
580
581 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
583 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
584 Ok(_) => Self::transition(
585 NodeConfig {
586 rpc_port: Some(rpc_port),
587 ..self.config
588 },
589 self.validation_context,
590 self.errors,
591 ),
592 Err(error) => Self::transition(
593 self.config,
594 self.validation_context,
595 merge_errors(self.errors, FieldError::RpcPort(error).into()),
596 ),
597 }
598 }
599
600 pub fn with_prometheus_port(self, prometheus_port: Port) -> Self {
602 match ensure_port_unique(prometheus_port, self.validation_context.clone()) {
603 Ok(_) => Self::transition(
604 NodeConfig {
605 prometheus_port: Some(prometheus_port),
606 ..self.config
607 },
608 self.validation_context,
609 self.errors,
610 ),
611 Err(error) => Self::transition(
612 self.config,
613 self.validation_context,
614 merge_errors(self.errors, FieldError::PrometheusPort(error).into()),
615 ),
616 }
617 }
618
619 pub fn with_p2p_port(self, p2p_port: Port) -> Self {
621 match ensure_port_unique(p2p_port, self.validation_context.clone()) {
622 Ok(_) => Self::transition(
623 NodeConfig {
624 p2p_port: Some(p2p_port),
625 ..self.config
626 },
627 self.validation_context,
628 self.errors,
629 ),
630 Err(error) => Self::transition(
631 self.config,
632 self.validation_context,
633 merge_errors(self.errors, FieldError::P2pPort(error).into()),
634 ),
635 }
636 }
637
638 pub fn with_p2p_cert_hash(self, p2p_cert_hash: impl Into<String>) -> Self {
641 Self::transition(
642 NodeConfig {
643 p2p_cert_hash: Some(p2p_cert_hash.into()),
644 ..self.config
645 },
646 self.validation_context,
647 self.errors,
648 )
649 }
650
651 pub fn with_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
653 Self::transition(
654 NodeConfig {
655 db_snapshot: Some(location.into()),
656 ..self.config
657 },
658 self.validation_context,
659 self.errors,
660 )
661 }
662
663 pub fn build(self) -> Result<NodeConfig, (String, Vec<anyhow::Error>)> {
665 if !self.errors.is_empty() {
666 return Err((self.config.name.clone(), self.errors));
667 }
668
669 Ok(self.config)
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use std::collections::HashSet;
676
677 use super::*;
678
679 #[test]
680 fn node_config_builder_should_succeeds_and_returns_a_node_config() {
681 let node_config =
682 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
683 .with_name("node")
684 .with_command("mycommand")
685 .with_image("myrepo:myimage")
686 .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
687 .validator(true)
688 .invulnerable(true)
689 .bootnode(true)
690 .with_initial_balance(100_000_042)
691 .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
692 .with_bootnodes_addresses(vec![
693 "/ip4/10.41.122.55/tcp/45421",
694 "/ip4/51.144.222.10/tcp/2333",
695 ])
696 .with_resources(|resources| {
697 resources
698 .with_request_cpu("200M")
699 .with_request_memory("500M")
700 .with_limit_cpu("1G")
701 .with_limit_memory("2G")
702 })
703 .with_ws_port(5000)
704 .with_rpc_port(6000)
705 .with_prometheus_port(7000)
706 .with_p2p_port(8000)
707 .with_p2p_cert_hash(
708 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
709 )
710 .with_db_snapshot("/tmp/mysnapshot")
711 .build()
712 .unwrap();
713
714 assert_eq!(node_config.name(), "node");
715 assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
716 assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
717 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
718 assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
719 assert!(node_config.is_validator());
720 assert!(node_config.is_invulnerable());
721 assert!(node_config.is_bootnode());
722 assert_eq!(node_config.initial_balance(), 100_000_042);
723 let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
724 assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
725 let bootnodes_addresses: Vec<Multiaddr> = vec![
726 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
727 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
728 ];
729 assert_eq!(
730 node_config.bootnodes_addresses(),
731 bootnodes_addresses.iter().collect::<Vec<_>>()
732 );
733 let resources = node_config.resources().unwrap();
734 assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
735 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
736 assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
737 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
738 assert_eq!(node_config.ws_port().unwrap(), 5000);
739 assert_eq!(node_config.rpc_port().unwrap(), 6000);
740 assert_eq!(node_config.prometheus_port().unwrap(), 7000);
741 assert_eq!(node_config.p2p_port().unwrap(), 8000);
742 assert_eq!(
743 node_config.p2p_cert_hash().unwrap(),
744 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
745 );
746 assert!(matches!(
747 node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
748 ));
749 }
750
751 #[test]
752 fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
753 let mut used_nodes_names = HashSet::new();
754 used_nodes_names.insert("mynode".into());
755 let validation_context = Rc::new(RefCell::new(ValidationContext {
756 used_nodes_names,
757 ..Default::default()
758 }));
759 let node_config =
760 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
761 .with_name("mynode")
762 .build()
763 .unwrap();
764
765 assert_eq!(node_config.name, "mynode-1");
766 }
767
768 #[test]
769 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
770 let (node_name, errors) =
771 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
772 .with_name("node")
773 .with_command("invalid command")
774 .build()
775 .unwrap_err();
776
777 assert_eq!(node_name, "node");
778 assert_eq!(errors.len(), 1);
779 assert_eq!(
780 errors.first().unwrap().to_string(),
781 "command: 'invalid command' shouldn't contains whitespace"
782 );
783 }
784
785 #[test]
786 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
787 let (node_name, errors) =
788 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
789 .with_name("node")
790 .with_image("myinvalid.image")
791 .build()
792 .unwrap_err();
793
794 assert_eq!(node_name, "node");
795 assert_eq!(errors.len(), 1);
796 assert_eq!(
797 errors.first().unwrap().to_string(),
798 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
799 );
800 }
801
802 #[test]
803 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
804 ) {
805 let (node_name, errors) =
806 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
807 .with_name("node")
808 .with_bootnodes_addresses(vec!["/ip4//tcp/45421"])
809 .build()
810 .unwrap_err();
811
812 assert_eq!(node_name, "node");
813 assert_eq!(errors.len(), 1);
814 assert_eq!(
815 errors.first().unwrap().to_string(),
816 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
817 );
818 }
819
820 #[test]
821 fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
822 ) {
823 let (node_name, errors) =
824 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
825 .with_name("node")
826 .with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
827 .build()
828 .unwrap_err();
829
830 assert_eq!(node_name, "node");
831 assert_eq!(errors.len(), 2);
832 assert_eq!(
833 errors.first().unwrap().to_string(),
834 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
835 );
836 assert_eq!(
837 errors.get(1).unwrap().to_string(),
838 "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
839 );
840 }
841
842 #[test]
843 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
844 ) {
845 let (node_name, errors) =
846 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
847 .with_name("node")
848 .with_resources(|resources| resources.with_limit_cpu("invalid"))
849 .build()
850 .unwrap_err();
851
852 assert_eq!(node_name, "node");
853 assert_eq!(errors.len(), 1);
854 assert_eq!(
855 errors.first().unwrap().to_string(),
856 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
857 );
858 }
859
860 #[test]
861 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
862 ) {
863 let (node_name, errors) =
864 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
865 .with_name("node")
866 .with_resources(|resources| {
867 resources
868 .with_limit_cpu("invalid")
869 .with_request_memory("invalid")
870 })
871 .build()
872 .unwrap_err();
873
874 assert_eq!(node_name, "node");
875 assert_eq!(errors.len(), 2);
876 assert_eq!(
877 errors.first().unwrap().to_string(),
878 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
879 );
880 assert_eq!(
881 errors.get(1).unwrap().to_string(),
882 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
883 );
884 }
885
886 #[test]
887 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
888 ) {
889 let (node_name, errors) =
890 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
891 .with_name("node")
892 .with_command("invalid command")
893 .with_image("myinvalid.image")
894 .with_resources(|resources| {
895 resources
896 .with_limit_cpu("invalid")
897 .with_request_memory("invalid")
898 })
899 .build()
900 .unwrap_err();
901
902 assert_eq!(node_name, "node");
903 assert_eq!(errors.len(), 4);
904 assert_eq!(
905 errors.first().unwrap().to_string(),
906 "command: 'invalid command' shouldn't contains whitespace"
907 );
908 assert_eq!(
909 errors.get(1).unwrap().to_string(),
910 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
911 );
912 assert_eq!(
913 errors.get(2).unwrap().to_string(),
914 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
915 );
916 assert_eq!(
917 errors.get(3).unwrap().to_string(),
918 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
919 );
920 }
921
922 #[test]
923 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
924 ) {
925 let validation_context = Rc::new(RefCell::new(ValidationContext {
926 used_ports: vec![30333],
927 ..Default::default()
928 }));
929 let (node_name, errors) =
930 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
931 .with_name("node")
932 .with_ws_port(30333)
933 .build()
934 .unwrap_err();
935
936 assert_eq!(node_name, "node");
937 assert_eq!(errors.len(), 1);
938 assert_eq!(
939 errors.first().unwrap().to_string(),
940 "ws_port: '30333' is already used across config"
941 );
942 }
943
944 #[test]
945 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
946 ) {
947 let validation_context = Rc::new(RefCell::new(ValidationContext {
948 used_ports: vec![4444],
949 ..Default::default()
950 }));
951 let (node_name, errors) =
952 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
953 .with_name("node")
954 .with_rpc_port(4444)
955 .build()
956 .unwrap_err();
957
958 assert_eq!(node_name, "node");
959 assert_eq!(errors.len(), 1);
960 assert_eq!(
961 errors.first().unwrap().to_string(),
962 "rpc_port: '4444' is already used across config"
963 );
964 }
965
966 #[test]
967 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
968 ) {
969 let validation_context = Rc::new(RefCell::new(ValidationContext {
970 used_ports: vec![9089],
971 ..Default::default()
972 }));
973 let (node_name, errors) =
974 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
975 .with_name("node")
976 .with_prometheus_port(9089)
977 .build()
978 .unwrap_err();
979
980 assert_eq!(node_name, "node");
981 assert_eq!(errors.len(), 1);
982 assert_eq!(
983 errors.first().unwrap().to_string(),
984 "prometheus_port: '9089' is already used across config"
985 );
986 }
987
988 #[test]
989 fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
990 ) {
991 let validation_context = Rc::new(RefCell::new(ValidationContext {
992 used_ports: vec![45093],
993 ..Default::default()
994 }));
995 let (node_name, errors) =
996 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
997 .with_name("node")
998 .with_p2p_port(45093)
999 .build()
1000 .unwrap_err();
1001
1002 assert_eq!(node_name, "node");
1003 assert_eq!(errors.len(), 1);
1004 assert_eq!(
1005 errors.first().unwrap().to_string(),
1006 "p2p_port: '45093' is already used across config"
1007 );
1008 }
1009
1010 #[test]
1011 fn node_config_builder_should_fails_if_node_name_is_empty() {
1012 let validation_context = Rc::new(RefCell::new(ValidationContext {
1013 ..Default::default()
1014 }));
1015
1016 let (_, errors) =
1017 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1018 .with_name("")
1019 .build()
1020 .unwrap_err();
1021
1022 assert_eq!(errors.len(), 1);
1023 assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1024 }
1025}