use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use chrono::{Duration, Utc};
use dtg_credentials::DTGCredential;
use serde_json::{Value, json};
use trust_tasks_rs::TrustTask;
use uuid::Uuid;
use vta_sdk::protocols::join_requests::manifest;
const JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE: &str =
<manifest::v0_2::Response as trust_tasks_rs::Payload>::TYPE_URI;
use vta_sdk::protocols::vetting::{
VETTED_PREDICATE, VETTER_ROLE, VETTING_DECLINE_TYPE, VETTING_REQUEST_ERR_INVALID_TICKET,
VETTING_REQUEST_ERR_NOT_ELIGIBLE, VETTING_REQUEST_TYPE, VETTING_SESSION_TYPE,
VETTING_VETTER_PROFILE_ERR_NOT_ELIGIBLE, VETTING_VETTER_RESEND_ERR_NOT_GRANTED, VettingMethod,
VettingRelationship, VettingRequirements, decline, request, revoke_statement, session, vetters,
};
use vta_sdk::trust_task_proof::TrustTaskVmResolver;
use vta_sdk::vetting::card::sign_card;
use vta_sdk::vetting::statement::sign_statement;
use vta_sdk::vetting::status::StatusCheck;
use super::VettingBook;
use super::applicant::{
ApplicantError, Application, ChosenFace, GrantStatus, NextStep, RequestDraft, RequestState,
TicketUriError, VetterEligibility,
};
use super::book::{Knowledge, VetterPolicy};
use super::inbound::{Context, Handled, Notice, Reply, handle};
use super::queries::{CommunityAnswer, CommunityQuery, QueryKind};
use super::registry::{ProfileDraft, ProfileState};
use super::tickets::{DEFAULT_VALIDITY, Ticket};
use super::vetter::{Attestation, DeskState};
use super::wire::{
self,
tests::{did, secret},
};
use crate::config::account::{Account, CommunityRecord, PersonaId};
use crate::persona::disclosure::ReleasedClaim;
const COMMUNITY: &str = "did:key:z6MkkckEJvRiDoUSv2KFGPFuUoNjJbWTZUvWThqshF7g1u4p";
const COMMUNITY_SEED: u8 = 0xC0;
const DIGEST: &str = "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR";
fn unfiltered_list() -> vetters::list::v0_1::Payload {
vetters::list::v0_1::Payload::try_from(vetters::list::v0_1::Payload::builder()).unwrap()
}
fn code_ticket(code: &str) -> request::v0_1::Ticket {
request::v0_1::Ticket::ShortCodeTicket(
request::v0_1::ShortCodeTicket::try_from(
request::v0_1::ShortCodeTicket::builder().code(code),
)
.expect("a Crockford code"),
)
}
async fn verified_delivery(
message: &Message,
sender: &str,
resolver: &affinidi_did_resolver_cache_sdk::DIDCacheClient,
) -> Option<
Result<
crate::issued_credential::VerifiedIssuedCredential,
crate::issued_credential::IssuedCredentialError,
>,
> {
if message.typ != vta_sdk::protocols::credential_exchange::ISSUE {
return None;
}
crate::messaging::credential_in_issue(message)?;
Some(
crate::issued_credential::verify_issued_delivery(
&message.body,
sender,
resolver,
Utc::now(),
)
.await,
)
}
async fn did_resolver() -> affinidi_did_resolver_cache_sdk::DIDCacheClient {
affinidi_did_resolver_cache_sdk::DIDCacheClient::new(
affinidi_did_resolver_cache_sdk::config::DIDCacheConfigBuilder::default().build(),
)
.await
.expect("a DID resolver")
}
#[test]
fn the_community_is_its_key() {
assert_eq!(did(&secret(COMMUNITY_SEED)), COMMUNITY);
}
fn role_vac(issuer: &Secret, community: &str, subject: &str, role: &str) -> DTGCredential {
let now = Utc::now();
DTGCredential::new_vac(
did(issuer),
dtg_credentials::IssuerScope::Public,
subject.to_string(),
community.to_string(),
vec![vta_sdk::protocols::vetting::role_action(role)],
now - chrono::Duration::minutes(1),
now + chrono::Duration::days(365),
)
.unwrap()
.with_max_attenuation(0)
.unwrap()
.with_id(wire::new_id())
}
async fn role_credential(issuer: &Secret, community: &str, subject: &str, role: &str) -> Value {
let mut credential = role_vac(issuer, community, subject, role);
credential.sign(issuer, None).await.unwrap();
serde_json::to_value(&credential).unwrap()
}
async fn delivery(credential: &Value, issuer: &Secret) -> Message {
let document: TrustTask<Value> = serde_json::from_value(json!({
"id": format!("urn:uuid:{}", wire::new_id()),
"type": vta_sdk::protocols::credential_exchange::ISSUE,
"issuer": did(issuer),
"recipient": "did:key:zTheMember",
"issuedAt": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": { "credential_response": { "credential": credential } },
}))
.expect("an issue document");
signed(document, issuer).await
}
async fn signed_reply(reply: Reply, signer: &Secret) -> Message {
let mut document = reply.document;
if let Some(eligibility) = reply.eligibility {
wire::attach_eligibility(&mut document, signer, eligibility)
.await
.unwrap();
}
signed(document, signer).await
}
struct Party {
secret: Secret,
did: String,
persona: PersonaId,
book: VettingBook,
account: Account,
seen: crate::operational::SeenDocuments,
}
impl Party {
fn new(seed: u8) -> Self {
let secret = secret(seed);
Self {
did: did(&secret),
secret,
persona: PersonaId::new(),
book: VettingBook::default(),
account: Account::default(),
seen: crate::operational::SeenDocuments::default(),
}
}
fn member_of(mut self, community: &str) -> Self {
let mut record = CommunityRecord::new_pending(
community.to_string(),
None,
"openvtc/test".to_string(),
self.persona,
Uuid::new_v4(),
Utc::now(),
);
record.activate(Utc::now());
self.account.add_membership(record);
self
}
async fn named_vetter(&mut self) {
let credential = role_credential(
&secret(COMMUNITY_SEED),
COMMUNITY,
&self.did.clone(),
VETTER_ROLE,
)
.await;
let handled = self
.receive(
&delivery(&credential, &secret(COMMUNITY_SEED)).await,
COMMUNITY,
)
.await;
assert!(matches!(handled.notice, Some(Notice::VetterGranted { .. })));
}
async fn receive(&mut self, message: &Message, sender: &str) -> Handled {
self.try_receive(message, sender)
.await
.expect("a vetting message is claimed")
}
async fn try_receive(&mut self, message: &Message, sender: &str) -> Option<Handled> {
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let issued = verified_delivery(message, sender, &did_resolver).await;
let ctx = Context {
account: &self.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: issued.as_ref(),
community_answer: None,
recipient: Some((self.persona, &self.did)),
now: Utc::now(),
};
handle(&mut self.book, &ctx, &mut self.seen, message, sender).await
}
async fn receive_checked(
&mut self,
message: &Message,
sender: &str,
answer: &Result<
crate::operational::VerifiedOperational,
crate::operational::OperationalError,
>,
) -> Handled {
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let ctx = Context {
account: &self.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: None,
community_answer: Some(answer),
recipient: Some((self.persona, &self.did)),
now: Utc::now(),
};
handle(&mut self.book, &ctx, &mut self.seen, message, sender)
.await
.expect("a vetting message is claimed")
}
fn application(&mut self) -> &mut super::applicant::Application {
self.book
.application_mut(COMMUNITY, self.persona)
.expect("application started")
}
}
async fn signed(mut document: TrustTask<Value>, signer: &Secret) -> Message {
wire::sign(&mut document, signer).await.unwrap();
wire::to_message(&document).unwrap()
}
fn requirements() -> VettingRequirements {
serde_json::from_value(json!({
"version": "0.1",
"statementType": VETTED_PREDICATE,
"minStatements": 1,
"minByMethod": { "inPerson": 1 },
"acceptedMethods": ["inPerson", "video"],
"requiredClaims": ["name.legal"],
"eligibleVetters": { "role": "vetter" }
}))
.unwrap()
}
fn manifest_body() -> manifest::v0_2::Response {
manifest_with(Some(requirements()), None)
}
fn manifest_with(
vetting: Option<VettingRequirements>,
branding: Option<manifest::v0_2::CommunityBranding>,
) -> manifest::v0_2::Response {
let criterion = manifest::v0_2::Criterion::try_from(
manifest::v0_2::Criterion::builder()
.id("vetted")
.presentation_definition(serde_json::Map::new())
.vetting(vetting)
.requirements_digest(Some(
manifest::v0_2::DigestMultibase::try_from(DIGEST).unwrap(),
)),
)
.unwrap();
manifest::v0_2::Response::try_from(
manifest::v0_2::Response::builder()
.community_did(COMMUNITY)
.criteria(vec![criterion])
.branding(branding),
)
.unwrap()
}
fn manifest_raw_with_ext(ext: Value, critical: Option<Value>) -> Value {
let mut raw = serde_json::to_value(manifest_body()).unwrap();
let vetting = raw["criteria"][0]["vetting"].as_object_mut().unwrap();
vetting.insert("ext".into(), ext);
if let Some(c) = critical {
vetting.insert("extCritical".into(), c);
}
raw
}
fn hidden_params() -> Value {
use openvtc_vetting_pcs::{scheme::key_text, vtc::Vtc};
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(0x0BED);
let vtc = Vtc::new(
COMMUNITY,
"2026-10",
serde_json::to_value(requirements()).unwrap(),
&mut rng,
)
.expect("a community");
json!({
"suite": super::hidden::SUITE,
"helperKey": key_text(vtc.hvk()).unwrap(),
"tokenKey": key_text(vtc.tvk()).unwrap(),
"vetterLabels": ["vetter/2026-10"],
"tokenLabels": [vtc.current_token_label()],
"tickLength": "P3D"
})
}
#[tokio::test]
async fn an_application_adopts_the_hidden_vetting_parameters_a_community_publishes() {
let mut party = Party::new(1);
party
.book
.start_application(COMMUNITY, party.persona, &party.did, Utc::now())
.expect("an application starts");
let raw = manifest_raw_with_ext(
json!({ super::hidden::HIDDEN_VETTING_NS: hidden_params() }),
Some(json!([super::hidden::HIDDEN_VETTING_NS])),
);
let changed = party
.application()
.adopt_manifest(&manifest_body(), &raw)
.expect("the criterion is one this build implements");
assert!(changed);
let hidden = party
.application()
.hidden
.clone()
.expect("the parameters were adopted");
let published = hidden_params();
assert_eq!(hidden.helper_key, published["helperKey"].as_str().unwrap());
assert_eq!(hidden.token_key, published["tokenKey"].as_str().unwrap());
assert_eq!(hidden.vetter_labels, vec!["vetter/2026-10".to_string()]);
assert!(party.application().hidden_id().is_some());
assert!(party.application().requirements.is_some());
}
#[tokio::test]
async fn a_challenge_is_answered_to_its_question_and_spent_by_the_submit_it_binds() {
let mut applicant = Party::new(1);
applicant
.book
.start_application(COMMUNITY, applicant.persona, &applicant.did, Utc::now())
.expect("an application starts");
let raw = manifest_raw_with_ext(
json!({ super::hidden::HIDDEN_VETTING_NS: hidden_params() }),
Some(json!([super::hidden::HIDDEN_VETTING_NS])),
);
applicant
.application()
.adopt_manifest(&manifest_body(), &raw)
.expect("a hidden criterion");
let request = wire::pcs_challenge_request(&applicant.did, COMMUNITY, None).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::PcsChallenge));
let issued = wire::pcs::ChallengeResponse {
challenge: "c0ffee".into(),
expires_at: Utc::now() + Duration::minutes(15),
ext: None,
};
let handled = applicant
.receive(&community_answer(&request, &issued).await, COMMUNITY)
.await;
assert!(matches!(
handled.answer,
Some(CommunityAnswer::Challenge { ref query, ref community })
if *query == request.id && community == COMMUNITY
));
assert!(matches!(
handled.notice,
Some(Notice::ChallengeIssued { .. })
));
assert_eq!(
applicant.application().hidden_challenge.as_deref(),
Some("c0ffee")
);
assert!(!applicant.application().hidden_submission_sent());
assert_eq!(
applicant.application().hidden_challenge.as_deref(),
Some("c0ffee")
);
applicant
.application()
.set_hidden_submission(json!({ "challenge": "c0ffee" }));
assert!(applicant.application().hidden_submission_sent());
assert_eq!(applicant.application().hidden_challenge, None);
assert_eq!(applicant.application().hidden_submission, None);
}
#[tokio::test]
async fn a_critical_namespace_this_build_cannot_honour_stops_the_application() {
let mut party = Party::new(1);
party
.book
.start_application(COMMUNITY, party.persona, &party.did, Utc::now())
.expect("an application starts");
let raw = manifest_raw_with_ext(
json!({ "com.example.some-scheme": { "a": 1 } }),
Some(json!(["com.example.some-scheme"])),
);
let err = party
.application()
.adopt_manifest(&manifest_body(), &raw)
.expect_err("a marked namespace we do not implement must refuse");
assert!(matches!(
err,
super::applicant::ApplicantError::Hidden(super::hidden::HiddenError::UnsupportedExtension(
_
))
));
assert!(party.application().hidden.is_none());
assert!(party.application().requirements.is_none());
}
#[tokio::test]
async fn an_unmarked_namespace_this_build_does_not_implement_is_ignored() {
let mut party = Party::new(1);
party
.book
.start_application(COMMUNITY, party.persona, &party.did, Utc::now())
.expect("an application starts");
let raw = manifest_raw_with_ext(json!({ "com.example.hint": { "a": 1 } }), None);
party
.application()
.adopt_manifest(&manifest_body(), &raw)
.expect("an unmarked namespace is advisory");
assert!(party.application().hidden.is_none());
assert!(party.application().requirements.is_some());
}
async fn manifest_reply(to: &str) -> Message {
signed_manifest(to, manifest_body()).await
}
async fn signed_manifest(to: &str, body: impl serde::Serialize) -> Message {
let document = crate::operational::test_support::sign(
json!({
"id": wire::new_id(),
"type": JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE,
"issuer": COMMUNITY,
"recipient": to,
"issuedAt": Utc::now().to_rfc3339(),
"payload": body,
}),
&secret(COMMUNITY_SEED),
)
.await;
Message::build(
wire::new_id(),
JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE.to_string(),
document,
)
.from(COMMUNITY.to_string())
.thid(wire::new_id())
.finalize()
}
async fn ready() -> (Party, Party, request::v0_1::Ticket) {
let mut applicant = Party::new(1);
let mut vetter = Party::new(2).member_of(COMMUNITY);
vetter.named_vetter().await;
let now = Utc::now();
applicant
.book
.start_application(COMMUNITY, applicant.persona, &applicant.did, now)
.unwrap();
let handled = applicant
.receive(&manifest_reply(&applicant.did.clone()).await, COMMUNITY)
.await;
assert!(matches!(
handled.notice,
Some(Notice::RequirementsUpdated { .. })
));
let ticket = Ticket::issue(COMMUNITY, vetter.persona, vec![], 1, DEFAULT_VALIDITY, now);
let presented = ticket.code_presentation().unwrap();
vetter.book.tickets.push(ticket);
(applicant, vetter, presented)
}
async fn request(applicant: &mut Party, vetter: &Party, ticket: request::v0_1::Ticket) -> Message {
let id = wire::new_id();
let vetter_did = vetter.did.clone();
let body = applicant
.application()
.prepare_request(
&id,
&vetter_did,
ticket,
RequestDraft::default(),
Utc::now(),
)
.unwrap();
let doc = wire::document(VETTING_REQUEST_TYPE, &applicant.did, &vetter.did, id, &body).unwrap();
signed(doc, &applicant.secret).await
}
async fn in_session(applicant: &mut Party, vetter: &mut Party) -> (String, TrustTask<Value>) {
let ticket = vetter.book.tickets[0].code_presentation().unwrap();
let message = request(applicant, vetter, ticket).await;
let handled = vetter.receive(&message, &applicant.did).await;
let Some(Notice::RequestAccepted { request_id, .. }) = handled.notice.clone() else {
panic!("the request is accepted");
};
let reply = handled.reply.expect("an accepted request is answered");
assert!(
reply.eligibility.is_some(),
"a named vetter presents the grant"
);
let message = signed_reply(reply, &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
assert!(matches!(
handled.notice,
Some(Notice::VetterAccepted {
shown_eligible: true,
..
})
));
let accepted = applicant.application().requests.last().unwrap().clone();
assert!(matches!(
accepted.eligibility,
Some(VetterEligibility::Shown { .. })
));
let session_id = wire::new_id();
let body = vetter
.book
.open_session(
&request_id,
VettingMethod::InPerson,
vec!["name.legal".into()],
vec![],
&session_id,
Utc::now(),
)
.unwrap();
let doc = wire::document(
VETTING_SESSION_TYPE,
&vetter.did,
&applicant.did,
session_id,
&body,
)
.unwrap();
vetter
.book
.record_session_document(&request_id, serde_json::to_value(&doc).unwrap())
.unwrap();
(request_id, doc)
}
#[tokio::test]
async fn an_applicant_is_vetted_end_to_end() {
let (mut applicant, mut vetter, _) = ready().await;
let resolver = TrustTaskVmResolver::did_key_only();
let (request_id, session_doc) = in_session(&mut applicant, &mut vetter).await;
let message = signed(session_doc.clone(), &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
let Some(Notice::SessionOpened {
session_id,
match_code,
..
}) = handled.notice.clone()
else {
panic!("the session opens");
};
assert_eq!(
Some(match_code.as_str()),
vetter.book.desk_entry(&request_id).unwrap().match_code()
);
assert!(
handled.notice.unwrap().task().is_some(),
"the applicant must act"
);
let draft = applicant
.application()
.card_draft(
&session_id,
vec![
session::v0_1::VettingCardClaim::try_from(
session::v0_1::VettingCardClaim::builder()
.type_("name.legal")
.value(json!("Alice Example"))
.provenance("selfAsserted"),
)
.unwrap(),
],
Utc::now(),
)
.unwrap();
let card = sign_card(draft, &applicant.secret).await.unwrap();
applicant
.application()
.record_card(&session_id, &card, &resolver, Utc::now())
.await
.unwrap();
let doc = wire::response(&session_doc, &json!({ "card": card })).unwrap();
let message = signed(doc, &applicant.secret).await;
let handled = vetter.receive(&message, &applicant.did).await;
assert!(matches!(handled.notice, Some(Notice::CardReceived { .. })));
let draft = vetter
.book
.statement_draft(
&request_id,
&vetter.did,
Attestation {
method: VettingMethod::InPerson,
document_classes: vec!["passport".into()],
claims_verified: vec!["name.legal".into()],
liveness_confirmed: true,
declared_relationship: VettingRelationship::None,
attestation_text_digest: None,
},
Utc::now(),
)
.unwrap();
let statement = sign_statement(draft, &vetter.secret).await.unwrap();
let issued = vetter
.book
.record_statement(&request_id, &statement, &resolver, Utc::now())
.await
.unwrap();
assert!(matches!(
vetter.book.desk_entry(&request_id).unwrap().state,
DeskState::Attested { .. }
));
let message = wire::credential_delivery(
&vetter.did,
&applicant.did,
&statement,
&session_id,
&vetter.secret,
)
.await
.unwrap();
let handled = applicant.receive(&message, &vetter.did).await;
assert!(matches!(
handled.notice,
Some(Notice::StatementReceived { .. })
));
let application = applicant.application();
assert!(application.checklist(Utc::now()).unwrap().satisfied());
assert_eq!(application.presentable_statements(Utc::now()).len(), 1);
assert_eq!(
application.join_extensions(super::protocol::JoinProtocol::V0_2)["requirementsDigest"],
DIGEST
);
assert!(
application
.join_extensions(super::protocol::JoinProtocol::V0_3)
.get("requirementsDigest")
.is_none(),
"0.3 names the criterion in the submit, not in extensions"
);
let notice_id = wire::new_id();
let (body, _) = vetter
.book
.withdrawal(
&issued.id,
Some(revoke_statement::v0_1::PayloadReason::Mistake),
¬ice_id,
Utc::now(),
)
.unwrap();
assert_eq!(
body.statement_digest_multibase.as_str(),
issued.digest_multibase
);
assert!(
vetter
.book
.on_withdrawal_recorded(COMMUNITY, ¬ice_id, Utc::now())
.is_some()
);
}
fn released(name: Option<&str>) -> Vec<ReleasedClaim> {
vec![
ReleasedClaim {
claim_type: "name.legal".into(),
value: name.map(|n| json!(n)),
provenance: Some("selfAsserted".into()),
stale: false,
},
ReleasedClaim {
claim_type: "email.work".into(),
value: Some(json!("alice@example.com")),
provenance: Some("selfAsserted".into()),
stale: false,
},
]
}
#[tokio::test]
async fn cards_come_from_the_face_and_keep_showing_the_same_identity() {
let (mut applicant, mut vetter, _) = ready().await;
let resolver = TrustTaskVmResolver::did_key_only();
let (_, session_doc) = in_session(&mut applicant, &mut vetter).await;
let message = signed(session_doc, &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
let Some(Notice::SessionOpened { session_id, .. }) = handled.notice else {
panic!("the session opens");
};
let signer = applicant.secret.clone();
let app = applicant.application();
assert_eq!(app.requested_claims(&session_id), vec!["name.legal"]);
let claims = app
.card_claims(&session_id, &released(Some("Alice Example")))
.unwrap();
assert_eq!(claims.len(), 1);
assert_eq!(claims[0].value, json!("Alice Example"));
assert!(matches!(
app.card_claims(&session_id, &released(None)),
Err(ApplicantError::ValueWithheld(t)) if t == "name.legal"
));
assert!(matches!(
app.card_claims(&session_id, &[]),
Err(ApplicantError::MissingClaim(_))
));
let draft = app
.card_draft(&session_id, claims.clone(), Utc::now())
.unwrap();
let card = sign_card(draft, &signer).await.unwrap();
let sent = app
.record_card(&session_id, &card, &resolver, Utc::now())
.await
.unwrap();
app.record_sent_card(&session_id, sent, &claims, Utc::now())
.unwrap();
assert_eq!(
serde_json::to_value(&app.identity_claims).unwrap(),
serde_json::to_value(&claims).unwrap()
);
assert!(matches!(
app.card_claims(&session_id, &released(Some("Alicia Example"))),
Err(ApplicantError::IdentityChanged(t)) if t == "name.legal"
));
}
#[tokio::test]
async fn without_a_ticket_there_is_no_answer_at_all() {
let (mut applicant, mut vetter, _) = ready().await;
let message = request(&mut applicant, &vetter, code_ticket("0000-0000")).await;
let handled = vetter.receive(&message, &applicant.did).await;
assert!(handled.reply.is_none() && handled.notice.is_none());
assert!(vetter.book.desk.is_empty());
}
#[tokio::test]
async fn a_bad_scanned_ticket_is_refused_and_the_applicant_hears_why() {
let (mut applicant, mut vetter, _) = ready().await;
let ticket_id = vetter.book.tickets[0].id.clone();
let scanned = request::v0_1::Ticket::QrTicket(
request::v0_1::QrTicket::try_from(
request::v0_1::QrTicket::builder()
.ticket_id(ticket_id)
.secret("A".repeat(43)),
)
.unwrap(),
);
let message = request(&mut applicant, &vetter, scanned).await;
let handled = vetter.receive(&message, &applicant.did).await;
let reply = handled.reply.expect("a scanned ticket is answered");
assert_eq!(
reply.document.payload["code"],
VETTING_REQUEST_ERR_INVALID_TICKET
);
let Some(Notice::RequestRefused {
applicant: who,
code,
}) = handled.notice
else {
panic!("the vetter is told a request was turned away");
};
assert_eq!(who, applicant.did);
assert_eq!(code, VETTING_REQUEST_ERR_INVALID_TICKET);
let message = signed(reply.document, &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
let refused = handled.notice.expect("the applicant is told");
assert!(matches!(refused, Notice::VetterRefused { .. }));
let said = refused.describe();
assert!(said.contains(VETTING_REQUEST_ERR_INVALID_TICKET), "{said}");
assert!(said.contains("Ask them for a fresh one"), "{said}");
assert!(matches!(
applicant.application().requests[0].state,
RequestState::Refused { .. }
));
}
#[tokio::test]
async fn someone_who_is_not_a_member_cannot_vet() {
let (mut applicant, _, _) = ready().await;
let mut outsider = Party::new(3);
let ticket = Ticket::issue(
COMMUNITY,
outsider.persona,
vec![],
1,
DEFAULT_VALIDITY,
Utc::now(),
);
let presented = ticket.code_presentation().unwrap();
outsider.book.tickets.push(ticket);
let message = request(&mut applicant, &outsider, presented).await;
let handled = outsider.receive(&message, &applicant.did).await;
assert_eq!(
handled.reply.unwrap().document.payload["code"],
VETTING_REQUEST_ERR_NOT_ELIGIBLE
);
}
#[tokio::test]
async fn a_bare_vetter_grant_delivery_is_refused() {
let mut member = Party::new(6).member_of(COMMUNITY);
let credential = role_credential(
&secret(COMMUNITY_SEED),
COMMUNITY,
&member.did.clone(),
VETTER_ROLE,
)
.await;
let bare = Message::build(
wire::new_id(),
vta_sdk::protocols::credential_exchange::ISSUE.to_string(),
json!({ "credential_response": { "credential": credential } }),
)
.from(COMMUNITY.to_string())
.finalize();
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let ctx = Context {
account: &member.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: None,
community_answer: None,
recipient: Some((member.persona, &member.did)),
now: Utc::now(),
};
let handled = handle(
&mut member.book,
&ctx,
&mut crate::operational::SeenDocuments::default(),
&bare,
COMMUNITY,
)
.await;
assert!(
handled.is_none_or(|h| h.notice.is_none()),
"a bare delivery grants nothing"
);
assert!(member.book.vetter_grants.is_empty());
}
#[tokio::test]
async fn a_vetter_grant_relayed_by_another_party_is_refused() {
let mut member = Party::new(7).member_of(COMMUNITY);
let credential = role_credential(
&secret(COMMUNITY_SEED),
COMMUNITY,
&member.did.clone(),
VETTER_ROLE,
)
.await;
let relay = secret(0xD7);
let handled = member
.receive(&delivery(&credential, &relay).await, &did(&relay))
.await;
assert!(handled.notice.is_none() && member.book.vetter_grants.is_empty());
}
#[tokio::test]
async fn a_member_the_community_has_not_named_cannot_vet() {
let (mut applicant, _, _) = ready().await;
let mut member = Party::new(4).member_of(COMMUNITY);
let ticket = Ticket::issue(
COMMUNITY,
member.persona,
vec![],
1,
DEFAULT_VALIDITY,
Utc::now(),
);
let presented = ticket.code_presentation().unwrap();
member.book.tickets.push(ticket);
let message = request(&mut applicant, &member, presented).await;
let handled = member.receive(&message, &applicant.did).await;
assert_eq!(
handled.reply.unwrap().document.payload["code"],
VETTING_REQUEST_ERR_NOT_ELIGIBLE
);
assert!(member.book.desk.is_empty());
}
#[tokio::test]
async fn a_vetter_grant_is_kept_only_from_its_community() {
let mut member = Party::new(5).member_of(COMMUNITY);
let impostor = secret(0xC1);
let forged = role_credential(&impostor, COMMUNITY, &member.did.clone(), VETTER_ROLE).await;
let handled = member
.try_receive(&delivery(&forged, &impostor).await, &did(&impostor))
.await;
assert!(handled.is_none_or(|h| h.notice.is_none()) && member.book.vetter_grants.is_empty());
let for_someone_else = role_credential(
&secret(COMMUNITY_SEED),
COMMUNITY,
"did:key:zSomeoneElse",
VETTER_ROLE,
)
.await;
let handled = member
.receive(
&delivery(&for_someone_else, &secret(COMMUNITY_SEED)).await,
COMMUNITY,
)
.await;
assert!(handled.notice.is_none() && member.book.vetter_grants.is_empty());
let ordinary = role_credential(
&secret(COMMUNITY_SEED),
COMMUNITY,
&member.did.clone(),
"member",
)
.await;
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let issued: Option<
Result<
crate::issued_credential::VerifiedIssuedCredential,
crate::issued_credential::IssuedCredentialError,
>,
> = None;
let ctx = Context {
account: &member.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: issued.as_ref(),
community_answer: None,
recipient: Some((member.persona, &member.did)),
now: Utc::now(),
};
assert!(
handle(
&mut member.book,
&ctx,
&mut crate::operational::SeenDocuments::default(),
&delivery(&ordinary, &secret(COMMUNITY_SEED)).await,
COMMUNITY
)
.await
.is_none(),
"an ordinary role credential is not vetting's"
);
member.named_vetter().await;
assert!(
member
.book
.vetter_grant(COMMUNITY, member.persona, Utc::now())
.is_some()
);
}
#[tokio::test]
async fn an_acceptance_without_a_grant_shown_is_recorded_as_such() {
let (mut applicant, mut vetter, ticket) = ready().await;
let message = request(&mut applicant, &vetter, ticket).await;
let handled = vetter.receive(&message, &applicant.did).await;
let mut reply = handled.reply.unwrap();
reply.eligibility = None;
let message = signed_reply(reply, &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
assert!(matches!(
handled.notice,
Some(Notice::VetterAccepted {
shown_eligible: false,
..
})
));
assert_eq!(
applicant.application().requests[0].eligibility,
Some(VetterEligibility::NotShown)
);
}
#[tokio::test]
async fn a_vetter_can_decline_without_a_reason() {
let (mut applicant, mut vetter, _) = ready().await;
let (request_id, _) = in_session(&mut applicant, &mut vetter).await;
let body = vetter
.book
.decline(
&request_id,
Some(decline::v0_1::PayloadCode::NotComfortable),
None,
Utc::now(),
)
.unwrap();
let doc = wire::document(
VETTING_DECLINE_TYPE,
&vetter.did,
&applicant.did,
wire::new_id(),
&body,
)
.unwrap();
let message = signed(doc, &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
assert!(matches!(handled.notice, Some(Notice::Declined { .. })));
assert!(applicant.application().checklist(Utc::now()).is_some());
}
#[tokio::test]
async fn a_membership_credential_is_left_for_the_join_handler() {
let mut applicant = Party::new(1);
let vmc = json!({ "credential_response": { "credential": {
"type": ["VerifiableCredential", "MembershipCredential"],
"issuer": COMMUNITY
} } });
let message = Message::build(
wire::new_id(),
vta_sdk::protocols::credential_exchange::ISSUE.to_string(),
vmc,
)
.from(COMMUNITY.to_string())
.finalize();
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let issued: Option<
Result<
crate::issued_credential::VerifiedIssuedCredential,
crate::issued_credential::IssuedCredentialError,
>,
> = None;
let ctx = Context {
account: &applicant.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: issued.as_ref(),
community_answer: None,
recipient: Some((applicant.persona, &applicant.did)),
now: Utc::now(),
};
assert!(
handle(
&mut applicant.book,
&ctx,
&mut crate::operational::SeenDocuments::default(),
&message,
COMMUNITY
)
.await
.is_none()
);
}
#[tokio::test]
async fn a_vetter_asks_for_the_claims_the_community_requires() {
let mut vetter = Party::new(2).member_of(COMMUNITY);
assert_eq!(
vetter.book.required_claims_for(COMMUNITY, None),
(vec!["name.legal".to_string()], false),
"without the manifest, the fallback — and it says so"
);
let handled = vetter
.receive(&manifest_reply(&vetter.did.clone()).await, COMMUNITY)
.await;
assert!(handled.changed, "the criteria are remembered");
let (claims, known) = vetter.book.required_claims_for(COMMUNITY, Some(DIGEST));
assert!(known);
assert_eq!(
claims,
requirements()
.required_claims
.iter()
.flatten()
.map(|c| c.as_str().to_string())
.collect::<Vec<_>>()
);
assert!(
!vetter
.receive(&manifest_reply(&vetter.did.clone()).await, COMMUNITY)
.await
.changed,
"the same manifest again changes nothing"
);
}
#[tokio::test]
async fn the_communitys_decision_sla_is_known_once_its_manifest_is() {
let (applicant, _, _) = ready().await;
let persona = applicant.persona;
assert_eq!(applicant.book.decision_sla(COMMUNITY, persona), None);
let mut with_sla = Party::new(4);
with_sla
.book
.start_application(COMMUNITY, with_sla.persona, &with_sla.did, Utc::now())
.unwrap();
let mut requirements = requirements();
requirements.decision_sla = Some("P21D".try_into().unwrap());
let body = manifest_with(Some(requirements), None);
let reply = signed_manifest(&with_sla.did.clone(), body).await;
with_sla.receive(&reply, COMMUNITY).await;
assert_eq!(
with_sla.book.decision_sla(COMMUNITY, with_sla.persona),
chrono::Duration::try_days(21)
);
}
async fn community_answer<P: serde::Serialize>(request: &TrustTask<Value>, payload: &P) -> Message {
let document = wire::response(request, payload).unwrap();
let mut message = wire::to_message(&document).unwrap();
message.body = crate::operational::test_support::sign(
serde_json::to_value(&document).unwrap(),
&secret(COMMUNITY_SEED),
)
.await;
message
}
fn community_refusal(request: &TrustTask<Value>, code: &str) -> Message {
wire::to_message(&wire::refusal(request, code, Some("because")).unwrap()).unwrap()
}
fn asked(request: &TrustTask<Value>, persona: PersonaId, kind: QueryKind) -> CommunityQuery {
CommunityQuery {
document_id: request.id.clone(),
community: COMMUNITY.into(),
persona,
kind,
sent_at: Utc::now(),
}
}
fn listed_vetter() -> vetters::list::v0_1::ListedVetter {
serde_json::from_value(json!({
"vetterDid": "did:key:zCarol",
"displayName": "Carol",
"languages": ["en", "cs"],
"location": { "country": "CZ", "city": "Prague" },
"methods": ["inPerson", "video"],
"acceptsDocumentation": ["passport"],
"contactHint": "find me at the LPC vetting desk",
"events": [{ "name": "LPC", "startDate": "2026-10-05", "endDate": "2026-10-07" }],
"grantValidUntil": "2027-09-01T00:00:00Z",
"updatedAt": "2026-09-01T00:00:00Z"
}))
.unwrap()
}
#[tokio::test]
async fn a_vetter_publishes_a_profile_and_hears_the_communitys_answer() {
let mut vetter = Party::new(2).member_of(COMMUNITY);
vetter.named_vetter().await;
let now = Utc::now();
let mut draft = ProfileDraft::new(&VetterPolicy::default());
draft.listed = true;
draft.languages = "en, cs".into();
let body = draft.to_body().unwrap();
let mut request = wire::vetter_profile_request(&vetter.did, COMMUNITY, &body).unwrap();
wire::sign(&mut request, &vetter.secret).await.unwrap();
assert_eq!(
request.proof.as_ref().unwrap().proof_purpose,
"authentication"
);
let sent = wire::to_message(&request).unwrap();
assert_eq!(sent.typ, crate::capabilities::TRUST_TASK_ENVELOPE_TYPE);
let opened: wire::Opened<vetters::profile::v0_1::Payload> = wire::open(
&crate::didcomm::open_didcomm_envelope(&sent).unwrap(),
&vetter.did,
&TrustTaskVmResolver::did_key_only(),
)
.await
.unwrap();
assert_eq!(
serde_json::to_value(&opened.payload).unwrap(),
serde_json::to_value(&body).unwrap()
);
vetter
.book
.record_profile_sent(COMMUNITY, vetter.persona, &body, now);
vetter
.book
.ask(asked(&request, vetter.persona, QueryKind::VetterProfile));
let stored = vetters::profile::v0_1::Response::try_from(
vetters::profile::v0_1::Response::builder()
.listed(true)
.updated_at(now),
)
.unwrap();
let handled = vetter
.receive(&community_answer(&request, &stored).await, COMMUNITY)
.await;
assert!(handled.changed, "the stored state is kept");
assert!(matches!(
handled.notice,
Some(Notice::ProfilePublished { listed: true, .. })
));
assert!(matches!(
handled.answer,
Some(CommunityAnswer::ProfileStored { listed: true, .. })
));
assert!(matches!(
vetter
.book
.vetter_profile(COMMUNITY, vetter.persona)
.unwrap()
.state,
ProfileState::Stored { listed: true, .. }
));
let request = wire::vetter_profile_request(&vetter.did, COMMUNITY, &body).unwrap();
vetter
.book
.record_profile_sent(COMMUNITY, vetter.persona, &body, now);
vetter
.book
.ask(asked(&request, vetter.persona, QueryKind::VetterProfile));
let handled = vetter
.receive(
&community_refusal(&request, VETTING_VETTER_PROFILE_ERR_NOT_ELIGIBLE),
COMMUNITY,
)
.await;
assert!(matches!(
&handled.answer,
Some(CommunityAnswer::Refused { kind: QueryKind::VetterProfile, code, .. })
if code == VETTING_VETTER_PROFILE_ERR_NOT_ELIGIBLE
));
let notice = handled.notice.expect("a refusal is explained");
assert!(notice.describe().contains("live vetter credential"));
assert!(matches!(
vetter
.book
.vetter_profile(COMMUNITY, vetter.persona)
.unwrap()
.state,
ProfileState::Refused { .. }
));
}
#[tokio::test]
async fn the_directory_answers_only_what_was_asked() {
let mut applicant = Party::new(1);
let page = vetters::list::v0_1::Response::try_from(
vetters::list::v0_1::Response::builder()
.vetters(vec![listed_vetter()])
.next_cursor(Some(
vetters::list::v0_1::ResponseNextCursor::try_from("page-2").unwrap(),
)),
)
.unwrap();
let request = wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
let handled = applicant
.receive(&community_answer(&request, &page).await, COMMUNITY)
.await;
assert!(handled.answer.is_none());
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::VetterList));
let handled = applicant
.receive(
&community_answer(&request, &page).await,
"did:key:zElsewhere",
)
.await;
assert!(handled.answer.is_none());
let handled = applicant
.receive(&community_answer(&request, &page).await, COMMUNITY)
.await;
let Some(CommunityAnswer::Vetters {
query, page: got, ..
}) = handled.answer
else {
panic!("the page is handed to whoever asked");
};
assert_eq!(query, request.id);
assert_eq!(
serde_json::to_value(&got).unwrap(),
serde_json::to_value(&page).unwrap()
);
assert!(!handled.changed, "a directory page is never saved");
let request = wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::VetterList));
let handled = applicant
.receive(&community_refusal(&request, "permissionDenied"), COMMUNITY)
.await;
assert!(matches!(
&handled.answer,
Some(CommunityAnswer::Refused { kind: QueryKind::VetterList, code, .. })
if code == "permissionDenied"
));
let stray = wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let issued: Option<
Result<
crate::issued_credential::VerifiedIssuedCredential,
crate::issued_credential::IssuedCredentialError,
>,
> = None;
let ctx = Context {
account: &applicant.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: issued.as_ref(),
community_answer: None,
recipient: Some((applicant.persona, &applicant.did)),
now: Utc::now(),
};
assert!(
handle(
&mut applicant.book,
&ctx,
&mut crate::operational::SeenDocuments::default(),
&community_refusal(&stray, "permissionDenied"),
COMMUNITY
)
.await
.is_none()
);
let request = wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::VetterList));
let handled = applicant
.receive(
&community_answer(&request, &json!({ "vetters": "not a list" })).await,
COMMUNITY,
)
.await;
assert!(matches!(
handled.answer,
Some(CommunityAnswer::Unreadable {
kind: QueryKind::VetterList,
..
})
));
assert!(applicant.book.queries.is_empty());
}
fn enveloped(message: &Message) -> Message {
let mut enveloped = message.clone();
enveloped.typ = crate::capabilities::TRUST_TASK_ENVELOPE_TYPE.to_string();
enveloped
}
#[tokio::test]
async fn a_community_answer_is_read_in_either_carriage() {
let mut applicant = Party::new(1);
let page = vetters::list::v0_1::Response::try_from(
vetters::list::v0_1::Response::builder().vetters(vec![listed_vetter()]),
)
.unwrap();
for in_envelope in [false, true] {
let request =
wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::VetterList));
let reply = community_answer(&request, &page).await;
let reply = if in_envelope {
crate::didcomm::open_didcomm_envelope(&enveloped(&reply))
.expect("an enveloped document opens")
} else {
assert!(
crate::didcomm::open_didcomm_envelope(&reply).is_none(),
"a document-typed reply needs no opening"
);
reply
};
let handled = applicant.receive(&reply, COMMUNITY).await;
assert!(
matches!(&handled.answer, Some(CommunityAnswer::Vetters { query, .. }) if *query == request.id),
"answered in the envelope: {in_envelope}"
);
let request =
wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::VetterList));
let refusal = community_refusal(&request, "permissionDenied");
let refusal = if in_envelope {
crate::didcomm::open_didcomm_envelope(&enveloped(&refusal)).unwrap()
} else {
refusal
};
let handled = applicant.receive(&refusal, COMMUNITY).await;
assert!(matches!(
&handled.answer,
Some(CommunityAnswer::Refused { kind: QueryKind::VetterList, code, .. })
if code == "permissionDenied"
));
}
assert!(applicant.book.queries.is_empty());
}
#[tokio::test]
async fn a_problem_report_refuses_the_question_it_threads_on() {
let mut applicant = Party::new(1);
let request = wire::vetter_list_request(&applicant.did, COMMUNITY, &unfiltered_list()).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::VetterList));
let report = |thid: &str| {
Message::build(
wire::new_id(),
vta_sdk::protocols::PROBLEM_REPORT_TYPE.to_string(),
json!({
"code": "e.p.msg.bad-request",
"comment": "unsupported message type: … — Trust Tasks must be carried in the \
DIDComm binding envelope",
}),
)
.from(COMMUNITY.to_string())
.thid(thid.to_string())
.finalize()
};
let handled = applicant.receive(&report(&request.id), COMMUNITY).await;
let Some(CommunityAnswer::Refused {
query,
kind: QueryKind::VetterList,
code,
message,
..
}) = handled.answer
else {
panic!("the question is refused");
};
assert_eq!(query, request.id);
assert_eq!(code, "e.p.msg.bad-request");
assert!(message.unwrap().contains("binding envelope"));
assert!(applicant.book.queries.is_empty());
let resolver = TrustTaskVmResolver::did_key_only();
let did_resolver = did_resolver().await;
let issued: Option<
Result<
crate::issued_credential::VerifiedIssuedCredential,
crate::issued_credential::IssuedCredentialError,
>,
> = None;
let ctx = Context {
account: &applicant.account,
resolver: &resolver,
did_resolver: &did_resolver,
issued_credential: issued.as_ref(),
community_answer: None,
recipient: Some((applicant.persona, &applicant.did)),
now: Utc::now(),
};
assert!(
handle(
&mut applicant.book,
&ctx,
&mut crate::operational::SeenDocuments::default(),
&report("urn:uuid:not-ours"),
COMMUNITY
)
.await
.is_none()
);
}
#[tokio::test]
async fn a_resend_is_answered_or_refused_in_plain_words() {
let mut member = Party::new(5).member_of(COMMUNITY);
let request = wire::vetter_resend_request(&member.did, COMMUNITY).unwrap();
member
.book
.ask(asked(&request, member.persona, QueryKind::VetterResend));
let resent = vetters::resend::v0_1::Response::try_from(
vetters::resend::v0_1::Response::builder()
.credential_id("urn:uuid:grant")
.valid_until(Utc::now() + Duration::days(300)),
)
.unwrap();
let handled = member
.receive(&community_answer(&request, &resent).await, COMMUNITY)
.await;
assert!(matches!(
handled.answer,
Some(CommunityAnswer::Resent { .. })
));
assert!(matches!(handled.notice, Some(Notice::GrantResent { .. })));
let request = wire::vetter_resend_request(&member.did, COMMUNITY).unwrap();
member
.book
.ask(asked(&request, member.persona, QueryKind::VetterResend));
let handled = member
.receive(
&community_refusal(&request, VETTING_VETTER_RESEND_ERR_NOT_GRANTED),
COMMUNITY,
)
.await;
assert!(matches!(
handled.answer,
Some(CommunityAnswer::Refused {
kind: QueryKind::VetterResend,
..
})
));
let notice = handled.notice.expect("a refusal is explained");
assert!(notice.describe().contains("has not named you a vetter"));
}
#[tokio::test]
async fn a_manifest_answers_whoever_asked_and_brings_the_communitys_branding() {
let mut applicant = Party::new(1);
let request = wire::manifest_request(
&applicant.did,
COMMUNITY,
super::protocol::JoinProtocol::V0_2,
)
.unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::Manifest));
let branding = manifest::v0_2::CommunityBranding::try_from(
manifest::v0_2::CommunityBranding::builder()
.display_name(Some(
manifest::v0_2::CommunityBrandingDisplayName::try_from("Kernel Developers")
.unwrap(),
))
.accent_color(Some(
manifest::v0_2::CommunityBrandingAccentColor::try_from("#336699").unwrap(),
)),
)
.unwrap();
let body = manifest_with(Some(requirements()), Some(branding));
let handled = applicant
.receive(&community_answer(&request, &body).await, COMMUNITY)
.await;
assert!(matches!(
handled.answer,
Some(CommunityAnswer::Manifest { .. })
));
assert!(matches!(
applicant.book.knowledge(COMMUNITY),
Knowledge::Vetting(_)
));
let branding = applicant.book.branding(COMMUNITY).unwrap();
assert_eq!(branding.display_name.as_deref(), Some("Kernel Developers"));
assert_eq!(branding.accent_rgb(), Some((0x33, 0x66, 0x99)));
assert!(applicant.book.queries.is_empty());
}
async fn role_credential_with_status(issuer: &Secret, subject: &str) -> Value {
let credential = role_vac(issuer, COMMUNITY, subject, VETTER_ROLE);
let mut value = serde_json::to_value(&credential).unwrap();
value.as_object_mut().unwrap().remove("proof");
value["credentialStatus"] = json!({
"id": "https://vtc.example.com/v1/status-lists/revocation#7",
"type": "BitstringStatusListEntry",
"statusPurpose": "revocation",
"statusListIndex": "7",
"statusListCredential": "https://vtc.example.com/v1/status-lists/revocation"
});
let proof = DataIntegrityProof::sign(
&value,
issuer,
SignOptions::new().with_proof_purpose("assertionMethod"),
)
.await
.unwrap();
value["proof"] = serde_json::to_value(proof).unwrap();
value
}
#[tokio::test]
async fn a_verified_grant_is_handed_on_for_a_revocation_check() {
let (mut applicant, mut vetter, ticket) = ready().await;
let credential =
role_credential_with_status(&secret(COMMUNITY_SEED), &vetter.did.clone()).await;
vetter.book.keep_vetter_grant(super::book::VetterGrant {
community: COMMUNITY.to_string(),
persona: vetter.persona,
credential_id: credential["id"].as_str().map(str::to_string),
valid_until: credential["validUntil"]
.as_str()
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|d| d.with_timezone(&Utc)),
received_at: Utc::now(),
credential: credential.clone(),
});
let message = request(&mut applicant, &vetter, ticket).await;
let reply = vetter
.receive(&message, &applicant.did)
.await
.reply
.expect("accepted");
let handled = applicant
.receive(&signed_reply(reply, &vetter.secret).await, &vetter.did)
.await;
let check = handled
.grant_check
.expect("a verified grant with a status entry is checked");
assert_eq!(check.issuer, COMMUNITY);
assert_eq!(check.vetter, vetter.did);
assert_eq!(check.credential_status["statusListIndex"], "7");
let app = applicant.application();
assert!(matches!(
app.requests[0].grant_status,
Some(GrantStatus::Checking { .. })
));
app.record_grant_status(
&check.request_document_id,
&check.vetter,
GrantStatus::from_check(StatusCheck::Revoked, Utc::now()),
)
.unwrap();
assert!(matches!(
app.requests[0].grant_status,
Some(GrantStatus::Revoked { .. })
));
}
#[tokio::test]
async fn a_grant_that_names_no_status_list_is_not_called_unrevoked() {
let (mut applicant, mut vetter, ticket) = ready().await;
let message = request(&mut applicant, &vetter, ticket).await;
let reply = vetter
.receive(&message, &applicant.did)
.await
.reply
.unwrap();
let handled = applicant
.receive(&signed_reply(reply, &vetter.secret).await, &vetter.did)
.await;
assert!(handled.grant_check.is_none());
assert!(matches!(
applicant.application().requests[0].grant_status,
Some(GrantStatus::Unknown { .. })
));
}
#[tokio::test]
async fn a_scanned_ticket_link_is_enough_to_ask_and_another_communitys_is_refused() {
let (mut applicant, mut vetter, _) = ready().await;
let link = vetter.book.tickets[0].uri(&vetter.did).unwrap();
let ticket = applicant.application().ticket_from_uri(&link).unwrap();
assert_eq!(ticket.vetter, vetter.did);
let message = request(&mut applicant, &vetter, ticket.presentation).await;
let handled = vetter.receive(&message, &applicant.did).await;
assert!(matches!(
handled.notice,
Some(Notice::RequestAccepted { .. })
));
let elsewhere = Ticket::issue(
"did:key:zOtherCommunity",
vetter.persona,
vec![],
1,
DEFAULT_VALIDITY,
Utc::now(),
);
assert!(matches!(
applicant
.application()
.ticket_from_uri(&elsewhere.uri(&vetter.did).unwrap()),
Err(TicketUriError::OtherCommunity(c)) if c == "did:key:zOtherCommunity"
));
assert!(matches!(
applicant.application().ticket_from_uri("K7QF-2M9X"),
Err(TicketUriError::Unreadable(_))
));
}
#[tokio::test]
async fn the_next_step_follows_the_application() {
let fresh = Application::new(COMMUNITY, PersonaId::new(), "did:key:zA", Utc::now()).unwrap();
assert_eq!(fresh.next_step(Utc::now()), NextStep::LearnRequirements);
let (mut applicant, mut vetter, _) = ready().await;
assert_eq!(
applicant.application().next_step(Utc::now()),
NextStep::ChooseFace
);
applicant.application().face = Some(ChosenFace {
profile_id: "p1".into(),
name: "Work".into(),
});
assert_eq!(
applicant.application().next_step(Utc::now()),
NextStep::AskVetter
);
let (_, session_doc) = in_session(&mut applicant, &mut vetter).await;
assert_eq!(
applicant.application().next_step(Utc::now()),
NextStep::WaitForVetters
);
let message = signed(session_doc.clone(), &vetter.secret).await;
applicant.receive(&message, &vetter.did).await;
assert_eq!(
applicant.application().next_step(Utc::now()),
NextStep::SendCard {
session_id: session_doc.id.clone()
}
);
}
#[tokio::test]
async fn an_unsigned_or_altered_community_answer_is_not_acted_on() {
let mut applicant = Party::new(1);
applicant
.book
.start_application(COMMUNITY, applicant.persona, &applicant.did, Utc::now())
.unwrap();
let unsigned = Message::build(
wire::new_id(),
JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE.to_string(),
json!({
"type": JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE,
"issuer": COMMUNITY,
"payload": manifest_body(),
}),
)
.from(COMMUNITY.to_string())
.finalize();
let handled = applicant.receive(&unsigned, COMMUNITY).await;
assert!(!handled.changed && handled.notice.is_none());
let mut altered = manifest_reply(&applicant.did.clone()).await;
altered.body["payload"]["tampered"] = json!(true);
let handled = applicant.receive(&altered, COMMUNITY).await;
assert!(!handled.changed && handled.notice.is_none());
let handled = applicant
.receive(
&manifest_reply(&applicant.did.clone()).await,
"did:key:zElsewhere",
)
.await;
assert!(!handled.changed && handled.notice.is_none());
let handled = applicant
.receive(&manifest_reply(&applicant.did.clone()).await, COMMUNITY)
.await;
assert!(matches!(
handled.notice,
Some(Notice::RequirementsUpdated { .. })
));
}
#[tokio::test]
async fn a_community_answer_is_taken_from_its_off_loop_check() {
let mut applicant = Party::new(1);
applicant
.book
.start_application(COMMUNITY, applicant.persona, &applicant.did, Utc::now())
.unwrap();
let reply = manifest_reply(&applicant.did.clone()).await;
let refused = Err(crate::operational::OperationalError::NotChecked);
let handled = applicant.receive_checked(&reply, COMMUNITY, &refused).await;
assert!(!handled.changed && handled.notice.is_none());
let checked = crate::operational::verify_operational(
&reply.body,
COMMUNITY,
&[applicant.did.as_str()],
&reply.typ,
&did_resolver().await,
&applicant.seen,
Utc::now(),
)
.await;
let handled = applicant.receive_checked(&reply, COMMUNITY, &checked).await;
assert!(matches!(
handled.notice,
Some(Notice::RequirementsUpdated { .. })
));
}
#[tokio::test]
async fn a_tampered_vetter_grant_is_not_kept() {
let mut member = Party::new(2).member_of(COMMUNITY);
let mut credential = role_credential(
&secret(COMMUNITY_SEED),
COMMUNITY,
&member.did.clone(),
VETTER_ROLE,
)
.await;
credential["validUntil"] = json!("2999-01-01T00:00:00Z");
let handled = member
.receive(
&delivery(&credential, &secret(COMMUNITY_SEED)).await,
COMMUNITY,
)
.await;
assert!(handled.notice.is_none() && member.book.vetter_grants.is_empty());
}
#[tokio::test]
async fn a_grant_whose_status_cannot_be_read_is_not_kept() {
let mut vetter = Party::new(2).member_of(COMMUNITY);
let credential =
role_credential_with_status(&secret(COMMUNITY_SEED), &vetter.did.clone()).await;
let handled = vetter
.receive(
&delivery(&credential, &secret(COMMUNITY_SEED)).await,
COMMUNITY,
)
.await;
assert!(handled.notice.is_none() && vetter.book.vetter_grants.is_empty());
}
#[tokio::test]
async fn a_community_answer_is_taken_once_and_only_from_the_operational_key() {
let mut applicant = Party::new(1);
applicant
.book
.start_application(COMMUNITY, applicant.persona, &applicant.did, Utc::now())
.unwrap();
let asserted = crate::proof_check::test_support::sign(
json!({
"id": wire::new_id(),
"type": JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE,
"issuer": COMMUNITY,
"recipient": applicant.did,
"issuedAt": Utc::now().to_rfc3339(),
"payload": manifest_body(),
}),
&[&secret(COMMUNITY_SEED)],
)
.await;
let message = Message::build(
wire::new_id(),
JOIN_REQUEST_MANIFEST_0_2_RESPONSE_TYPE.to_string(),
asserted,
)
.from(COMMUNITY.to_string())
.finalize();
let handled = applicant.receive(&message, COMMUNITY).await;
assert!(handled.notice.is_none());
let reply = manifest_reply(&applicant.did.clone()).await;
let handled = applicant.receive(&reply, COMMUNITY).await;
assert!(matches!(
handled.notice,
Some(Notice::RequirementsUpdated { .. })
));
let mut again = reply.clone();
again.id = wire::new_id();
let handled = applicant.receive(&again, COMMUNITY).await;
assert!(
!handled.changed && handled.notice.is_none(),
"a replayed answer is refused"
);
let other = manifest_reply("did:key:zSomeoneElse").await;
let handled = applicant.receive(&other, COMMUNITY).await;
assert!(!handled.changed && handled.notice.is_none());
}
#[tokio::test]
async fn an_unsolicited_manifest_writes_nothing() {
let mut stranger = Party::new(7);
let reply = manifest_reply(&stranger.did.clone()).await;
let handled = stranger.receive(&reply, COMMUNITY).await;
assert!(!handled.changed && handled.notice.is_none() && handled.answer.is_none());
assert!(stranger.seen.is_empty(), "the replay set is untouched");
assert!(matches!(
stranger.book.knowledge(COMMUNITY),
Knowledge::Unknown
));
}
impl Party {
fn with_persona_record(mut self) -> Self {
self.account.personas.insert(
self.persona,
crate::config::account::PersonaRecord {
extra: serde_json::Map::new(),
persona_id: self.persona,
did: self.did.clone(),
did_document: None,
key_refs: Vec::new(),
mediator_did: None,
origin_context_id: String::new(),
created_at: Utc::now(),
label: None,
},
);
self
}
}
#[tokio::test]
async fn a_v0_3_manifest_is_learned_with_its_version_and_admission() {
use super::protocol::{Admission, JoinProtocol};
let mut applicant = Party::new(1);
let request = wire::manifest_request(&applicant.did, COMMUNITY, JoinProtocol::V0_3).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::Manifest));
let body = json!({
"communityDid": COMMUNITY,
"criteria": [
{ "id": "invited", "admission": "automatic", "invitationRequired": true,
"requirementsDigest": DIGEST },
{ "id": "vetted", "admission": "review", "requirementsDigest": DIGEST,
"vetting": serde_json::to_value(requirements()).unwrap() },
],
});
let handled = applicant
.receive(&community_answer(&request, &body).await, COMMUNITY)
.await;
assert!(
matches!(handled.answer, Some(CommunityAnswer::Manifest { .. })),
"{:?}",
handled.answer
);
assert_eq!(applicant.book.protocol_for(COMMUNITY), JoinProtocol::V0_3);
let routes = applicant.book.routes(COMMUNITY);
assert_eq!(routes.len(), 2);
assert_eq!(routes[0].admission, Some(Admission::Automatic));
assert_eq!(routes[0].invitation_required, Some(true));
assert!(routes[1].vetting && routes[1].admission == Some(Admission::Review));
assert!(matches!(
applicant.book.knowledge(COMMUNITY),
Knowledge::Vetting(_)
));
}
#[tokio::test]
async fn a_manifest_refused_as_an_unsupported_version_is_asked_again_in_0_2() {
use super::protocol::JoinProtocol;
let mut applicant = Party::new(1).with_persona_record();
let request = wire::manifest_request(&applicant.did, COMMUNITY, JoinProtocol::V0_3).unwrap();
applicant
.book
.ask(asked(&request, applicant.persona, QueryKind::Manifest));
let handled = applicant
.receive(
&community_refusal(&request, "unsupportedVersion"),
COMMUNITY,
)
.await;
assert!(
handled.answer.is_none(),
"not an answer: {:?}",
handled.answer
);
let reply = handled.reply.expect("asked again");
assert_eq!(
reply.document.type_uri.to_string(),
JoinProtocol::V0_2.manifest_type()
);
assert_eq!(reply.persona, applicant.persona);
assert_eq!(applicant.book.protocol_for(COMMUNITY), JoinProtocol::V0_2);
assert!(
applicant
.book
.waiting_on(COMMUNITY, QueryKind::Manifest)
.is_some_and(|q| q.document_id == reply.document.id),
"the new question is the one waited on"
);
let handled = applicant
.receive(
&community_refusal(&reply.document, "unsupportedVersion"),
COMMUNITY,
)
.await;
assert!(handled.reply.is_none(), "nothing older to ask in");
assert!(matches!(
handled.answer,
Some(CommunityAnswer::Refused {
kind: QueryKind::Manifest,
..
})
));
}
#[tokio::test]
async fn the_journeys_follow_the_exchange() {
use super::journey::{
ApplicantStep, StepState, VetterStep, applicant_journey, current, vetter_journey,
};
let (mut applicant, mut vetter, _) = ready().await;
let (request_id, _session) = in_session(&mut applicant, &mut vetter).await;
let entry = vetter.book.desk_entry(&request_id).expect("on the desk");
let (steps, ending) = vetter_journey(entry);
assert_eq!(ending, None);
assert_eq!(current(&steps), Some(VetterStep::Code));
let card = steps.iter().find(|s| s.step == VetterStep::Card).unwrap();
assert_eq!(card.state, StepState::Waiting, "the card is theirs to send");
let code = steps.iter().find(|s| s.step == VetterStep::Code).unwrap();
assert!(code.detail.is_some(), "the code is on the step");
let journey = applicant_journey(applicant.application(), Utc::now());
let sessions = journey
.iter()
.find(|s| s.step == ApplicantStep::Sessions)
.unwrap();
assert_ne!(sessions.state, StepState::Todo, "{journey:?}");
}
#[tokio::test]
async fn a_declines_reason_and_note_reach_the_applicant() {
let (mut applicant, mut vetter, _) = ready().await;
let (request_id, _) = in_session(&mut applicant, &mut vetter).await;
let body = vetter
.book
.decline(
&request_id,
Some(decline::v0_1::PayloadCode::DocumentMismatch),
Some("The name on your passport was spelled differently.".into()),
Utc::now(),
)
.unwrap();
let doc = wire::document(
VETTING_DECLINE_TYPE,
&vetter.did,
&applicant.did,
wire::new_id(),
&body,
)
.unwrap();
let message = signed(doc, &vetter.secret).await;
applicant.receive(&message, &vetter.did).await;
let request = applicant.application().requests.last().unwrap().clone();
match request.state {
RequestState::Declined { code, message } => {
assert_eq!(code, Some(decline::v0_1::PayloadCode::DocumentMismatch));
assert_eq!(
message.as_deref(),
Some("The name on your passport was spelled differently.")
);
}
other => panic!("declined, with its reason: {other:?}"),
}
}
#[tokio::test]
async fn a_finished_request_forgets_the_person_and_leaves_the_desk() {
use super::book::{CLOSED_GRACE, VettedOutcome};
let (mut applicant, mut vetter, _) = ready().await;
let resolver = TrustTaskVmResolver::did_key_only();
let (request_id, session_doc) = in_session(&mut applicant, &mut vetter).await;
let message = signed(session_doc.clone(), &vetter.secret).await;
let handled = applicant.receive(&message, &vetter.did).await;
let Some(Notice::SessionOpened { session_id, .. }) = handled.notice else {
panic!("the session opens");
};
let draft = applicant
.application()
.card_draft(
&session_id,
vec![
session::v0_1::VettingCardClaim::try_from(
session::v0_1::VettingCardClaim::builder()
.type_("name.legal")
.value(json!("Alice Example"))
.provenance("selfAsserted"),
)
.unwrap(),
],
Utc::now(),
)
.unwrap();
let card = sign_card(draft, &applicant.secret).await.unwrap();
applicant
.application()
.record_card(&session_id, &card, &resolver, Utc::now())
.await
.unwrap();
let doc = wire::response(&session_doc, &json!({ "card": card })).unwrap();
vetter
.receive(&signed(doc, &applicant.secret).await, &applicant.did)
.await;
let closed = Utc::now();
vetter
.book
.decline(&request_id, None, None, closed)
.unwrap();
let held = |book: &VettingBook| {
serde_json::to_string(book)
.unwrap()
.contains("Alice Example")
};
assert!(
held(&vetter.book),
"the card is there until the first prune"
);
assert!(vetter.book.prune(closed));
assert!(!held(&vetter.book), "the legal name is gone at once");
assert!(
vetter.book.desk_entry(&request_id).is_some(),
"a failed send must find it"
);
assert!(vetter.book.prune(closed + CLOSED_GRACE));
assert!(vetter.book.desk_entry(&request_id).is_none());
let record = vetter
.book
.vetted
.last()
.expect("a record that we vetted someone");
assert_eq!(record.outcome, VettedOutcome::Declined);
assert_eq!(record.community, COMMUNITY);
let stored = serde_json::to_string(&vetter.book.vetted).unwrap();
assert!(!stored.contains(&applicant.did), "no identifier: {stored}");
}
fn vetter_with_a_draw_in_flight(label: &str, tick: u32) -> (Party, TrustTask<Value>) {
use rand::SeedableRng;
let mut vetter = Party::new(7).member_of(COMMUNITY);
let params: super::hidden::HiddenParams = serde_json::from_value(hidden_params()).unwrap();
let mut rng = rand::rngs::StdRng::seed_from_u64(0x71C);
let mut snapshot =
super::hidden::vetter_start(COMMUNITY, ¶ms, &vetter.did, &mut rng).unwrap();
let body =
super::hidden::drip_request(COMMUNITY, ¶ms, &mut snapshot, tick, label, 3, &mut rng)
.unwrap();
assert_eq!(
snapshot.pending.len(),
3,
"the serials are kept for the answer"
);
vetter
.book
.hidden_vetter
.push(super::book::HiddenVetterState::new(
COMMUNITY,
vetter.persona,
params,
snapshot,
));
let request = wire::pcs_tokens_request(&vetter.did, COMMUNITY, &body).unwrap();
vetter
.book
.ask(asked(&request, vetter.persona, QueryKind::PcsTokens));
vetter
.book
.draws_in_flight
.insert(request.id.clone(), (label.to_string(), tick));
(vetter, request)
}
#[tokio::test]
async fn a_tick_not_begun_is_waited_for_and_a_served_one_is_done() {
let label = "token/2026-10";
let (mut vetter, request) = vetter_with_a_draw_in_flight(label, 4);
let handled = vetter
.receive(
&community_refusal(&request, super::hidden::TOKENS_TICK_NOT_YET),
COMMUNITY,
)
.await;
assert!(handled.notice.is_none(), "{:?}", handled.notice);
assert!(matches!(
&handled.answer,
Some(CommunityAnswer::Refused { kind, code, .. }) if !kind.refusal_is_news(code)
));
let state = &vetter.book.hidden_vetter[0];
assert!(state.retry_at.is_some_and(|t| t > Utc::now()));
assert!(state.last_ticks.is_empty(), "not served");
assert!(state.last_refusal.is_none(), "not a failure");
assert!(
state.snapshot.pending.is_empty(),
"the refused draw's serials are dropped"
);
assert!(vetter.book.draws_in_flight.is_empty());
let (mut vetter, request) = vetter_with_a_draw_in_flight(label, 4);
let handled = vetter
.receive(
&community_refusal(&request, super::hidden::TOKENS_ALREADY_SERVED),
COMMUNITY,
)
.await;
assert!(handled.notice.is_none());
assert_eq!(vetter.book.hidden_vetter[0].last_ticks.get(label), Some(&4));
}
#[tokio::test]
async fn any_other_drip_refusal_is_said_in_words_and_kept() {
let (mut vetter, request) = vetter_with_a_draw_in_flight("token/2026-10", 4);
let handled = vetter
.receive(
&community_refusal(&request, "vtc/vetting/vetters/pcs-tokens:labelNotLive"),
COMMUNITY,
)
.await;
let notice = handled.notice.expect("a refusal that is news");
assert!(
notice
.describe()
.contains("not issuing tokens under that label"),
"{}",
notice.describe()
);
let refusal = vetter.book.hidden_vetter[0]
.last_refusal
.clone()
.expect("kept for the desk");
assert_eq!(refusal.code, "vtc/vetting/vetters/pcs-tokens:labelNotLive");
assert!(vetter.book.hidden_vetter[0].last_ticks.is_empty());
}
#[test]
fn the_engine_follows_the_published_labels_and_stops_at_new_keys() {
use super::book::{Adopted, HiddenVetterState};
use super::hidden::{Due, HiddenParams, due};
let enrolled: HiddenParams = serde_json::from_value(hidden_params()).unwrap();
let mut snapshot = openvtc_vetting_pcs::snapshot::VetterSnapshot::without_keys("member-1");
snapshot
.credentials
.insert("2026-10".into(), "zOctober".into());
snapshot
.credentials
.insert("2026-11".into(), "zNovember".into());
let mut state = HiddenVetterState::new(COMMUNITY, PersonaId::new(), enrolled.clone(), snapshot);
let november = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 11, 2, 9, 0, 0).unwrap();
let live = HiddenParams {
vetter_labels: vec!["vetter/2026-11".into(), "vetter/2026-10".into()],
token_labels: vec!["token/2026-11".into(), "token/2026-10".into()],
tick_length: Some("P1D".into()),
..enrolled.clone()
};
assert_eq!(state.adopt_published(&live, november), Adopted::Updated);
assert_eq!(state.adopt_published(&live, november), Adopted::Unchanged);
assert_eq!(
due(
&state.params,
Some(&state.snapshot),
&state.last_ticks,
&[],
november
),
Due::Draw {
label: "token/2026-11".into(),
tick: 0,
rate: state.params.drip_per_tick,
},
"drawn under the live label, oldest tick first"
);
let rekeyed = HiddenParams {
token_key: "zSomeOtherTokenKey".into(),
..live.clone()
};
assert_eq!(
state.adopt_published(&rekeyed, november),
Adopted::Rekeyed { first: true }
);
assert_eq!(
state.adopt_published(&rekeyed, november),
Adopted::Rekeyed { first: false }
);
assert_eq!(state.params, live, "the keys we enrolled under stay");
assert!(state.rekeyed_at.is_some());
let plan = state.plan(Some(&rekeyed), &[], november);
assert!(plan.owed.is_empty(), "the drip stops: {:?}", plan.owed);
let plan = state.plan(Some(&live), &[], november);
assert_eq!(plan.adopted, Adopted::Updated);
assert!(state.rekeyed_at.is_none());
assert_eq!(
plan.owed.len(),
2,
"ticks 0 and 1 of November: {:?}",
plan.owed
);
let plan = state.plan(Some(&live), &[("token/2026-11".into(), 0)], november);
assert_eq!(
plan.owed,
[Due::Draw {
label: "token/2026-11".into(),
tick: 1,
rate: state.params.drip_per_tick,
}]
);
state.retry_at = Some(november + Duration::minutes(5));
assert!(state.plan(Some(&live), &[], november).owed.is_empty());
}
fn manifest_publishing(params: &Value) -> Value {
json!({
"criteria": [{
"id": "c1",
"vetting": {
"minStatements": 1,
"ext": { super::hidden::HIDDEN_VETTING_NS: params }
}
}]
})
}
fn vetter_drawing_at(rate: usize) -> (VettingBook, Value) {
let mut published = hidden_params();
published["dripPerTick"] = json!(rate);
let params: super::hidden::HiddenParams = serde_json::from_value(published.clone()).unwrap();
let mut snapshot = openvtc_vetting_pcs::snapshot::VetterSnapshot::without_keys("member-1");
snapshot
.credentials
.insert("2026-10".into(), "zOctober".into());
let mut book = VettingBook::default();
book.hidden_vetter.push(super::book::HiddenVetterState::new(
COMMUNITY,
PersonaId::new(),
params,
snapshot,
));
(book, published)
}
#[test]
fn a_manifest_read_moves_the_rate_the_next_draw_asks_for() {
use super::hidden::Due;
let (mut book, mut published) = vetter_drawing_at(100);
let october = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 10, 5, 21, 16, 0).unwrap();
assert!(
book.hidden_vetter[0].draw_hold(false, october).is_some(),
"never read from the community: no draw yet"
);
published["dripPerTick"] = json!(20);
book.learn_mode(COMMUNITY, &manifest_publishing(&published), None, october);
let held = &mut book.hidden_vetter[0];
assert_eq!(held.params.drip_per_tick, 20, "taken at the read");
assert_eq!(held.params_read_at, Some(october));
assert_eq!(held.draw_hold(false, october), None);
let plan = held.plan(book.hidden_published.get(COMMUNITY), &[], october);
assert!(!plan.owed.is_empty());
for owed in &plan.owed {
assert!(
matches!(owed, Due::Draw { rate: 20, .. }),
"drawn at the rate read: {owed:?}"
);
}
assert_eq!(
held.draw_hold(true, october),
Some(super::book::DrawHold::Reading)
);
assert_eq!(
held.draw_hold(
false,
october + super::book::PARAMS_FRESH_FOR + Duration::seconds(1)
),
Some(super::book::DrawHold::Unread)
);
}
#[test]
fn a_read_a_pass_waited_for_runs_the_schedule_again() {
let (mut book, published) = vetter_drawing_at(20);
book.hidden_vetter[0].awaiting_read = true;
book.vetter_refresh_due = false;
book.learn_mode(
COMMUNITY,
&manifest_publishing(&published),
None,
Utc::now(),
);
assert!(book.vetter_refresh_due);
assert!(!book.hidden_vetter[0].awaiting_read);
}
#[tokio::test]
async fn an_over_quota_refusal_rereads_the_rate_before_drawing_again() {
use super::book::DrawHold;
use super::hidden::Due;
let label = "token/2026-10";
let october = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 10, 5, 21, 16, 0).unwrap();
let refusal = |request: &TrustTask<Value>, detail: &str| {
wire::to_message(
&wire::refusal(request, super::hidden::TOKENS_OVER_QUOTA, Some(detail)).unwrap(),
)
.unwrap()
};
let setup = || {
let (mut vetter, request) = vetter_with_a_draw_in_flight(label, 1);
let held = &mut vetter.book.hidden_vetter[0];
held.params.drip_per_tick = 100;
held.params_read_at = Some(Utc::now() - Duration::minutes(1));
held.snapshot
.credentials
.insert("2026-10".into(), "zOctober".into());
vetter.book.vetter_refresh_due = false;
vetter
.book
.mode_asked(COMMUNITY, Utc::now() - Duration::minutes(1));
(vetter, request)
};
let (mut vetter, request) = setup();
let handled = vetter
.receive(
&refusal(
&request,
"drip refused: asked for 100 tokens; this community drips 20 a tick",
),
COMMUNITY,
)
.await;
let said = handled.notice.expect("news").describe();
assert!(said.contains("being read again"), "{said}");
assert!(
!said.contains("next pass asks for the published rate"),
"{said}"
);
assert!(vetter.book.vetter_refresh_due, "the re-read is asked now");
assert!(
vetter.book.params_reread_owed(COMMUNITY),
"owed despite a read a minute ago"
);
let now = Utc::now();
let held = &vetter.book.hidden_vetter[0];
assert_eq!(held.draw_hold(false, now), Some(DrawHold::Unread));
let q = held.over_quota.clone().expect("kept");
assert_eq!((q.asked, q.allowed), (100, Some(20)));
vetter.book.mode_asked(COMMUNITY, now);
assert!(!vetter.book.params_reread_owed(COMMUNITY));
let published = serde_json::to_value(&vetter.book.hidden_vetter[0].params).unwrap();
let later = now + Duration::seconds(2);
vetter
.book
.learn_mode(COMMUNITY, &manifest_publishing(&published), None, later);
let held = &mut vetter.book.hidden_vetter[0];
assert_eq!(held.draw_hold(false, later), None);
let plan = held.plan(None, &[], october);
assert!(!plan.owed.is_empty());
assert!(
plan.owed
.iter()
.all(|d| matches!(d, Due::Draw { rate: 20, .. })),
"{:?}",
plan.owed
);
let (mut vetter, request) = setup();
vetter
.receive(&refusal(&request, "because"), COMMUNITY)
.await;
let mut published = serde_json::to_value(&vetter.book.hidden_vetter[0].params).unwrap();
let later = Utc::now() + Duration::seconds(2);
vetter
.book
.learn_mode(COMMUNITY, &manifest_publishing(&published), None, later);
assert!(matches!(
vetter.book.hidden_vetter[0].draw_hold(false, later),
Some(DrawHold::Disagrees {
asked: 100,
published: 100,
..
})
));
published["dripPerTick"] = json!(20);
let later = later + Duration::seconds(1);
vetter
.book
.learn_mode(COMMUNITY, &manifest_publishing(&published), None, later);
let held = &mut vetter.book.hidden_vetter[0];
assert_eq!(held.draw_hold(false, later), None);
assert!(held.over_quota.is_none());
assert!(
held.plan(None, &[], october)
.owed
.iter()
.all(|d| matches!(d, Due::Draw { rate: 20, .. }))
);
}
#[test]
fn an_over_quota_refusal_is_read_for_its_numbers() {
use super::book::{over_quota_allowed, over_quota_asked};
let text = "drip refused: asked for 100 tokens; this community drips 20 a tick";
assert_eq!(over_quota_asked(text), Some(100));
assert_eq!(over_quota_allowed(text), Some(20));
assert_eq!(over_quota_allowed("because"), None);
}
#[test]
fn a_pass_reenrols_when_a_new_vetter_label_is_live() {
use super::book::HiddenVetterState;
use super::hidden::{Due, HiddenParams};
let enrolled: HiddenParams = serde_json::from_value(hidden_params()).unwrap();
let mut snapshot = openvtc_vetting_pcs::snapshot::VetterSnapshot::without_keys("member-1");
snapshot
.credentials
.insert("2026-10".into(), "zOctober".into());
let mut state = HiddenVetterState::new(COMMUNITY, PersonaId::new(), enrolled.clone(), snapshot);
let november = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 11, 2, 9, 0, 0).unwrap();
assert!(
state.plan(None, &[], november).owed.is_empty(),
"October's labels in November owe nothing"
);
let live = HiddenParams {
vetter_labels: vec!["vetter/2026-11".into(), "vetter/2026-10".into()],
token_labels: vec!["token/2026-11".into(), "token/2026-10".into()],
..enrolled
};
assert_eq!(
state.plan(Some(&live), &[], november).owed,
[Due::Enrol {
period: "2026-11".into()
}]
);
}
#[tokio::test]
async fn a_request_on_the_desk_makes_the_vetter_side_due_a_refresh() {
let (mut applicant, mut vetter, _) = ready().await;
vetter.book.vetter_refresh_due = false;
in_session(&mut applicant, &mut vetter).await;
assert!(
vetter.book.vetter_refresh_due,
"the accepted request asks for a re-read"
);
assert!(
!serde_json::to_string(&vetter.book)
.unwrap()
.contains("vetter_refresh_due"),
"a run-only flag is never saved"
);
}
fn issuing_community() -> (
openvtc_vetting_pcs::issuer::Issuer,
super::hidden::HiddenParams,
) {
let issuer = openvtc_vetting_pcs::issuer::Issuer::derive(COMMUNITY, &[0x5E; 32]).unwrap();
let (helper_key, token_key) = issuer.public_text().unwrap();
let params = super::hidden::HiddenParams {
suite: super::hidden::SUITE.into(),
helper_key,
token_key,
vetter_labels: vec!["vetter/2026-10".into()],
token_labels: vec!["token/2026-10".into()],
drip_per_tick: 10,
events: Vec::new(),
tick_length: None,
};
(issuer, params)
}
async fn vetter_asking_to_enrol() -> (Party, TrustTask<Value>, Message) {
use rand::SeedableRng;
let (issuer, params) = issuing_community();
let mut vetter = Party::new(7).member_of(COMMUNITY);
let mut rng = rand::rngs::StdRng::seed_from_u64(0xE1);
let snapshot = super::hidden::vetter_start(COMMUNITY, ¶ms, &vetter.did, &mut rng).unwrap();
let (body, blinding) =
super::hidden::enrolment_request(COMMUNITY, ¶ms, &snapshot, "2026-10", &mut rng)
.unwrap();
vetter
.book
.hidden_vetter
.push(super::book::HiddenVetterState::new(
COMMUNITY,
vetter.persona,
params,
snapshot,
));
let request = wire::pcs_root_request(&vetter.did, COMMUNITY, &body).unwrap();
vetter
.book
.ask(asked(&request, vetter.persona, QueryKind::PcsRoot));
vetter
.book
.remember_enrolment(super::book::PendingEnrolment {
document_id: request.id.clone(),
community: COMMUNITY.into(),
persona: vetter.persona,
blinding: std::sync::Arc::new(blinding),
});
let id = openvtc_vetting_pcs::scheme::point_from_text(&body.id).unwrap();
let pre = issuer
.issue_root(
"2026-10",
&id,
&serde_json::from_value(body.request.clone()).unwrap(),
&mut rng,
)
.unwrap();
let answer = wire::pcs::RootResponse {
label: "vetter/2026-10".into(),
pre_credential: openvtc_vetting_pcs::scheme::enc(&pre).unwrap(),
ext: None,
};
let message = community_answer(&request, &answer).await;
(vetter, request, message)
}
#[tokio::test]
async fn an_enrolment_answered_after_its_question_timed_out_is_still_taken() {
let (mut vetter, _request, answer) = vetter_asking_to_enrol().await;
let now = Utc::now();
let expired = vetter
.book
.expire_queries(now + Duration::seconds(31), super::queries::QUERY_TIMEOUT);
assert_eq!(expired.len(), 1, "the question timed out");
let retry = vetter.book.hidden_vetter[0].unanswered_at(now);
assert_eq!(
retry,
now + Duration::minutes(1),
"and is asked again, backed off"
);
let handled = vetter.receive(&answer, COMMUNITY).await;
assert!(
matches!(handled.notice, Some(Notice::HiddenVetterEnrolled { ref label, .. }) if label == "vetter/2026-10"),
"{:?}",
handled.notice
);
let held = &vetter.book.hidden_vetter[0];
assert!(
held.snapshot.credentials.contains_key("2026-10"),
"the credential is ours"
);
assert_eq!(held.unanswered, 0, "an answer resets the backoff");
assert!(held.retry_at.is_none());
assert!(held.lost_enrolment.is_none());
assert!(
vetter.book.pending_enrolments.is_empty(),
"the blinding state is spent"
);
assert!(
vetter.book.vetter_refresh_due,
"the ticks already owed are drawn now, not at the next hourly pass"
);
let outlook = vetter
.book
.hidden_outlook(COMMUNITY, vetter.persona, now)
.unwrap();
assert!(outlook.enrolled);
}
#[tokio::test]
async fn an_enrolment_answer_that_cannot_be_opened_stops_the_asking_until_the_next_label() {
let (mut vetter, _request, answer) = vetter_asking_to_enrol().await;
vetter.book.pending_enrolments.clear();
let handled = vetter.receive(&answer, COMMUNITY).await;
let notice = handled.notice.expect("said");
assert!(matches!(notice, Notice::HiddenEnrolmentLost { .. }));
assert!(
notice.describe().contains("one credential per label"),
"{}",
notice.describe()
);
let october = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 10, 5, 15, 0, 0).unwrap();
let held = &mut vetter.book.hidden_vetter[0];
assert_eq!(held.lost_enrolment.as_deref(), Some("2026-10"));
assert_eq!(
held.plan(None, &[], october).owed,
[super::hidden::Due::Enrol {
period: "2026-10".into()
}],
"asked once more: the community may re-issue to the same identifier"
);
assert!(
held.plan(None, &[], october).owed.is_empty(),
"and only once"
);
let november = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 11, 1, 9, 0, 0).unwrap();
let live = super::hidden::HiddenParams {
vetter_labels: vec!["vetter/2026-11".into(), "vetter/2026-10".into()],
token_labels: vec!["token/2026-11".into(), "token/2026-10".into()],
..held.params.clone()
};
assert_eq!(
held.plan(Some(&live), &[], november).owed,
[super::hidden::Due::Enrol {
period: "2026-11".into()
}]
);
}
#[tokio::test]
async fn already_enrolled_without_a_credential_asks_once_more_then_stops() {
let (mut vetter, request, _) = vetter_asking_to_enrol().await;
let refusal = community_refusal(&request, "vtc/vetting/vetters/pcs-root:alreadyEnrolled");
vetter.receive(&refusal, COMMUNITY).await;
let october = chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 10, 5, 15, 0, 0).unwrap();
let outlook = vetter
.book
.hidden_outlook(COMMUNITY, vetter.persona, october)
.unwrap();
assert!(
!outlook.enrolment_lost,
"one more ask to come, so not given up on"
);
let held = &mut vetter.book.hidden_vetter[0];
assert_eq!(held.lost_enrolment.as_deref(), Some("2026-10"));
assert_eq!(held.plan(None, &[], october).owed.len(), 1, "one more ask");
assert!(held.lost_reasked);
assert!(held.plan(None, &[], october).owed.is_empty());
assert!(
vetter.book.pending_enrolments.is_empty(),
"a refused request issued nothing to open"
);
let outlook = vetter
.book
.hidden_outlook(COMMUNITY, vetter.persona, october)
.unwrap();
assert!(outlook.enrolment_lost);
}
#[tokio::test]
async fn attesting_without_a_credential_or_a_token_says_which() {
use super::hidden::HiddenError;
use rand::SeedableRng;
let (mut vetter, _request, answer) = vetter_asking_to_enrol().await;
let mut rng = rand::rngs::StdRng::seed_from_u64(0xA7);
let params = vetter.book.hidden_vetter[0].params.clone();
let applicant =
super::hidden::start(COMMUNITY, ¶ms, "did:example:applicant", &mut rng).unwrap();
let applicant_id = applicant.id.clone();
let meta = || openvtc_vetting_pcs::meta::StatementMeta {
community: COMMUNITY.into(),
requirements_digest: "zDigest".into(),
method: vta_sdk::protocols::vetting::VettingMethod::Video,
claims_verified: Vec::new(),
liveness_confirmed: true,
declared_relationship: vta_sdk::protocols::vetting::VettingRelationship::None,
identity_commitment: "zCommit".into(),
card_digest_multibase: "zCard".into(),
valid_from: Utc::now().date_naive(),
valid_until: Utc::now().date_naive(),
token_label: String::new(),
token_serial: String::new(),
};
let mut snapshot = vetter.book.hidden_vetter[0].snapshot.clone();
let err = super::hidden::attest(
COMMUNITY,
¶ms,
&mut snapshot,
&applicant_id,
meta(),
&mut rng,
)
.unwrap_err();
assert_eq!(err, HiddenError::NotEnrolled);
assert!(!err.to_string().contains("could not be read"), "{err}");
vetter.receive(&answer, COMMUNITY).await;
let mut snapshot = vetter.book.hidden_vetter[0].snapshot.clone();
let err = super::hidden::attest(
COMMUNITY,
¶ms,
&mut snapshot,
&applicant_id,
meta(),
&mut rng,
)
.unwrap_err();
assert_eq!(err, HiddenError::NoToken);
assert!(err.to_string().contains("no vetting token"), "{err}");
assert!(!err.to_string().contains("could not be read"), "{err}");
}
#[test]
fn an_unanswered_question_is_asked_again_backed_off_and_bounded() {
use super::book::{HIDDEN_RETRY_CAP, hidden_retry_after};
assert_eq!(hidden_retry_after(1), Duration::minutes(1));
assert_eq!(hidden_retry_after(2), Duration::minutes(2));
assert_eq!(hidden_retry_after(3), Duration::minutes(4));
assert_eq!(hidden_retry_after(7), HIDDEN_RETRY_CAP);
assert_eq!(hidden_retry_after(u32::MAX), HIDDEN_RETRY_CAP);
}
#[tokio::test]
async fn a_draw_answered_after_its_question_timed_out_is_still_taken() {
use openvtc_vetting_pcs::vtc::Vtc;
use rand::SeedableRng;
let label = "token/2026-10";
let (mut vetter, request) = vetter_with_a_draw_in_flight(label, 1);
vetter.book.expire_queries(
Utc::now() + Duration::seconds(31),
super::queries::QUERY_TIMEOUT,
);
assert!(vetter.book.queries.is_empty(), "the question timed out");
let mut rng = rand::rngs::StdRng::seed_from_u64(0x0BED);
let mut vtc = Vtc::new(
COMMUNITY,
"2026-10",
serde_json::to_value(requirements()).unwrap(),
&mut rng,
)
.unwrap();
assert_eq!(vtc.current_token_label(), label);
vtc.grant(&vetter.did);
let asked: wire::pcs::TokensRequest = serde_json::from_value(request.payload.clone()).unwrap();
let requests = asked
.requests
.iter()
.map(|r| {
openvtc_vetting_pcs::issuer::TokenRequestWire {
commitment: r.commitment.clone(),
opening_proof: r.opening_proof.clone(),
}
.to_request()
.unwrap()
})
.collect::<Vec<_>>();
let pres = vtc
.drip(&vetter.did, 1, label, &requests, &mut rng)
.unwrap();
let served = wire::pcs::TokensResponse {
label: label.into(),
tick: 1,
pre_credentials: pres
.iter()
.map(|p| openvtc_vetting_pcs::scheme::enc(p).unwrap())
.collect(),
ext: None,
};
let handled = vetter
.receive(&community_answer(&request, &served).await, COMMUNITY)
.await;
assert!(
matches!(
handled.notice,
Some(Notice::HiddenTokensDrawn { taken: 3, .. })
),
"{:?}",
handled.notice
);
let held = &vetter.book.hidden_vetter[0];
assert_eq!(held.tokens().1, 3);
assert_eq!(held.last_ticks.get(label), Some(&1));
}
#[tokio::test]
async fn a_new_vetter_enrols_now_rather_than_at_the_next_pass() {
let mut vetter = Party::new(2).member_of(COMMUNITY);
assert!(!vetter.book.vetter_refresh_due);
vetter.named_vetter().await;
assert!(
vetter.book.vetter_refresh_due,
"the grant asks for the vetter side to run at once"
);
}
#[tokio::test]
async fn an_enrolment_answer_after_a_restart_is_opened_with_the_stored_blinding() {
let (mut vetter, request, answer) = vetter_asking_to_enrol().await;
let pending = vetter.book.pending_enrolments.remove(0);
let stored = super::hidden::blinding_text(&pending.blinding).unwrap();
vetter.book.hidden_vetter[0].remember_asked(super::book::AskedEnrolment {
document_id: request.id.clone(),
period: "2026-10".into(),
blinding: stored,
asked_at: Utc::now(),
});
let saved = serde_json::to_value(&vetter.book).unwrap();
vetter.book = serde_json::from_value(saved).unwrap();
assert!(vetter.book.pending_enrolments.is_empty());
assert!(vetter.book.queries.is_empty());
let handled = vetter.receive(&answer, COMMUNITY).await;
assert!(
matches!(handled.notice, Some(Notice::HiddenVetterEnrolled { .. })),
"{:?}",
handled.notice
);
let held = &vetter.book.hidden_vetter[0];
assert!(held.snapshot.credentials.contains_key("2026-10"));
assert!(held.lost_enrolment.is_none());
assert!(held.enrolments_asked.is_empty(), "spent once opened");
assert!(vetter.book.vetter_refresh_due, "and the first draw follows");
}
#[tokio::test]
async fn a_refused_enrolment_drops_its_stored_blinding() {
let (mut vetter, request, _) = vetter_asking_to_enrol().await;
let stored = super::hidden::blinding_text(&vetter.book.pending_enrolments[0].blinding).unwrap();
vetter.book.hidden_vetter[0].remember_asked(super::book::AskedEnrolment {
document_id: request.id.clone(),
period: "2026-10".into(),
blinding: stored,
asked_at: Utc::now(),
});
vetter
.receive(
&community_refusal(&request, "vtc/vetting/vetters/pcs-root:notAVetter"),
COMMUNITY,
)
.await;
assert!(vetter.book.hidden_vetter[0].enrolments_asked.is_empty());
}
#[test]
fn a_manifest_is_not_asked_for_again_within_seconds() {
let now = Utc::now();
let mut book = VettingBook::default();
assert!(!book.manifest_recently_asked(COMMUNITY, now));
book.mode_asked(COMMUNITY, now);
assert!(book.manifest_recently_asked(COMMUNITY, now + Duration::seconds(5)));
assert!(!book.manifest_recently_asked(COMMUNITY, now + super::mode::MANIFEST_MIN_GAP));
book.ask(super::queries::CommunityQuery {
document_id: "m1".into(),
community: COMMUNITY.into(),
persona: PersonaId::new(),
kind: QueryKind::Manifest,
sent_at: now,
});
assert!(
book.manifest_recently_asked(COMMUNITY, now + Duration::minutes(5)),
"an open question is answered once, for everyone"
);
}
const FIRST_VTC_MANIFEST: &str = include_str!("../../tests/fixtures/first-vtc-manifest-0.3.json");
fn first_vtc() -> (
String,
manifest::v0_2::Response,
Vec<super::protocol::CriterionMeta>,
Value,
) {
let document: Value = serde_json::from_str(FIRST_VTC_MANIFEST).unwrap();
let payload = document["payload"].clone();
let community = payload["communityDid"].as_str().unwrap().to_string();
let (parsed, meta) =
super::protocol::read_manifest(super::protocol::JoinProtocol::V0_3, &payload)
.expect("the live manifest parses");
(community, parsed, meta, payload)
}
#[tokio::test]
async fn an_application_started_from_the_book_takes_up_first_vtcs_hidden_vetting() {
let (community, parsed, meta, raw) = first_vtc();
let mut party = Party::new(1);
let now = Utc::now();
party.book.learn_manifest_in(
&community,
&parsed,
Some(super::protocol::JoinProtocol::V0_3),
&meta,
now,
);
party.book.learn_mode(&community, &raw, None, now);
let id = party
.book
.start_application(&community, party.persona, &party.did, now)
.unwrap()
.id
.clone();
assert!(party.book.adopt_known_requirements(&id).unwrap());
let app = party
.book
.application_mut(&community, party.persona)
.unwrap();
assert_eq!(app.criterion_id.as_deref(), Some("vetted-member"));
assert!(app.hidden.is_some(), "the hidden path, from the book");
assert!(app.hidden_id().is_some(), "with its own key minted");
assert!(!app.adopt_manifest(&parsed, &raw).unwrap());
assert!(app.hidden.is_some());
let shown = party.book.application_vetting(&id).unwrap();
assert_eq!(shown.path, super::applicant::VettingPath::Hidden);
assert_eq!(shown.paths, super::book::CriterionPaths::Either);
assert_eq!(
shown.description.as_deref(),
Some("One vetter must confirm who you are")
);
}
const NAMED_DIGEST: &str = "zQmNamedCriterionDigestAbcdefgh";
fn mixed_manifest(critical: bool) -> (manifest::v0_2::Response, Value) {
let hidden = manifest_raw_with_ext(
json!({ super::hidden::HIDDEN_VETTING_NS: hidden_params() }),
critical.then(|| json!([super::hidden::HIDDEN_VETTING_NS])),
);
let mut named = hidden["criteria"][0].clone();
named["id"] = json!("named");
named["requirementsDigest"] = json!(NAMED_DIGEST);
named["description"] = json!("Two people who know you");
let vetting = named["vetting"].as_object_mut().unwrap();
vetting.remove("ext");
vetting.remove("extCritical");
let mut raw = hidden.clone();
raw["criteria"] = json!([named, hidden["criteria"][0].clone()]);
(serde_json::from_value(raw.clone()).unwrap(), raw)
}
fn hidden_manifest(critical: bool) -> (manifest::v0_2::Response, Value) {
let raw = manifest_raw_with_ext(
json!({ super::hidden::HIDDEN_VETTING_NS: hidden_params() }),
critical.then(|| json!([super::hidden::HIDDEN_VETTING_NS])),
);
(serde_json::from_value(raw.clone()).unwrap(), raw)
}
fn learn(book: &mut VettingBook, manifest: &(manifest::v0_2::Response, Value)) {
let now = Utc::now();
book.learn_manifest_in(COMMUNITY, &manifest.0, None, &[], now);
book.learn_mode(COMMUNITY, &manifest.1, None, now);
}
fn sent_request() -> super::applicant::OutboundRequest {
let now = Utc::now();
super::applicant::OutboundRequest {
document_id: "urn:uuid:sent".into(),
vetter: "did:key:zVetter".into(),
state: RequestState::Sent,
eligibility: None,
grant_status: None,
sent_at: now,
updated_at: now,
}
}
fn held_statement() -> super::applicant::HeldStatement {
let now = Utc::now();
super::applicant::HeldStatement {
id: "urn:uuid:held".into(),
vetter: "did:key:zVetter".into(),
method: VettingMethod::InPerson,
declared_relationship: VettingRelationship::None,
document_classes: vec![],
claims_verified: vec!["name.legal".into()],
identity_commitment: "z".into(),
valid_from: now,
valid_until: now + Duration::days(30),
received_at: now,
credential: Value::Null,
}
}
#[tokio::test]
async fn in_a_mixed_community_a_new_application_picks_the_hidden_criterion() {
let manifest = mixed_manifest(false);
let mut party = Party::new(1);
learn(&mut party.book, &manifest);
let now = Utc::now();
let id = party
.book
.start_application(COMMUNITY, party.persona, &party.did, now)
.unwrap()
.id
.clone();
party.book.adopt_known_requirements(&id).unwrap();
let app = party.application();
assert_eq!(app.criterion_id.as_deref(), Some("vetted"));
assert!(app.hidden.is_some());
let mut fresh = Application::new(COMMUNITY, party.persona, &party.did, now).unwrap();
fresh.adopt_manifest(&manifest.0, &manifest.1).unwrap();
assert_eq!(fresh.criterion_id.as_deref(), Some("vetted"));
assert!(fresh.hidden.is_some());
let mut under_way = Application::new(COMMUNITY, party.persona, &party.did, now).unwrap();
under_way.criterion_id = Some("named".into());
under_way.requests.push(sent_request());
under_way.adopt_manifest(&manifest.0, &manifest.1).unwrap();
assert_eq!(under_way.criterion_id.as_deref(), Some("named"));
assert!(under_way.hidden.is_none());
}
#[tokio::test]
async fn an_application_under_way_keeps_its_named_path_under_a_criterion_offering_both() {
let now = Utc::now();
let mut app = Application::new(COMMUNITY, PersonaId::new(), "did:key:zA", now).unwrap();
app.requests.push(sent_request());
let alongside = hidden_manifest(false);
app.adopt_manifest(&alongside.0, &alongside.1).unwrap();
assert!(app.hidden.is_none(), "kept named");
let critical = hidden_manifest(true);
app.adopt_manifest(&critical.0, &critical.1).unwrap();
assert!(app.hidden.is_some(), "the proof is the only path");
}
#[tokio::test]
async fn an_application_switches_criterion_and_path_before_evidence_only() {
use super::applicant::VettingPath;
use super::book::CriterionPaths;
let manifest = mixed_manifest(false);
let mut party = Party::new(1);
learn(&mut party.book, &manifest);
let id = party
.book
.start_application(COMMUNITY, party.persona, &party.did, Utc::now())
.unwrap()
.id
.clone();
party.book.adopt_known_requirements(&id).unwrap();
let options = party.book.vetting_options(COMMUNITY);
let ways: Vec<(&str, VettingPath)> = options
.iter()
.map(|o| (o.criterion_id.as_str(), o.path))
.collect();
assert_eq!(
ways,
vec![
("named", VettingPath::Named),
("vetted", VettingPath::Hidden),
("vetted", VettingPath::Named),
]
);
let now = party.book.switch_vetting(&id).unwrap();
assert_eq!(
(now.criterion_id.as_str(), now.path, now.paths),
("vetted", VettingPath::Named, CriterionPaths::Either)
);
assert!(party.application().hidden.is_none());
assert!(
party.application().hidden_state.is_some(),
"its key is kept for a switch back"
);
let now = party.book.switch_vetting(&id).unwrap();
assert_eq!(now.criterion_id, "named");
assert_eq!(
party.application().requirements_digest.as_deref(),
Some(NAMED_DIGEST)
);
assert_eq!(now.description.as_deref(), Some("Two people who know you"));
assert!(
party.application().criterion_repicked.is_none(),
"a switch is not a re-pick"
);
let now = party.book.switch_vetting(&id).unwrap();
assert_eq!(
(now.criterion_id.as_str(), now.path),
("vetted", VettingPath::Hidden)
);
assert!(party.application().hidden.is_some());
assert!(party.application().hidden_id().is_some());
party.application().statements.push(held_statement());
let refused = party.book.switch_vetting(&id).unwrap_err();
assert!(refused.contains("already holds vetting"), "{refused}");
}
#[tokio::test]
async fn a_stale_criterion_is_repicked_and_said() {
let manifest = mixed_manifest(false);
let now = Utc::now();
let mut app = Application::new(COMMUNITY, PersonaId::new(), "did:key:zA", now).unwrap();
app.criterion_id = Some("retired".into());
assert!(app.adopt_manifest(&manifest.0, &manifest.1).unwrap());
assert_eq!(app.criterion_id.as_deref(), Some("vetted"), "hidden first");
assert_eq!(app.criterion_repicked.as_deref(), Some("retired"));
}
#[tokio::test]
async fn the_vetter_attests_named_for_a_named_request_and_pcs_for_a_hidden_one() {
use super::book::RequestVetting;
let alongside = hidden_manifest(false);
let (mut applicant, mut vetter, _) = ready().await;
learn(&mut vetter.book, &alongside);
assert!(vetter.book.pcs_zkp(COMMUNITY), "the community runs PCS ZKP");
assert!(
applicant.application().hidden.is_none(),
"this one is named"
);
let (request_id, _) = in_session(&mut applicant, &mut vetter).await;
assert_eq!(
vetter.book.request_vetting(&request_id),
RequestVetting::Named {
criterion: Some("vetted".into())
}
);
let (mut applicant, mut vetter, _) = ready().await;
learn(&mut vetter.book, &alongside);
applicant
.application()
.adopt_manifest(&alongside.0, &alongside.1)
.unwrap();
assert!(applicant.application().hidden.is_some());
let (request_id, _) = in_session(&mut applicant, &mut vetter).await;
assert_eq!(
vetter.book.request_vetting(&request_id),
RequestVetting::Hidden
);
let (mut applicant, mut vetter, _) = ready().await;
learn(&mut vetter.book, &hidden_manifest(true));
let (request_id, _) = in_session(&mut applicant, &mut vetter).await;
assert_eq!(
vetter.book.request_vetting(&request_id),
RequestVetting::HiddenWithoutId {
criterion: "vetted".into()
}
);
}