zombienet_configuration/shared/
node.rs

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/// An environment variable with a name and a value.
30/// It can be constructed from a `(&str, &str)`.
31///
32/// # Examples:
33///
34/// ```
35/// use zombienet_configuration::shared::node::EnvVar;
36///
37/// let simple_var: EnvVar = ("FOO", "BAR").into();
38///
39/// assert_eq!(
40///     simple_var,
41///     EnvVar {
42///         name: "FOO".into(),
43///         value: "BAR".into()
44///     }
45/// )
46/// ```
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct EnvVar {
49    /// The name of the environment variable.
50    pub name: String,
51
52    /// The value of the environment variable.
53    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/// A node configuration, with fine-grained configuration options.
66#[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    // used to skip serialization of fields with defaults to avoid duplication
96    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    /// Node name (should be unique).
173    pub fn name(&self) -> &str {
174        &self.name
175    }
176
177    /// Image to run (only podman/k8s).
178    pub fn image(&self) -> Option<&Image> {
179        self.image.as_ref()
180    }
181
182    /// Command to run the node.
183    pub fn command(&self) -> Option<&Command> {
184        self.command.as_ref()
185    }
186
187    /// Subcommand to run the node.
188    pub fn subcommand(&self) -> Option<&Command> {
189        self.subcommand.as_ref()
190    }
191
192    /// Arguments to use for node.
193    pub fn args(&self) -> Vec<&Arg> {
194        self.args.iter().collect()
195    }
196
197    /// Arguments to use for node.
198    pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
199        self.args = args;
200    }
201
202    /// Whether the node is a validator.
203    pub fn is_validator(&self) -> bool {
204        self.is_validator
205    }
206
207    /// Whether the node keys must be added to invulnerables.
208    pub fn is_invulnerable(&self) -> bool {
209        self.is_invulnerable
210    }
211
212    /// Whether the node is a bootnode.
213    pub fn is_bootnode(&self) -> bool {
214        self.is_bootnode
215    }
216
217    /// Node initial balance present in genesis.
218    pub fn initial_balance(&self) -> u128 {
219        self.initial_balance.0
220    }
221
222    /// Environment variables to set (inside pod for podman/k8s, inside shell for native).
223    pub fn env(&self) -> Vec<&EnvVar> {
224        self.env.iter().collect()
225    }
226
227    /// List of node's bootnodes addresses to use.
228    pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
229        self.bootnodes_addresses.iter().collect()
230    }
231
232    /// Default resources.
233    pub fn resources(&self) -> Option<&Resources> {
234        self.resources.as_ref()
235    }
236
237    /// Websocket port to use.
238    pub fn ws_port(&self) -> Option<u16> {
239        self.ws_port
240    }
241
242    /// RPC port to use.
243    pub fn rpc_port(&self) -> Option<u16> {
244        self.rpc_port
245    }
246
247    /// Prometheus port to use.
248    pub fn prometheus_port(&self) -> Option<u16> {
249        self.prometheus_port
250    }
251
252    /// P2P port to use.
253    pub fn p2p_port(&self) -> Option<u16> {
254        self.p2p_port
255    }
256
257    /// `libp2p` cert hash to use with `WebRTC` transport.
258    pub fn p2p_cert_hash(&self) -> Option<&str> {
259        self.p2p_cert_hash.as_deref()
260    }
261
262    /// Database snapshot.
263    pub fn db_snapshot(&self) -> Option<&AssetLocation> {
264        self.db_snapshot.as_ref()
265    }
266}
267
268/// A node configuration builder, used to build a [`NodeConfig`] declaratively with fields validation.
269pub 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    /// Set the name of the node.
343    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                    // we still set the name in error case to display error path
358                    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    /// Set the command that will be executed to launch the node. Override the default.
370    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    /// Set the subcommand that will be executed to launch the node.
393    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    /// Set the image that will be used for the node (only podman/k8s). Override the default.
416    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    /// Set the arguments that will be used when launching the node. Override the default.
439    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    /// Set whether the node is a validator.
451    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    /// Set whether the node is invulnerable.
463    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    /// Set whether the node is a bootnode.
475    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    /// Set the node initial balance.
487    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    /// Set the node environment variables that will be used when launched. Override the default.
499    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    /// Set the bootnodes addresses that the node will try to connect to. Override the default.
510    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    /// Set the resources limits what will be used for the node (only podman/k8s). Override the default.
538    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    /// Set the websocket port that will be exposed. Uniqueness across config will be checked.
563    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    /// Set the RPC port that will be exposed. Uniqueness across config will be checked.
582    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    /// Set the Prometheus port that will be exposed for metrics. Uniqueness across config will be checked.
601    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    /// Set the P2P port that will be exposed. Uniqueness across config will be checked.
620    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    /// Set the P2P cert hash that will be used as part of the multiaddress
639    /// if and only if the multiaddress is set to use `webrtc`.
640    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    /// Set the database snapshot that will be used to launch the node. Override the default.
652    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    /// Seals the builder and returns a [`NodeConfig`] if there are no validation errors, else returns errors.
664    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}