use std::net::SocketAddr;
use tokio::net::TcpListener;
use trust_tasks_https::{BearerAuth, ClientError, HttpsClient, HttpsServer};
use trust_tasks_rs::{
specs::acl::{grant, list, revoke},
specs::trust_task_discovery::v0_1 as discovery,
RejectReason, StandardCode, TrustTask, TypeUri,
};
const SERVER_VID: &str = "did:web:maintainer.example";
async fn spawn_server() -> SocketAddr {
let auth = BearerAuth::from_pairs([
("alice", "did:web:alice.example"),
("eve", "did:web:eve.example"),
]);
let server = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(auth)
.on::<grant::v0_1::Payload, grant::v0_1::Response, _>(|req, _ctx| {
Ok(grant::v0_1::Response {
entry: req.payload.entry.clone(),
})
})
.on::<revoke::v0_1::Payload, revoke::v0_1::Response, _>(|_req, ctx| {
if ctx.authenticated_sender.is_none() {
return Err(RejectReason::PermissionDenied {
reason: "revoke requires authentication".into(),
});
}
Ok(revoke::v0_1::Response { entry: None })
})
.enable_discovery()
.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
addr
}
fn entry() -> grant::v0_1::AclEntry {
grant::v0_1::AclEntry {
subject: "did:web:carol.example".into(),
role: "admin".parse().unwrap(),
scopes: vec![],
label: None,
created_at: None,
created_by: None,
updated_at: None,
updated_by: None,
expires_at: None,
metadata: Default::default(),
}
}
fn build_client(addr: SocketAddr, my_vid: &str, my_token: Option<&str>) -> HttpsClient {
let mut builder = HttpsClient::builder()
.server_url(format!("http://{addr}"))
.server_vid(SERVER_VID)
.my_vid(my_vid);
if let Some(t) = my_token {
builder = builder.my_token(t);
}
builder.build().unwrap()
}
#[tokio::test]
async fn happy_path_acl_grant() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-grant-1",
grant::v0_1::Payload {
entry: entry(),
reason: None,
},
);
let resp = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap();
assert_eq!(
resp.type_uri,
"https://trusttasks.org/spec/acl/grant/0.1#response"
.parse::<TypeUri>()
.unwrap()
);
assert_eq!(&*resp.payload.entry.role, "admin");
assert_eq!(resp.thread_id.as_deref(), Some("urn:uuid:test-grant-1"));
assert_eq!(resp.recipient.as_deref(), Some("did:web:alice.example"));
}
#[tokio::test]
async fn identity_mismatch_when_in_band_issuer_differs_from_token() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:carol.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-mismatch",
grant::v0_1::Payload {
entry: entry(),
reason: None,
},
);
let err = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 403);
assert_eq!(error.payload.code, StandardCode::IdentityMismatch.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert!(!msg.contains("alice"), "wire leak: {msg}");
assert!(!msg.contains("carol"), "wire leak: {msg}");
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn unsupported_type_for_unregistered_uri() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-unsupported",
list::v0_1::Payload {
role: None,
scope: None,
subject_prefix: None,
page_size: None,
cursor: None,
},
);
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 400);
assert_eq!(error.payload.code, StandardCode::UnsupportedType.into());
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn discovery_advertises_registered_handlers() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-discover-all",
discovery::Payload { patterns: vec![] },
);
let resp = client
.send::<discovery::Payload, discovery::Response>(req)
.await
.unwrap();
let mut got = resp.payload.supported_types.clone();
got.sort();
assert_eq!(
got,
vec![
"https://trusttasks.org/spec/acl/grant/0.1".to_string(),
"https://trusttasks.org/spec/acl/revoke/0.1".to_string(),
"https://trusttasks.org/spec/trust-task-discovery/0.1".to_string(),
],
"enable_discovery() should advertise the registered acl/grant + acl/revoke handlers \
plus discovery itself"
);
assert_eq!(
resp.type_uri,
"https://trusttasks.org/spec/trust-task-discovery/0.1#response"
.parse::<TypeUri>()
.unwrap()
);
assert_eq!(
resp.thread_id.as_deref(),
Some("urn:uuid:test-discover-all")
);
}
#[tokio::test]
async fn discovery_filter_returns_only_matching_slugs() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-discover-acl",
discovery::Payload {
patterns: vec!["acl/*".parse().unwrap()],
},
);
let resp = client
.send::<discovery::Payload, discovery::Response>(req)
.await
.unwrap();
let mut got = resp.payload.supported_types.clone();
got.sort();
assert_eq!(
got,
vec![
"https://trusttasks.org/spec/acl/grant/0.1".to_string(),
"https://trusttasks.org/spec/acl/revoke/0.1".to_string(),
],
"acl/* should match grant + revoke but not trust-task-discovery"
);
}
#[tokio::test]
async fn permission_denied_from_spec_handler() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", None);
let req = TrustTask::for_payload(
"urn:uuid:test-revoke",
revoke::v0_1::Payload {
subject: "did:web:bob.example".into(),
scopes: vec![],
reason: None,
},
);
let err = client
.send::<revoke::v0_1::Payload, revoke::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 403);
assert_eq!(error.payload.code, StandardCode::PermissionDenied.into());
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}