Files

127 lines
4.2 KiB
Rust

//! Integration tests for the Yggdrasil admin socket client, against a
//! fake admin server speaking the documented protocol.
#![forbid(unsafe_code)]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::time::Duration;
use sn_node::error::NodeError;
use sn_node::yggdrasil::{peers_from_response, AdminClient, AdminEndpoint, SelfInfo};
const SELF_RESPONSE: &str = concat!(
r#"{"request": {"request": "getself"}, "status": "success", "response": {"#,
r#""200:1111:2222:3333:4444:5555:6666:7777": {"#,
r#""address": "200:1111:2222:3333:4444:5555:6666:7777","#,
r#""key": "0000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1111","#,
r#""build_name": "yggdrasil", "build_version": "0.5.12","#,
r#""subnet": "300:1111:2222:3333::/64", "coords": [1, 2, 3]}}}"#
);
const PEERS_RESPONSE: &str = concat!(
r#"{"request": {"request": "getpeers"}, "status": "success", "response": {"#,
r#""200:1111:2222:3333:4444:5555:6666:7777": {"coords": [1]}, "#,
r#""200:9999:2222:3333:4444:5555:6666:7777": {"#,
r#""coords": [1, 2], "port": 1, "key": "ffff...", "#,
r#""remote": "tcp://[fe80::1%25eth0]:1337"}}}"#
);
const ERROR_RESPONSE: &str =
r#"{"request": {"request": "getself"}, "status": "error", "error": "boom"}"#;
/// Fake admin server: answers each connection's request line, then closes.
async fn fake_admin_server(response: &'static str) -> std::net::SocketAddr {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let Ok((mut socket, _)) = listener.accept().await else {
break;
};
let mut line = Vec::new();
let mut byte = [0u8; 1];
while !line.ends_with(b"\n") {
if socket.read(&mut byte).await.unwrap_or(0) == 0 {
break;
}
line.push(byte[0]);
}
socket.write_all(response.as_bytes()).await.ok();
socket.shutdown().await.ok();
}
});
addr
}
fn tcp_endpoint(addr: std::net::SocketAddr) -> AdminEndpoint {
AdminEndpoint::Tcp {
host: addr.ip().to_string(),
port: addr.port(),
}
}
#[tokio::test]
async fn get_self_parses_address_subnet_and_version() {
let endpoint = tcp_endpoint(fake_admin_server(SELF_RESPONSE).await);
let info = SelfInfo::from_response(
&AdminClient::connect(&endpoint, Duration::from_secs(2))
.await
.unwrap()
.request("getself")
.await
.unwrap(),
)
.unwrap();
assert_eq!(
info.address.inner().to_string(),
"200:1111:2222:3333:4444:5555:6666:7777"
);
assert_eq!(info.subnet.as_deref(), Some("300:1111:2222:3333::/64"));
assert_eq!(info.build_version.as_deref(), Some("0.5.12"));
}
#[tokio::test]
async fn get_peers_lists_remote_uris() {
let endpoint = tcp_endpoint(fake_admin_server(PEERS_RESPONSE).await);
let value = AdminClient::connect(&endpoint, Duration::from_secs(2))
.await
.unwrap()
.request("getpeers")
.await
.unwrap();
let peers = peers_from_response(&value).unwrap();
assert_eq!(peers.len(), 2);
assert_eq!(
peers[1].remote.as_deref(),
Some("tcp://[fe80::1%25eth0]:1337")
);
}
#[tokio::test]
async fn admin_error_is_surfaced() {
let endpoint = tcp_endpoint(fake_admin_server(ERROR_RESPONSE).await);
let mut client = AdminClient::connect(&endpoint, Duration::from_secs(2))
.await
.unwrap();
let err = client.request("getself").await.unwrap_err();
assert!(matches!(err, NodeError::AdminFailed(ref text) if text == "boom"));
}
#[tokio::test]
async fn connection_refused_becomes_io_or_timeout_error() {
// Port 1 on localhost: virtually guaranteed to be closed.
let endpoint = AdminEndpoint::Tcp {
host: "127.0.0.1".into(),
port: 1,
};
let result = AdminClient::get_self(&endpoint, Duration::from_millis(500)).await;
assert!(matches!(
result,
Err(NodeError::Io(_)) | Err(NodeError::Timeout)
));
}