use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_did_resolver_cache_sdk::config::DIDCacheConfigBuilder;
use affinidi_messaging_core::{
ConnState, Inbound, InboundKind, MessageTransport, Protocol, RelationshipRequest,
};
use affinidi_messaging_sdk::DidCommTransport;
use affinidi_secrets_resolver::SecretsResolver as _;
use affinidi_secrets_resolver::secrets::Secret;
use affinidi_tdk::common::TDKSharedState;
use affinidi_tdk::common::config::TDKConfig;
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::messaging::ATM;
use affinidi_tdk::messaging::config::ATMConfig;
use affinidi_tdk::messaging::profiles::ATMProfile;
use futures_util::StreamExt as _;
use rand::RngExt as _;
use serde_json::Value;
use tokio::sync::{Semaphore, oneshot, watch};
use tracing::{debug, info, warn};
use trust_tasks_didcomm::ENVELOPE_TYPE;
use crate::inbox::{Escalation, InboxWatch, ReconnectGovernor};
use crate::sender_order::{FrameOrder, SenderOrder};
#[async_trait::async_trait]
pub trait Responder: Send + Sync + 'static {
fn name(&self) -> &str;
fn did(&self) -> &str;
async fn answer(&self, sender: &str, document: &[u8]) -> Option<Value>;
}
pub const CONNECT_RETRY_BASE: Duration = Duration::from_secs(2);
pub const CONNECT_RETRY_CAP: Duration = Duration::from_secs(60);
pub const MIN_HEALTHY_SESSION: Duration = Duration::from_secs(60);
pub const SELF_RESOLUTION_FRESH_FOR: Duration = Duration::from_secs(300);
const WEBSOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_INFLIGHT_INBOUND: usize = 16;
pub struct Shared {
pub did_resolver: DIDCacheClient,
pub resolver_url: Option<String>,
}
pub struct IdentitySession {
identity: Arc<dyn Responder>,
mediator_did: String,
secrets: Vec<Secret>,
relationships: Option<Arc<crate::relationship_store::KeyspaceRelationshipStore>>,
requester: Option<Requester>,
}
impl std::fmt::Debug for IdentitySession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IdentitySession")
.field("identity", &self.identity.did())
.field("mediator_did", &self.mediator_did)
.field("durable_relationships", &self.relationships.is_some())
.field(
"secrets",
&format_args!("<{} redacted>", self.secrets.len()),
)
.finish()
}
}
impl IdentitySession {
pub fn new(identity: Arc<dyn Responder>, mediator_did: String, secrets: Vec<Secret>) -> Self {
Self {
identity,
mediator_did,
secrets,
relationships: None,
requester: None,
}
}
pub fn with_requester(mut self, requester: Requester) -> Self {
self.requester = Some(requester);
self
}
pub fn with_relationship_store(
mut self,
store: Arc<crate::relationship_store::KeyspaceRelationshipStore>,
) -> Self {
self.relationships = Some(store);
self
}
}
struct Live {
atm: Arc<ATM>,
profile: Arc<ATMProfile>,
transport: Arc<DidCommTransport>,
}
enum SessionEnd {
Shutdown,
Lost(String),
}
pub async fn supervise(
session: IdentitySession,
shared: Arc<Shared>,
shutdown: watch::Receiver<bool>,
) {
supervise_reporting(session, shared, shutdown, watch::channel(false).0).await;
}
pub async fn supervise_reporting(
session: IdentitySession,
shared: Arc<Shared>,
mut shutdown: watch::Receiver<bool>,
connected: watch::Sender<bool>,
) {
let _not_connected_on_return = NotConnectedOnDrop(&connected);
let name = session.identity.name().to_string();
let did = session.identity.did().to_string();
let mut probe = SelfResolution::new(&did, shared.resolver_url.as_deref());
let governor = Arc::new(ReconnectGovernor::new(Escalation::EndSession));
let mut attempt: u32 = 0;
loop {
if stopping(&shutdown) {
return;
}
let Some(resolvable) = until_shutdown(&mut shutdown, probe.is_resolvable()).await else {
return;
};
let failure = if !resolvable {
"its own DID does not resolve over the network yet (the mediator authenticates it \
by resolving it)"
.to_string()
} else {
match until_shutdown(&mut shutdown, connect(&session, &shared)).await {
None => return,
Some(Err(e)) => e,
Some(Ok(live)) => {
let live = Arc::new(live);
info!(
identity = %name,
did = %did,
mediator = %session.mediator_did,
"host identity connected to its mediator — serving Trust Tasks over TSP \
and DIDComm"
);
let started = tokio::time::Instant::now();
if let Some(requester) = &session.requester {
requester.attach(live.clone(), &did);
}
let end = serve(&live, &session, &governor, &mut shutdown, &connected).await;
if let Some(requester) = &session.requester {
requester.detach();
}
connected.send_replace(false);
live.atm.graceful_shutdown().await;
match end {
SessionEnd::Shutdown => return,
SessionEnd::Lost(why) => {
if started.elapsed() >= MIN_HEALTHY_SESSION {
attempt = 0;
}
why
}
}
}
}
};
let wait = connect_backoff(attempt);
warn!(
identity = %name,
attempt = attempt + 1,
retry_in_secs = wait.as_secs_f64(),
"host identity is not connected: {failure}; retrying"
);
if until_shutdown(&mut shutdown, tokio::time::sleep(wait))
.await
.is_none()
{
return;
}
attempt = attempt.saturating_add(1);
}
}
struct NotConnectedOnDrop<'a>(&'a watch::Sender<bool>);
impl Drop for NotConnectedOnDrop<'_> {
fn drop(&mut self) {
self.0.send_replace(false);
}
}
fn stopping(shutdown: &watch::Receiver<bool>) -> bool {
*shutdown.borrow()
}
async fn until_shutdown<F: std::future::Future>(
shutdown: &mut watch::Receiver<bool>,
fut: F,
) -> Option<F::Output> {
if stopping(shutdown) {
return None;
}
tokio::pin!(fut);
loop {
tokio::select! {
out = &mut fut => return Some(out),
changed = shutdown.changed() => {
if changed.is_err() || stopping(shutdown) {
return None;
}
}
}
}
}
async fn connect(session: &IdentitySession, shared: &Shared) -> Result<Live, String> {
let tdk_config = TDKConfig::builder()
.with_did_resolver(shared.did_resolver.clone())
.build()
.map_err(|e| format!("build TDK config: {e}"))?;
let tdk = TDKSharedState::new(tdk_config)
.await
.map_err(|e| format!("create TDK shared state: {e}"))?;
for secret in &session.secrets {
tdk.secrets_resolver().insert(secret.clone()).await;
}
let mut atm_config = ATMConfig::builder().with_delete_unprocessable(true);
if let Some(store) = &session.relationships {
atm_config = atm_config.with_relationship_store(store.clone());
}
let atm_config = atm_config
.build()
.map_err(|e| format!("build ATM config: {e}"))?;
let atm = Arc::new(
ATM::new(atm_config, Arc::new(tdk))
.await
.map_err(|e| format!("create ATM: {e}"))?,
);
match bind(&atm, session).await {
Ok((profile, transport)) => Ok(Live {
atm,
profile,
transport: Arc::new(transport),
}),
Err(e) => {
atm.graceful_shutdown().await;
Err(e)
}
}
}
async fn bind(
atm: &Arc<ATM>,
session: &IdentitySession,
) -> Result<(Arc<ATMProfile>, DidCommTransport), String> {
let profile = ATMProfile::new(
atm,
None,
session.identity.did().to_string(),
Some(session.mediator_did.clone()),
)
.await
.map_err(|e| format!("create ATM profile: {e}"))?;
let profile = atm
.profile_add(&profile, false)
.await
.map_err(|e| format!("register ATM profile: {e}"))?;
match tokio::time::timeout(
WEBSOCKET_CONNECT_TIMEOUT,
atm.profile_enable_websocket(&profile),
)
.await
{
Ok(res) => res.map_err(|e| format!("enable websocket: {e}"))?,
Err(_) => {
return Err(format!(
"timed out enabling the websocket to {} after {}s",
session.mediator_did,
WEBSOCKET_CONNECT_TIMEOUT.as_secs()
));
}
}
let transport = DidCommTransport::new((**atm).clone(), profile.clone())
.await
.map_err(|e| format!("bind the transport: {e}"))?;
Ok((profile, transport))
}
async fn serve(
live: &Arc<Live>,
session: &IdentitySession,
governor: &Arc<ReconnectGovernor>,
shutdown: &mut watch::Receiver<bool>,
connected: &watch::Sender<bool>,
) -> SessionEnd {
let mut inbound = live.transport.inbound();
let mut conn = live.transport.connection_state();
connected.send_replace(*conn.borrow_and_update() == ConnState::Connected);
let mut watch_task = tokio::spawn(crate::inbox::run_inbox_watch(
live.atm.clone(),
live.profile.clone(),
Some(live.transport.connection_state()),
InboxWatch::new(),
governor.clone(),
));
let inflight = Arc::new(Semaphore::new(MAX_INFLIGHT_INBOUND));
let sender_order = SenderOrder::new();
let end = loop {
tokio::select! {
frame = inbound.next() => {
let Some(frame) = frame else {
break SessionEnd::Lost("the inbound stream ended".to_string());
};
let Ok(permit) = inflight.clone().acquire_owned().await else {
break SessionEnd::Lost("the inbound limiter closed".to_string());
};
let ticket = sender_order.admit(frame.message.sender.as_deref(), frame_order(&frame));
let live = live.clone();
let identity = session.identity.clone();
let mediator_did = session.mediator_did.clone();
let requester = session.requester.clone();
tokio::spawn(async move {
let _permit = permit;
ticket.ready().await;
let _ticket = ticket;
handle_frame(frame, &live, &*identity, &mediator_did, requester.as_ref()).await;
});
}
Ok(()) = conn.changed() => {
connected.send_replace(*conn.borrow_and_update() == ConnState::Connected);
}
exit = &mut watch_task => {
break SessionEnd::Lost(format!(
"the mediator inbox watch ended the session ({exit:?})"
));
}
changed = shutdown.changed() => {
if changed.is_err() || stopping(shutdown) {
break SessionEnd::Shutdown;
}
}
}
};
watch_task.abort();
end
}
fn frame_order(frame: &Inbound) -> FrameOrder {
match frame.message.protocol {
Protocol::TSP if frame.is_relationship_control() => FrameOrder::Barrier,
Protocol::TSP => FrameOrder::Follower,
_ => FrameOrder::Unordered,
}
}
async fn handle_frame(
frame: Inbound,
live: &Live,
identity: &dyn Responder,
mediator_did: &str,
requester: Option<&Requester>,
) {
handle(&frame, live, identity, mediator_did, requester).await;
if let Err(e) = live.transport.ack(frame.ack.clone()).await {
debug!(identity = %identity.name(), error = %e, "could not ack an inbound frame");
}
}
async fn handle(
frame: &Inbound,
live: &Live,
identity: &dyn Responder,
mediator_did: &str,
requester: Option<&Requester>,
) {
let Some(sender) = frame
.message
.sender
.clone()
.filter(|_| frame.message.verified)
else {
debug!(identity = %identity.name(), "inbound frame has no verified sender — dropped");
return;
};
if let InboundKind::RelationshipControl {
request,
thread_digest,
reply_expected,
..
} = &frame.kind
{
answer_relationship(
live,
identity,
&sender,
*request,
*thread_digest,
*reply_expected,
)
.await;
return;
}
match frame.message.protocol {
Protocol::TSP => {
handle_tsp(frame, live, identity, mediator_did, &sender).await;
}
Protocol::DIDComm => {
if let Some(requester) = requester
&& requester.complete(&sender, &frame.message.payload)
{
return;
}
handle_didcomm(frame, live, identity, &sender).await
}
other => debug!(
identity = %identity.name(),
protocol = ?other,
"inbound frame in a protocol this service does not speak — dropped"
),
}
}
async fn answer_relationship(
live: &Live,
identity: &dyn Responder,
sender: &str,
request: RelationshipRequest,
thread_digest: [u8; 32],
reply_expected: bool,
) {
match request {
RelationshipRequest::Invite => {
match live
.atm
.tsp()
.accept_relationship(&live.profile, sender, thread_digest)
.await
{
Ok(_) => info!(
identity = %identity.name(), %sender,
"accepted a TSP relationship request"
),
Err(e) => warn!(
identity = %identity.name(), %sender, error = %e,
"could not send a TSP relationship accept; the relationship stays recorded"
),
}
}
RelationshipRequest::Cancel if reply_expected => {
if let Err(e) = live
.atm
.tsp()
.answer_cancellation(&live.profile, sender, thread_digest)
.await
{
warn!(
identity = %identity.name(), %sender, error = %e,
"could not answer a TSP relationship cancellation"
);
}
}
_ => debug!(
identity = %identity.name(), %sender, ?request,
"recorded a TSP relationship message; no answer is due"
),
}
}
async fn handle_tsp(
frame: &Inbound,
live: &Live,
identity: &dyn Responder,
mediator_did: &str,
sender: &str,
) {
let payload = &frame.message.payload;
let wrapped = vta_sdk::tsp_binding::open_envelope(payload);
let document = match &wrapped {
Ok(document) => document.as_slice(),
Err(_) => payload.as_slice(),
};
let Some(reply) = identity.answer(sender, document).await else {
debug!(identity = %identity.name(), %sender, "TSP frame dropped unanswered");
return;
};
let Ok(bytes) = serde_json::to_vec(&reply) else {
return;
};
let bytes = if wrapped.is_ok() {
vta_sdk::tsp_binding::wrap_envelope(&bytes)
} else {
bytes
};
if let Err(e) =
crate::tsp_route::send_reply(&live.atm, &live.profile, mediator_did, sender, &bytes).await
{
warn!(identity = %identity.name(), %sender, error = %e, "could not send a TSP reply");
}
}
async fn handle_didcomm(frame: &Inbound, live: &Live, identity: &dyn Responder, sender: &str) {
let Some((thread, document)) = didcomm_request(&frame.message.payload) else {
debug!(identity = %identity.name(), %sender, "DIDComm message is not a Trust-Task envelope — not answered");
return;
};
let Some(reply) = identity.answer(sender, &document).await else {
debug!(identity = %identity.name(), %sender, "DIDComm request dropped unanswered");
return;
};
let message = Message::build(
uuid::Uuid::new_v4().to_string(),
ENVELOPE_TYPE.to_string(),
reply,
)
.from(identity.did().to_string())
.to(sender.to_string())
.thid(thread)
.finalize();
let packed = match live
.atm
.pack_encrypted(&message, sender, Some(identity.did()), Some(identity.did()))
.await
{
Ok((packed, _)) => packed,
Err(e) => {
warn!(identity = %identity.name(), %sender, error = %e, "could not pack a DIDComm reply");
return;
}
};
match live.transport.send(sender, packed.into_bytes()).await {
Ok(_) => {
debug!(identity = %identity.name(), %sender, "DIDComm reply handed to the mediator")
}
Err(e) => {
warn!(identity = %identity.name(), %sender, error = %e, "could not send a DIDComm reply")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RequestError {
#[error("not connected to the mediator")]
NotConnected,
#[error("the request document has no id")]
NoId,
#[error("could not send: {0}")]
Send(String),
#[error("no answer within {0:?}")]
Timeout(Duration),
}
#[derive(Clone, Default)]
pub struct Requester {
inner: Arc<RequesterInner>,
}
#[derive(Default)]
struct RequesterInner {
live: RwLock<Option<(Arc<Live>, String)>>,
pending: Mutex<HashMap<String, (String, oneshot::Sender<Value>)>>,
}
impl std::fmt::Debug for Requester {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Requester")
.field("connected", &self.is_connected())
.finish()
}
}
impl Requester {
pub fn new() -> Self {
Self::default()
}
pub fn is_connected(&self) -> bool {
self.inner.live.read().map(|l| l.is_some()).unwrap_or(false)
}
fn attach(&self, live: Arc<Live>, did: &str) {
if let Ok(mut slot) = self.inner.live.write() {
*slot = Some((live, did.to_string()));
}
}
fn detach(&self) {
if let Ok(mut slot) = self.inner.live.write() {
*slot = None;
}
}
pub async fn request(
&self,
peer: &str,
document: Value,
timeout: Duration,
) -> Result<Value, RequestError> {
let id = document
.get("id")
.and_then(Value::as_str)
.ok_or(RequestError::NoId)?
.to_string();
let (live, from) = self
.inner
.live
.read()
.ok()
.and_then(|l| l.clone())
.ok_or(RequestError::NotConnected)?;
let (tx, rx) = oneshot::channel();
if let Ok(mut pending) = self.inner.pending.lock() {
pending.insert(id.clone(), (peer.to_string(), tx));
}
let _forget = ForgetOnDrop(&self.inner, &id);
let message = Message::build(id.clone(), ENVELOPE_TYPE.to_string(), document)
.from(from.clone())
.to(peer.to_string())
.finalize();
let (packed, _) = live
.atm
.pack_encrypted(&message, peer, Some(&from), Some(&from))
.await
.map_err(|e| RequestError::Send(format!("pack: {e}")))?;
live.transport
.send(peer, packed.into_bytes())
.await
.map_err(|e| RequestError::Send(e.to_string()))?;
match tokio::time::timeout(timeout, rx).await {
Ok(Ok(answer)) => Ok(answer),
Ok(Err(_)) | Err(_) => Err(RequestError::Timeout(timeout)),
}
}
fn complete(&self, sender: &str, payload: &[u8]) -> bool {
let Ok(message) = serde_json::from_slice::<Value>(payload) else {
return false;
};
if message.get("type").and_then(Value::as_str) != Some(ENVELOPE_TYPE) {
return false;
}
let Some(body) = message.get("body") else {
return false;
};
let keys = [
message.get("thid").and_then(Value::as_str),
body.get("threadId").and_then(Value::as_str),
];
let Ok(mut pending) = self.inner.pending.lock() else {
return false;
};
for key in keys.into_iter().flatten() {
if pending.get(key).is_some_and(|(peer, _)| peer == sender)
&& let Some((_, tx)) = pending.remove(key)
{
let _ = tx.send(body.clone());
return true;
}
}
false
}
}
struct ForgetOnDrop<'a>(&'a RequesterInner, &'a str);
impl Drop for ForgetOnDrop<'_> {
fn drop(&mut self) {
if let Ok(mut pending) = self.0.pending.lock() {
pending.remove(self.1);
}
}
}
pub fn didcomm_request(payload: &[u8]) -> Option<(String, Vec<u8>)> {
let message: serde_json::Value = serde_json::from_slice(payload).ok()?;
if message.get("type")?.as_str()? != ENVELOPE_TYPE {
return None;
}
let thread = message.get("id")?.as_str()?.to_string();
let document = serde_json::to_vec(message.get("body")?).ok()?;
Some((thread, document))
}
fn connect_backoff(attempt: u32) -> Duration {
let ceiling = connect_backoff_ceiling(attempt);
let max_ms = u64::try_from(ceiling.as_millis()).unwrap_or(u64::MAX);
Duration::from_millis(rand::rng().random_range(0..=max_ms))
}
fn connect_backoff_ceiling(attempt: u32) -> Duration {
let factor = 1u32.checked_shl(attempt).unwrap_or(u32::MAX);
CONNECT_RETRY_BASE
.saturating_mul(factor)
.min(CONNECT_RETRY_CAP)
}
struct SelfResolution {
did: String,
resolver_url: Option<String>,
last_confirmed: Option<tokio::time::Instant>,
}
impl SelfResolution {
fn new(did: &str, resolver_url: Option<&str>) -> Self {
Self {
did: did.to_string(),
resolver_url: resolver_url.map(str::to_string),
last_confirmed: None,
}
}
async fn is_resolvable(&mut self) -> bool {
if !needs_network_probe(&self.did) {
return true;
}
let now = tokio::time::Instant::now();
if self
.last_confirmed
.is_some_and(|at| now.saturating_duration_since(at) < SELF_RESOLUTION_FRESH_FOR)
{
return true;
}
let ok = resolves_now(&self.did, self.resolver_url.as_deref()).await;
self.last_confirmed = ok.then_some(now);
ok
}
}
fn needs_network_probe(did: &str) -> bool {
vta_sdk::did_scid::is_network_resolved(did)
}
async fn resolves_now(did: &str, resolver_url: Option<&str>) -> bool {
let mut builder =
DIDCacheConfigBuilder::default().with_host_policy(vta_sdk::resolver::webvh_host_policy());
if let Some(url) = resolver_url {
builder = builder.with_network_mode(url);
}
let resolver = match DIDCacheClient::new(builder.build()).await {
Ok(r) => r,
Err(e) => {
debug!(error = %e, "could not build a resolver for the self-resolution check");
return false;
}
};
let resolved = resolver.resolve(did).await;
resolver.stop();
match resolved {
Ok(_) => true,
Err(e) => {
debug!(%did, error = %e, "own DID does not resolve yet");
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn waiting(requester: &Requester, id: &str, peer: &str) -> oneshot::Receiver<Value> {
let (tx, rx) = oneshot::channel();
requester
.inner
.pending
.lock()
.unwrap()
.insert(id.to_string(), (peer.to_string(), tx));
rx
}
fn envelope(thid: Option<&str>, body: Value) -> Vec<u8> {
let mut m = json!({ "id": "m-2", "type": ENVELOPE_TYPE, "body": body });
if let Some(t) = thid {
m["thid"] = json!(t);
}
serde_json::to_vec(&m).unwrap()
}
#[test]
fn an_answer_completes_its_request_by_thid_or_thread_id() {
let requester = Requester::new();
let mut by_thid = waiting(&requester, "req-1", "did:example:registry");
let answer = json!({ "id": "a-1", "type": "x#response" });
assert!(requester.complete(
"did:example:registry",
&envelope(Some("req-1"), answer.clone())
));
assert_eq!(by_thid.try_recv().unwrap(), answer);
let mut by_thread = waiting(&requester, "req-2", "did:example:registry");
let answer = json!({ "id": "a-2", "threadId": "req-2" });
assert!(requester.complete("did:example:registry", &envelope(None, answer.clone())));
assert_eq!(by_thread.try_recv().unwrap(), answer);
assert!(requester.inner.pending.lock().unwrap().is_empty());
}
#[test]
fn only_the_peer_asked_answers_and_anything_else_is_a_request() {
let requester = Requester::new();
let _rx = waiting(&requester, "req-1", "did:example:registry");
assert!(!requester.complete("did:example:mallory", &envelope(Some("req-1"), json!({}))));
assert!(!requester.complete("did:example:registry", &envelope(Some("other"), json!({}))));
assert!(!requester.complete("did:example:registry", b"{\"type\":\"ping\"}"));
assert_eq!(requester.inner.pending.lock().unwrap().len(), 1);
}
#[tokio::test]
async fn a_request_without_a_session_is_not_connected() {
let requester = Requester::new();
assert!(!requester.is_connected());
let err = requester
.request(
"did:example:registry",
json!({ "id": "req-1" }),
Duration::from_millis(10),
)
.await
.unwrap_err();
assert_eq!(err, RequestError::NotConnected);
let err = requester
.request("did:example:registry", json!({}), Duration::from_millis(10))
.await
.unwrap_err();
assert_eq!(err, RequestError::NoId);
}
#[test]
fn the_backoff_grows_to_its_cap_and_never_past_it() {
assert_eq!(connect_backoff_ceiling(0), CONNECT_RETRY_BASE);
assert_eq!(connect_backoff_ceiling(1), CONNECT_RETRY_BASE * 2);
assert_eq!(connect_backoff_ceiling(5), CONNECT_RETRY_CAP);
assert_eq!(connect_backoff_ceiling(u32::MAX), CONNECT_RETRY_CAP);
for attempt in [0, 3, 40] {
assert!(connect_backoff(attempt) <= connect_backoff_ceiling(attempt));
}
}
#[test]
fn only_network_resolved_methods_are_gated() {
assert!(needs_network_probe("did:webvh:QmScid:example.com:w"));
assert!(needs_network_probe("did:web:example.com"));
assert!(!needs_network_probe("did:key:z6MkExample"));
assert!(!needs_network_probe("did:peer:2zExample"));
}
#[test]
fn a_didcomm_envelope_threads_on_its_own_id() {
let document = json!({ "id": "urn:uuid:doc", "type": "https://trusttasks.org/spec/x/0.1" });
let payload = serde_json::to_vec(&json!({
"id": "urn:uuid:envelope",
"type": ENVELOPE_TYPE,
"body": document,
}))
.unwrap();
let (thread, body) = didcomm_request(&payload).expect("an envelope");
assert_eq!(thread, "urn:uuid:envelope");
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&body).unwrap(),
document
);
}
#[test]
fn a_didcomm_message_that_is_not_an_envelope_is_not_a_request() {
for typ in [
"https://didcomm.org/report-problem/2.0/problem-report",
"https://didcomm.org/trust-ping/2.0/ping",
"https://trusttasks.org/spec/trust-task-discovery/0.3",
] {
let payload =
serde_json::to_vec(&json!({ "id": "urn:uuid:x", "type": typ, "body": {} }))
.unwrap();
assert!(didcomm_request(&payload).is_none(), "{typ}");
}
assert!(didcomm_request(b"not json").is_none());
}
#[tokio::test]
async fn a_did_key_identity_is_never_gated() {
let mut probe = SelfResolution::new("did:key:z6MkExample", None);
assert!(probe.is_resolvable().await);
}
#[tokio::test]
async fn shutdown_interrupts_a_wait() {
let (tx, mut rx) = watch::channel(false);
let waiting = tokio::spawn(async move {
until_shutdown(&mut rx, tokio::time::sleep(Duration::from_secs(3600))).await
});
tx.send(true).unwrap();
let out = tokio::time::timeout(Duration::from_secs(5), waiting)
.await
.expect("shutdown must not wait out the sleep")
.unwrap();
assert!(out.is_none());
}
}