mod common;
use std::sync::Arc;
use std::time::Duration;
use common::TestStations;
use macula_rust::cbor::Value;
use macula_rust::frame::{StreamEncoding, StreamMode, StreamRole};
use macula_rust::handshake::HandshakeError;
use macula_rust::node_key::{NodeKey, PUZZLE_DIFFICULTY};
use macula_rust::profile::Profile;
use macula_rust::record::{self, new_node_record, NodeRecordOptions, RecordType};
use macula_rust::seal;
use macula_rust::statement_issuer::StatementIssuer;
use macula_rust::station_link::{
handler, handshake_counters, stream_handler, Call, ConfidentialityError, ConfidentialityReason,
Config, Link, LinkError, Offer, Publication, Request, Seal, Stream, StreamCall, StreamEvent,
};
async fn node(env: &TestStations, station: usize) -> Link {
let identity = NodeKey::generate_identity(env.profile, PUZZLE_DIFFICULTY).unwrap();
link_as(env, station, identity).await.unwrap()
}
async fn link_as(env: &TestStations, station: usize, identity: NodeKey) -> Result<Link, LinkError> {
let identity = Arc::new(identity);
let issuer = StatementIssuer::with_wall_clock(identity.clone()).unwrap();
Link::dial(Config::new(env.target(station), identity, issuer)).await
}
async fn eventually(what: &str, mut ok: impl FnMut() -> bool) {
for _ in 0..400 {
if ok() {
return;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
panic!("never: {what}");
}
#[tokio::test(flavor = "multi_thread")]
async fn a_node_links_to_a_station_it_pinned_in_either_profile() {
for profile in [Profile::PqPure, Profile::PqHybrid] {
let env = TestStations::start(profile);
let link = node(&env, 0).await;
assert_eq!(link.station_node_id(), env.stations[0].node_id);
assert_eq!(link.handshake_version(), 5, "{profile:?}");
assert!(handshake_counters()["v5_connections"] > 0);
link.close("done").await.unwrap();
assert!(matches!(link.error(), Some(LinkError::Closed)));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_station_that_is_not_the_node_pinned_is_refused() {
let env = TestStations::start(Profile::PqPure);
let mut target = env.target(0);
target.expected_node_id = env.stations[1].node_id;
let identity = Arc::new(NodeKey::generate_identity(env.profile, PUZZLE_DIFFICULTY).unwrap());
let issuer = StatementIssuer::with_wall_clock(identity.clone()).unwrap();
let refused = Link::dial(Config::new(target, identity, issuer)).await;
assert!(matches!(
refused,
Err(LinkError::Handshake(
HandshakeError::PeerIdentityMismatch { .. }
))
));
}
#[tokio::test(flavor = "multi_thread")]
async fn the_dht_holds_verified_records() {
let env = TestStations::start(Profile::PqPure);
let identity = NodeKey::generate_identity(env.profile, PUZZLE_DIFFICULTY).unwrap();
let node_id = identity.node_id().unwrap();
let signed = record::sign(
&new_node_record(&node_id, &[], 0, &NodeRecordOptions::default()).unwrap(),
&identity,
)
.unwrap();
let link = link_as(&env, 0, identity).await.unwrap();
let (endpoints, dropped) = link
.find_records_by_type(RecordType::STATION_ENDPOINT)
.await
.unwrap();
assert_eq!(dropped, 0);
let mut signers: Vec<[u8; 32]> = endpoints
.iter()
.map(|r| r.record().signed.as_ref().unwrap().key_id)
.collect();
signers.sort();
let mut stations: Vec<[u8; 32]> = env.stations.iter().map(|s| s.node_id).collect();
stations.sort();
assert_eq!(signers, stations);
assert!(matches!(
link.find_record(&[0x22; 32]).await,
Err(LinkError::RecordNotFound)
));
link.put_record(&record::encode(&signed).unwrap())
.await
.unwrap();
let found = link.find_record(&node_id).await.unwrap();
assert_eq!(found.record().version, signed.version);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_call_nobody_serves_is_answered_with_the_station_s_relay_error() {
let env = TestStations::start(Profile::PqPure);
let link = node(&env, 0).await;
let outcome = link
.call(Call {
realm: env.realm_id,
procedure: format!("{}/nothing", env.org),
target: [9; 32],
payload: Value::Null,
seal: Some(Seal::Clear),
..Call::default()
})
.await;
match outcome {
Err(LinkError::Relay { code, reported_by }) => {
assert_eq!(code, "unknown_next_peer");
assert_eq!(reported_by, env.stations[0].node_id);
}
other => panic!("{other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_publication_is_heard_once_by_each_subscriber() {
let env = TestStations::start(Profile::PqHybrid);
let listener = node(&env, 0).await;
let publisher = node(&env, 0).await;
let topic = "mcl-rust/tests/greeting_sent_v1";
let mut sub = listener.subscribe(&env.realm_id, topic).await.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
publisher
.publish(Publication {
realm: env.realm_id,
topic: topic.to_string(),
payload: Value::text("hi"),
ttl_ms: None,
})
.await
.unwrap();
let event = tokio::time::timeout(Duration::from_secs(5), sub.recv())
.await
.unwrap()
.unwrap();
assert_eq!(event.payload, Value::text("hi"));
assert_eq!(event.publisher, publisher.node_id());
assert_eq!(event.topic, topic);
assert!(tokio::time::timeout(Duration::from_millis(300), sub.recv())
.await
.is_err());
sub.unsubscribe().await.unwrap();
}
async fn ring_answer(r: Request) -> Result<Value, String> {
if r.payload == Value::text("fail") {
return Err("refused by the handler".to_string());
}
Ok(Value::Map(vec![(
Value::text("rung_by"),
Value::Bytes(r.caller.to_vec()),
)]))
}
fn clear_call(env: &TestStations, procedure: &str, target: [u8; 32], payload: Value) -> Call {
Call {
realm: env.realm_id,
procedure: procedure.to_string(),
target,
payload,
seal: Some(Seal::Clear),
..Call::default()
}
}
#[tokio::test(flavor = "multi_thread")]
async fn a_procedure_in_a_node_s_own_namespace_is_served_and_called() {
let env = TestStations::start(Profile::PqPure);
let provider = node(&env, 0).await;
let caller = node(&env, 0).await;
let ring = record::own_procedure(&provider.node_id(), "ring");
let served = provider
.serve(Offer::unary(env.realm_id, &ring, handler(ring_answer)))
.await
.unwrap();
let call = |payload: Value| clear_call(&env, &ring, provider.node_id(), payload);
let answered = caller.call(call(Value::Null)).await.unwrap();
assert_eq!(
answered.get("rung_by"),
Some(&Value::Bytes(caller.node_id().to_vec()))
);
match caller.call(call(Value::text("fail"))).await {
Err(LinkError::Provider { code, detail, .. }) => {
assert_eq!(code, "handler_error");
assert_eq!(detail.as_deref(), Some("refused by the handler"));
}
other => panic!("{other:?}"),
}
let unstated = caller
.call(Call {
seal: None,
..call(Value::Null)
})
.await;
assert!(
matches!(
&unstated,
Err(LinkError::Confidentiality(ConfidentialityError {
reason: ConfidentialityReason::NoSignedState,
..
}))
),
"{unstated:?}"
);
let key = seal::PrivateKey::generate(env.profile).unwrap();
let sealed = caller
.call(Call {
seal: Some(Seal::To(key.public_key().carried().to_vec())),
..call(Value::text("fail"))
})
.await;
assert_eq!(sealed, Err(LinkError::SealedRefused { named: None }));
served.stop().await.unwrap();
assert!(matches!(
caller.call(call(Value::Null)).await,
Err(LinkError::Relay { .. })
));
}
#[tokio::test(flavor = "multi_thread")]
async fn an_org_procedure_is_served_once_the_org_delegates_to_the_node() {
let env = TestStations::start(Profile::PqHybrid);
let identity = NodeKey::generate_identity(env.profile, PUZZLE_DIFFICULTY).unwrap();
env.admit(&identity.node_id().unwrap());
let provider = link_as(&env, 0, identity).await.unwrap();
let caller = node(&env, 0).await;
let procedure = format!("{}/echo", env.org);
let mut offer = Offer::unary(
env.realm_id,
&procedure,
handler(|r| async move { Ok(r.payload) }),
);
offer.realm_key = Some(env.realm_key.clone());
let served = provider.serve(offer).await.unwrap();
let answered = caller
.call(Call {
realm: env.realm_id,
procedure,
target: provider.node_id(),
payload: Value::text("hello"),
seal: Some(Seal::Clear),
..Call::default()
})
.await
.unwrap();
assert_eq!(answered, Value::text("hello"));
served.stop().await.unwrap();
let bare = Offer::unary(
env.realm_id,
&format!("{}/other", env.org),
handler(|r| async move { Ok(r.payload) }),
);
assert!(matches!(
provider.serve(bare).await,
Err(LinkError::InvalidOffer)
));
}
async fn three_chunks(stream: Stream) -> Result<(), String> {
for chunk in ["one", "two", "three"] {
stream
.send(chunk.as_bytes())
.await
.map_err(|e| e.to_string())?;
}
Ok(())
}
async fn count_bytes(stream: Stream) -> Result<(), String> {
let mut total = 0i128;
while let Ok(event) = stream.recv().await {
match event {
StreamEvent::Data {
body: Value::Bytes(b),
..
} => total += b.len() as i128,
StreamEvent::End { .. } => break,
_ => {}
}
}
stream
.reply(Value::Int(total))
.await
.map_err(|e| e.to_string())
}
fn clear_stream_call(
env: &TestStations,
procedure: &str,
target: [u8; 32],
mode: StreamMode,
) -> StreamCall {
StreamCall {
realm: env.realm_id,
procedure: procedure.to_string(),
target,
mode,
payload: Value::Null,
seal: Some(Seal::Clear),
..StreamCall::default()
}
}
async fn raw_chunks_until_both_end(stream: &Stream) -> Vec<Vec<u8>> {
let mut got = Vec::new();
loop {
match stream.recv().await.unwrap() {
StreamEvent::Data {
body: Value::Bytes(b),
encoding: StreamEncoding::Raw,
} => got.push(b),
StreamEvent::End { role } => {
assert_eq!(role, StreamRole::Both);
break;
}
other => panic!("{other:?}"),
}
}
got
}
#[tokio::test(flavor = "multi_thread")]
async fn streams_deliver_their_frames_and_are_released() {
let env = TestStations::start(Profile::PqPure);
let provider = node(&env, 0).await;
let caller = node(&env, 0).await;
let watch = record::own_procedure(&provider.node_id(), "watch");
let count = record::own_procedure(&provider.node_id(), "count");
let _watch = provider
.serve(Offer::stream(
env.realm_id,
&watch,
StreamMode::ServerStream,
stream_handler(three_chunks),
))
.await
.unwrap();
let _count = provider
.serve(Offer::stream(
env.realm_id,
&count,
StreamMode::ClientStream,
stream_handler(count_bytes),
))
.await
.unwrap();
let open = |procedure: &str, mode| clear_stream_call(&env, procedure, provider.node_id(), mode);
let stream = caller
.open_stream(open(&watch, StreamMode::ServerStream))
.await
.unwrap();
let got = raw_chunks_until_both_end(&stream).await;
assert_eq!(
got,
vec![b"one".to_vec(), b"two".to_vec(), b"three".to_vec()]
);
let upload = caller
.open_stream(open(&count, StreamMode::ClientStream))
.await
.unwrap();
for chunk in ["ab", "cde", "f"] {
upload.send(chunk.as_bytes()).await.unwrap();
}
upload.close_send().await.unwrap();
assert_eq!(
upload.recv().await.unwrap(),
StreamEvent::Reply {
payload: Value::Int(6)
}
);
let wrong = caller
.open_stream(open(&watch, StreamMode::Bidi))
.await
.unwrap();
match wrong.recv().await {
Err(LinkError::Stream { code, .. }) => assert_eq!(code, "mode_mismatch"),
other => panic!("{other:?}"),
}
eventually("every stream released", || env.relayed() == 0).await;
}