diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/portal.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/portal.rs index 5fe1edd60cf..90e3e60b85a 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/portal.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/portal.rs @@ -91,6 +91,7 @@ pub struct InletStatus<'a> { #[b(3)] pub alias: Cow<'a, str>, /// An optional status payload #[b(4)] pub payload: Option>, + #[b(5)] pub outlet_route: Cow<'a, str>, } impl<'a> InletStatus<'a> { @@ -102,6 +103,7 @@ impl<'a> InletStatus<'a> { worker_addr: "".into(), alias: "".into(), payload: Some(reason.into()), + outlet_route: "".into(), } } @@ -110,6 +112,7 @@ impl<'a> InletStatus<'a> { worker_addr: impl Into>, alias: impl Into>, payload: impl Into>>, + outlet_route: impl Into>, ) -> Self { Self { #[cfg(feature = "tag")] @@ -118,6 +121,7 @@ impl<'a> InletStatus<'a> { worker_addr: worker_addr.into(), alias: alias.into(), payload: payload.into(), + outlet_route: outlet_route.into(), } } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs index a92d7c0c8d8..82a82014bb0 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/services.rs @@ -192,3 +192,44 @@ impl<'a> StartCredentialsService<'a> { self.oneway } } + +#[derive(Debug, Clone, Decode, Encode)] +#[rustfmt::skip] +#[cbor(map)] +pub struct ServiceStatus<'a> { + #[cfg(feature = "tag")] + #[n(0)] tag: TypeTag<8542064>, + #[n(2)] pub addr: Cow<'a, str>, + #[n(3)] pub service_type: Cow<'a, str>, +} + +impl<'a> ServiceStatus<'a> { + pub fn new(addr: impl Into>, service_type: impl Into>) -> Self { + Self { + #[cfg(feature = "tag")] + tag: TypeTag, + addr: addr.into(), + service_type: service_type.into(), + } + } +} + +/// Response body for listing services +#[derive(Debug, Clone, Decode, Encode)] +#[rustfmt::skip] +#[cbor(map)] +pub struct ServiceList<'a> { + #[cfg(feature = "tag")] + #[n(0)] tag: TypeTag<9587601>, + #[n(1)] pub list: Vec> +} + +impl<'a> ServiceList<'a> { + pub fn new(list: Vec>) -> Self { + Self { + #[cfg(feature = "tag")] + tag: TypeTag, + list, + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/registry.rs b/implementations/rust/ockam/ockam_api/src/nodes/registry.rs index 84ce1d65329..e45fe61e9a9 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/registry.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/registry.rs @@ -101,10 +101,15 @@ pub(crate) struct AuthenticatorServiceInfo {} pub(crate) struct InletInfo { pub(crate) bind_addr: String, pub(crate) worker_addr: Address, + pub(crate) outlet_route: Route, } impl InletInfo { - pub(crate) fn new(bind_addr: &str, worker_addr: Option<&Address>) -> Self { + pub(crate) fn new( + bind_addr: &str, + worker_addr: Option<&Address>, + outlet_route: &Route, + ) -> Self { let worker_addr = match worker_addr { Some(addr) => addr.clone(), None => Address::from_string(""), @@ -112,6 +117,7 @@ impl InletInfo { Self { bind_addr: bind_addr.to_owned(), worker_addr, + outlet_route: outlet_route.to_owned(), } } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service.rs b/implementations/rust/ockam/ockam_api/src/nodes/service.rs index 15f6e92fc77..679e0a2c70f 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service.rs @@ -435,6 +435,7 @@ impl NodeManager { .start_credentials_service(ctx, req, dec) .await? .to_vec()?, + (Get, ["node", "services"]) => self.list_services(req).to_vec()?, // ==*== Forwarder commands ==*== (Post, ["node", "forwarder"]) => self.create_forwarder(ctx, req, dec).await?, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs index 7d90855e30f..9516062645b 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs @@ -28,7 +28,7 @@ impl NodeManager { info.worker_addr.to_string(), alias, None, - // FIXME route.as_ref().map(|r| r.to_string().into()), + info.outlet_route.to_string(), ) }) .collect(), @@ -41,13 +41,7 @@ impl NodeManager { .outlets .iter() .map(|(alias, info)| { - OutletStatus::new( - &info.tcp_addr, - info.worker_addr.to_string(), - alias, - None, - // FIXME route.as_ref().map(|r| r.to_string().into()), - ) + OutletStatus::new(&info.tcp_addr, info.worker_addr.to_string(), alias, None) }) .collect(), )) @@ -81,7 +75,7 @@ impl NodeManager { }; let access_control = self.access_control(check_credential)?; - let options = InletOptions::new(bind_addr.clone(), outlet_route, access_control); + let options = InletOptions::new(bind_addr.clone(), outlet_route.clone(), access_control); let res = self.tcp_transport.create_inlet_extended(options).await; @@ -90,7 +84,7 @@ impl NodeManager { // TODO: Use better way to store inlets? self.registry.inlets.insert( alias.clone(), - InletInfo::new(&bind_addr, Some(&worker_addr)), + InletInfo::new(&bind_addr, Some(&worker_addr), &outlet_route), ); Response::ok(req.id()).body(InletStatus::new( @@ -98,19 +92,22 @@ impl NodeManager { worker_addr.to_string(), alias, None, + outlet_route.to_string(), )) } Err(e) => { // TODO: Use better way to store inlets? - self.registry - .inlets - .insert(alias.clone(), InletInfo::new(&bind_addr, None)); + self.registry.inlets.insert( + alias.clone(), + InletInfo::new(&bind_addr, None, &outlet_route), + ); Response::bad_request(req.id()).body(InletStatus::new( bind_addr, "", alias, Some(e.to_string().into()), + outlet_route.to_string(), )) } }) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/services.rs index ee302a4b782..dcb9fdf2ddd 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/services.rs @@ -3,9 +3,9 @@ use crate::echoer::Echoer; use crate::error::ApiError; use crate::identity::IdentityService; use crate::nodes::models::services::{ - StartAuthenticatedServiceRequest, StartAuthenticatorRequest, StartCredentialsService, - StartEchoerServiceRequest, StartIdentityServiceRequest, StartUppercaseServiceRequest, - StartVaultServiceRequest, StartVerifierService, + ServiceList, ServiceStatus, StartAuthenticatedServiceRequest, StartAuthenticatorRequest, + StartCredentialsService, StartEchoerServiceRequest, StartIdentityServiceRequest, + StartUppercaseServiceRequest, StartVaultServiceRequest, StartVerifierService, }; use crate::nodes::registry::{CredentialsServiceInfo, VerifierServiceInfo}; use crate::nodes::NodeManager; @@ -323,4 +323,47 @@ impl NodeManager { .insert(addr, AuthenticatorServiceInfo::default()); Ok(()) } + + pub(super) fn list_services(&mut self, req: &Request<'_>) -> ResponseBuilder { + let mut list = Vec::new(); + + self.registry + .vault_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "vault"))); + self.registry + .identity_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "identity"))); + self.registry + .authenticated_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "authenticated"))); + self.registry + .uppercase_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "uppercase"))); + self.registry + .echoer_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "echoer"))); + self.registry + .verifier_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "verifier"))); + self.registry + .credentials_services + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "credentials"))); + + #[cfg(feature = "direct-authenticator")] + self.registry + .authenticator_service + .keys() + .for_each(|addr| list.push(ServiceStatus::new(addr.address(), "authenticator"))); + + let res_body = ServiceList::new(list); + + Response::ok(req.id()).body(res_body) + } } diff --git a/implementations/rust/ockam/ockam_api/src/util.rs b/implementations/rust/ockam/ockam_api/src/util.rs index 4b737fc7556..d7198f7e351 100644 --- a/implementations/rust/ockam/ockam_api/src/util.rs +++ b/implementations/rust/ockam/ockam_api/src/util.rs @@ -152,6 +152,12 @@ pub fn route_to_multiaddr(r: &Route) -> Option { Some(ma) } +/// Try to convert an Ockam Address into a MultiAddr. +pub fn addr_to_multiaddr>(a: T) -> Option { + let r: Route = Route::from(a); + route_to_multiaddr(&r) +} + /// Tells whether the input MultiAddr references a local node or a remote node. /// /// This should be called before cleaning the MultiAddr. diff --git a/implementations/rust/ockam/ockam_command/src/identity/show.rs b/implementations/rust/ockam/ockam_command/src/identity/show.rs index e0b476a6ad8..f3295198da6 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/show.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/show.rs @@ -56,7 +56,7 @@ pub async fn show_identity( let resp: Vec = ctx .send_and_receive( base_route.modify().append(NODEMANAGER_ADDR), - api::short_identity()?, + api::short_identity().to_vec()?, ) .await?; diff --git a/implementations/rust/ockam/ockam_command/src/node/show.rs b/implementations/rust/ockam/ockam_command/src/node/show.rs index c3ddb2fa59a..ccdfd6d7811 100644 --- a/implementations/rust/ockam/ockam_command/src/node/show.rs +++ b/implementations/rust/ockam/ockam_command/src/node/show.rs @@ -3,12 +3,23 @@ use crate::{help, node::HELP_DETAIL, CommandGlobalOpts}; use anyhow::Context; use clap::Args; use colorful::Colorful; -use ockam::Route; +use minicbor::Decoder; use ockam_api::config::cli::NodeConfig; -use ockam_api::nodes::{models::base::NodeStatus, NODEMANAGER_ADDR}; -use ockam_core::api::Status; +use ockam_api::nodes::models::portal::{InletList, OutletList}; +use ockam_api::nodes::models::services::ServiceList; +use ockam_api::nodes::models::transport::TransportList; +use ockam_api::nodes::NODEMANAGER_ADDR; +use ockam_api::{addr_to_multiaddr, route_to_multiaddr}; +use ockam_core::api::{Response, Status}; +use ockam_core::{Result, Route}; +use ockam_multiaddr::proto::{DnsAddr, Node, Tcp}; +use ockam_multiaddr::MultiAddr; use std::time::Duration; +const IS_NODE_UP_ATTEMPTS: usize = 10; +const IS_NODE_UP_SLEEP_MILLIS: u64 = 250; +const SEND_RECEIVE_TIMEOUT_SECS: u64 = 1; + /// Show Nodes #[derive(Clone, Debug, Args)] #[clap(arg_required_else_help = true, help_template = help::template(HELP_DETAIL))] @@ -40,42 +51,94 @@ impl ShowCommand { // printing the node state in the future but for now we can just tell // clippy to stop complainaing about it. #[allow(clippy::too_many_arguments)] -fn print_node_info(node_cfg: &NodeConfig, node_name: &str, status: &str, default_id: &str) { +fn print_node_info( + node_cfg: &NodeConfig, + node_name: &str, + status: &str, + default_id: &str, + services: Option<&ServiceList>, + tcp_listeners: Option<&TransportList>, + secure_channel_listeners: Option<&Vec>, + inlets_outlets: Option<(&InletList, &OutletList)>, +) { + println!(); + println!("Node:"); + println!(" Name: {}", node_name); println!( - r#" -Node: - Name: {} - Status: {} - Services: - Service: - Type: TCP Listener - Address: /ip4/127.0.0.1/tcp/{} - Service: - Type: Secure Channel Listener - Address: /service/api - Route: /ip4/127.0.0.1/tcp/{}/service/api - Identity: {} - Authorized Identities: - - {} - Service: - Type: Uppercase - Address: /service/uppercase - Service: - Type: Echo - Address: /service/echo - Secure Channel Listener Address: /service/api -"#, - node_name, + " Status: {}", match status { "UP" => status.light_green(), "DOWN" => status.light_red(), _ => status.white(), - }, - node_cfg.port, - node_cfg.port, - default_id, - default_id, + } ); + + println!(" Route To Node:"); + let mut m = MultiAddr::default(); + if m.push_back(Node::new(node_name)).is_ok() { + println!(" Short: {}", m); + } + + let mut m = MultiAddr::default(); + if m.push_back(DnsAddr::new("localhost")).is_ok() + && m.push_back(Tcp::new(node_cfg.port)).is_ok() + { + println!(" Verbose: {}", m); + } + println!(" Identity: {}", default_id); + + if let Some(list) = tcp_listeners { + println!(" Transports:"); + for e in &list.list { + println!(" Transport:"); + println!(" Type: {}", e.tt); + println!(" Mode: {}", e.tm); + println!(" Address: {}", e.payload); + } + } + + if let Some(list) = secure_channel_listeners { + println!(" Secure Channel Listeners:"); + for e in list { + println!(" Listener:"); + if let Some(ma) = addr_to_multiaddr(e) { + println!(" Address: {}", ma); + } + } + } + + if let Some((inlets, outlets)) = inlets_outlets { + println!(" Inlets:"); + for e in &inlets.list { + println!(" Inlet:"); + println!(" Listen Address: {}", e.bind_addr); + if let Some(r) = Route::parse(e.outlet_route.as_ref()) { + if let Some(ma) = route_to_multiaddr(&r) { + println!(" Route To Outlet: {}", ma); + } + } + } + println!(" Outlets:"); + for e in &outlets.list { + println!(" Outlet:"); + println!(" Forward Address: {}", e.tcp_addr); + + if let Some(ma) = addr_to_multiaddr(e.worker_addr.as_ref()) { + println!(" Address: {}", ma); + } + } + } + + if let Some(list) = services { + println!(" Services:"); + for e in &list.list { + println!(" Service:"); + println!(" Type: {}", e.service_type); + if let Some(ma) = addr_to_multiaddr(e.addr.as_ref()) { + println!(" Address: {}", ma); + } + } + } } pub async fn print_query_status( @@ -86,59 +149,138 @@ pub async fn print_query_status( let route = base_route.modify().append(NODEMANAGER_ADDR).into(); let node_cfg = cfg.get_node(&node_name)?; - // Wait until node is up. - if query_status(&mut ctx, &route).await.is_err() { - if wait_until_ready { - let mut attempts = 10; - while attempts > 0 { - tokio::time::sleep(Duration::from_millis(250)).await; - if query_status(&mut ctx, &route).await.is_ok() { - break; - } - attempts -= 1; - } - if attempts <= 0 { - print_node_info(&node_cfg, &node_name, "DOWN", "N/A"); - return Ok(()); + if !is_node_up(&mut ctx, &route, wait_until_ready).await? { + print_node_info(&node_cfg, &node_name, "DOWN", "N/A", None, None, None, None); + } else { + // Get short id for the node + let resp: Vec = ctx + .send_and_receive_with_timeout( + route.clone(), + api::short_identity().to_vec()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await + .context("Failed to get short identity from node")?; + let (response, result) = api::parse_short_identity_response(&resp)?; + let default_id = match response.status() { + Some(Status::Ok) => { + format!("{}", result.identity_id) } - } else { - print_node_info(&node_cfg, &node_name, "DOWN", "N/A"); - return Ok(()); - } - } + _ => String::from("NOT FOUND"), + }; - // Get short id for the node - ctx.send(route.clone(), api::short_identity()?).await?; - let resp = ctx - .receive_duration_timeout::>(Duration::from_millis(250)) - .await - .context("Failed to process request for short id")?; - - let (response, result) = api::parse_short_identity_response(&resp)?; - let default_id = match response.status() { - Some(Status::Ok) => { - format!("{}", result.identity_id) - } - _ => String::from("NOT FOUND"), - }; + // Get list of services for the node + let resp: Vec = ctx + .send_and_receive_with_timeout( + route.clone(), + api::list_services().to_vec()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await + .context("Failed to get list of services from node")?; + let services = api::parse_list_services_response(&resp)?; + + // Get list of TCP listeners for node + let resp: Vec = ctx + .send_and_receive_with_timeout( + route.clone(), + api::list_tcp_listeners().to_vec()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await + .context("Failed to get list of tcp listeners from node")?; + let tcp_listeners = api::parse_tcp_list(&resp)?; + + // Get list of Secure Channel Listeners + let resp: Vec = ctx + .send_and_receive_with_timeout( + route.clone(), + api::list_secure_channel_listener().to_vec()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await + .context("Failed to get list of secure channel listeners from node")?; + let mut dec = Decoder::new(&resp); + let _ = dec.decode::()?; + let secure_channel_listeners = dec.decode::>()?; + + // Get list of inlets + let resp: Vec = ctx + .send_and_receive_with_timeout( + route.clone(), + api::list_inlets().to_vec()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await + .context("Failed to get list of inlets from node")?; + let inlets = api::parse_list_inlets_response(&resp)?; + + // Get list of outlets + let resp: Vec = ctx + .send_and_receive_with_timeout( + route.clone(), + api::list_outlets().to_vec()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await + .context("Failed to get list of outlets from node")?; + let outlets = api::parse_list_outlets_response(&resp)?; + + print_node_info( + &node_cfg, + &node_name, + "UP", + &default_id, + Some(&services), + Some(&tcp_listeners), + Some(&secure_channel_listeners), + Some((&inlets, &outlets)), + ); + } - print_node_info(&node_cfg, &node_name, "UP", &default_id); Ok(()) } -async fn query_status(ctx: &mut ockam::Context, route: &Route) -> anyhow::Result<()> { - ctx.send(route.clone(), api::query_status()?).await?; +/// Send message(s) to a node to determine if it is 'up' and +/// responding to requests. +/// +/// If `wait_until_ready` is `true` and the node does not +/// appear to be 'up', retry the test at time intervals up to +/// a maximum number of retries. A use case for this is to +/// allow a node time to start up and become ready. +async fn is_node_up( + ctx: &mut ockam::Context, + route: &Route, + wait_until_ready: bool, +) -> anyhow::Result { + let mut node_up = false; + let attempts = match wait_until_ready { + true => IS_NODE_UP_ATTEMPTS, + false => 1, + }; - let resp = ctx - .receive_duration_timeout::>(Duration::from_millis(250)) - .await - .context("Failed to process request"); + for att in 0..attempts { + // Sleep, if this not the first loop + if att > 0 { + tokio::time::sleep(Duration::from_millis(IS_NODE_UP_SLEEP_MILLIS)).await; + } - match resp { - Ok(resp) => { - let NodeStatus { .. } = api::parse_status(&resp)?; - Ok(()) + // Test if node is up + let tx_result: Result> = ctx + .send_and_receive_with_timeout( + route.clone(), + api::query_status()?, + SEND_RECEIVE_TIMEOUT_SECS, + ) + .await; + if let Ok(data) = tx_result { + if api::parse_status(&data).is_ok() { + // Node is up, break loop + node_up = true; + break; + } } - Err(e) => Err(e), } + + Ok(node_up) } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs index ce8f8a73618..7a7adead370 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs @@ -29,7 +29,7 @@ pub async fn list_listeners(ctx: Context, _: (), mut base_route: Route) -> anyho let resp: Vec = match ctx .send_and_receive( base_route.modify().append(NODEMANAGER_ADDR), - api::list_tcp_listeners()?, + api::list_tcp_listeners().to_vec()?, ) .await { diff --git a/implementations/rust/ockam/ockam_command/src/util/api.rs b/implementations/rust/ockam/ockam_command/src/util/api.rs index 8cbeaa058a3..5233f63bfe1 100644 --- a/implementations/rust/ockam/ockam_command/src/util/api.rs +++ b/implementations/rust/ockam/ockam_command/src/util/api.rs @@ -38,11 +38,8 @@ pub(crate) fn list_tcp_connections() -> Result> { } /// Construct a request to query node tcp listeners -pub(crate) fn list_tcp_listeners() -> Result> { - let mut buf = vec![]; - let builder = Request::get("/node/tcp/listener"); - builder.encode(&mut buf)?; - Ok(buf) +pub(crate) fn list_tcp_listeners() -> RequestBuilder<'static, ()> { + Request::get("/node/tcp/listener") } /// Construct a request to create node tcp connection @@ -125,10 +122,23 @@ pub(crate) fn long_identity() -> Result> { } /// Construct a request to print Identity Id -pub(crate) fn short_identity() -> Result> { - let mut buf = vec![]; - Request::post("/node/identity/actions/show/short").encode(&mut buf)?; - Ok(buf) +pub(crate) fn short_identity() -> RequestBuilder<'static, ()> { + Request::post("/node/identity/actions/show/short") +} + +/// Construct a request to print a list of services for the given node +pub(crate) fn list_services() -> RequestBuilder<'static, ()> { + Request::get("/node/services") +} + +/// Construct a request to print a list of inlets for the given node +pub(crate) fn list_inlets() -> RequestBuilder<'static, ()> { + Request::get("/node/inlet") +} + +/// Construct a request to print a list of outlets for the given node +pub(crate) fn list_outlets() -> RequestBuilder<'static, ()> { + Request::get("/node/outlet") } /// Construct a request builder to list all secure channels on the given node @@ -429,6 +439,24 @@ pub(crate) fn parse_short_identity_response( )) } +pub(crate) fn parse_list_services_response(resp: &[u8]) -> Result { + let mut dec = Decoder::new(resp); + let _ = dec.decode::()?; + Ok(dec.decode::()?) +} + +pub(crate) fn parse_list_inlets_response(resp: &[u8]) -> Result { + let mut dec = Decoder::new(resp); + let _ = dec.decode::()?; + Ok(dec.decode::()?) +} + +pub(crate) fn parse_list_outlets_response(resp: &[u8]) -> Result { + let mut dec = Decoder::new(resp); + let _ = dec.decode::()?; + Ok(dec.decode::()?) +} + pub(crate) fn parse_create_secure_channel_listener_response(resp: &[u8]) -> Result { let mut dec = Decoder::new(resp); let response = dec.decode::()?; diff --git a/implementations/rust/ockam/ockam_command/tests/commands.bats b/implementations/rust/ockam/ockam_command/tests/commands.bats index cff0190dccd..7a86117ae73 100644 --- a/implementations/rust/ockam/ockam_command/tests/commands.bats +++ b/implementations/rust/ockam/ockam_command/tests/commands.bats @@ -86,9 +86,15 @@ teardown() { assert_success } -@test "create a node with a name" { +@test "create a node with a name and do show on it" { run $OCKAM node create n1 assert_success + + run $OCKAM node show n1 + assert_success + assert_output --partial "/dnsaddr/localhost/tcp/" + assert_output --partial "/service/api" + assert_output --partial "/service/uppercase" } @test "create a node with a name and send it a message" {