use auths_core::witness::{EventHash, WitnessProvider};
use auths_policy::{CanonicalCapability, DidParseError};
use auths_verifier::PresentationVerdict;
use auths_verifier::core::Attestation;
use auths_verifier::types::CanonicalDid;
use chrono::{DateTime, Utc};
use crate::keri::KeyState;
use crate::keri::event::EventReceipts;
use crate::keri::types::Said;
#[cfg(feature = "git-storage")]
use crate::storage::receipts::{check_receipt_consistency, verify_receipt_signature};
pub use auths_policy::{
CompileError, CompiledPolicy, Decision, EvalContext, Expr, Outcome, PolicyBuilder,
PolicyLimits, ReasonCode, SignerType, compile, compile_from_json, evaluate_strict,
};
pub fn context_from_attestation(
att: &Attestation,
now: DateTime<Utc>,
) -> Result<EvalContext, DidParseError> {
let mut ctx = EvalContext::try_from_strings(now, &att.issuer, att.subject.as_ref())?;
ctx = ctx.revoked(att.is_revoked());
if let Some(expires_at) = att.expires_at {
ctx = ctx.expires_at(expires_at);
}
if let Some(ref delegated_by) = att.delegated_by {
if let Ok(did) = auths_policy::CanonicalDid::parse(delegated_by) {
ctx = ctx.delegated_by(did);
}
}
if let Some(ref st) = att.signer_type {
let policy_st = match st {
auths_verifier::core::SignerType::Human => auths_policy::SignerType::Human,
auths_verifier::core::SignerType::Agent => auths_policy::SignerType::Agent,
auths_verifier::core::SignerType::Workload => auths_policy::SignerType::Workload,
_ => auths_policy::SignerType::Workload,
};
ctx = ctx.signer_type(policy_st);
}
Ok(ctx)
}
#[allow(clippy::too_many_arguments)]
pub fn context_from_delegated_member(
org_did: &str,
member_did: &str,
revoked: bool,
role: Option<&str>,
capabilities: &[auths_keri::Capability],
expires_at: Option<DateTime<Utc>>,
now: DateTime<Utc>,
) -> Result<EvalContext, DidParseError> {
let mut ctx = EvalContext::try_from_strings(now, org_did, member_did)?;
ctx = ctx.revoked(revoked);
let caps: Vec<CanonicalCapability> = capabilities
.iter()
.filter_map(|c| CanonicalCapability::parse(c.as_str()).ok())
.collect();
ctx = ctx.capabilities(caps);
if let Some(role) = role {
ctx = ctx.role(role.to_string());
}
if let Some(expires_at) = expires_at {
ctx = ctx.expires_at(expires_at);
}
if let Ok(did) = auths_policy::CanonicalDid::parse(org_did) {
ctx = ctx.delegated_by(did);
}
Ok(ctx)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapsSource {
AgentScopeSeal,
Acdc,
}
impl CapsSource {
pub fn governing(agentscope_present: bool, acdc_present: bool) -> Option<CapsSource> {
match (acdc_present, agentscope_present) {
(true, _) => Some(CapsSource::Acdc),
(false, true) => Some(CapsSource::AgentScopeSeal),
(false, false) => None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PolicyBridgeError {
#[error("no holder proof: presentation is not Valid, refusing to grant authority")]
NoHolderProof,
#[error("credential DID parse failed: {0}")]
Did(#[from] DidParseError),
}
pub fn context_from_credential(
presentation: &PresentationVerdict,
now: DateTime<Utc>,
) -> Result<EvalContext, PolicyBridgeError> {
let PresentationVerdict::Valid {
issuer,
subject,
caps,
role,
expires_at,
} = presentation
else {
return Err(PolicyBridgeError::NoHolderProof);
};
let mut ctx = EvalContext::try_from_strings(now, issuer.as_str(), subject.as_str())?;
ctx = ctx.revoked(false);
let caps: Vec<CanonicalCapability> = caps
.iter()
.filter_map(|c| CanonicalCapability::parse(c.as_str()).ok())
.collect();
ctx = ctx.capabilities(caps);
if let Some(role) = role {
ctx = ctx.role(role.clone());
}
if let Some(expires_at) = expires_at {
ctx = ctx.expires_at(*expires_at);
}
if let Ok(did) = CanonicalDid::parse(issuer.as_str()) {
ctx = ctx.delegated_by(did);
}
Ok(ctx)
}
pub fn evaluate_compiled(
att: &Attestation,
policy: &CompiledPolicy,
now: DateTime<Utc>,
) -> Result<Decision, DidParseError> {
let ctx = context_from_attestation(att, now)?;
Ok(evaluate_strict(policy, &ctx))
}
pub fn evaluate_with_witness(
identity: &KeyState,
att: &Attestation,
policy: &CompiledPolicy,
now: DateTime<Utc>,
local_head: EventHash,
witnesses: &[&dyn WitnessProvider],
) -> Result<Decision, DidParseError> {
if witnesses.is_empty() {
return evaluate_compiled(att, policy, now);
}
let required_quorum = witnesses.first().map(|w| w.quorum()).unwrap_or(1);
if required_quorum == 0 {
return evaluate_compiled(att, policy, now);
}
let mut matching = 0;
let mut total_opinions = 0;
for witness in witnesses {
if let Some(head) = witness.observe_identity_head(&identity.prefix) {
total_opinions += 1;
if head == local_head {
matching += 1;
}
}
}
if total_opinions == 0 {
return evaluate_compiled(att, policy, now);
}
if matching < required_quorum {
return Ok(Decision::deny(
ReasonCode::WitnessQuorumNotMet,
format!(
"Witness quorum not met: {}/{} matching, {} required",
matching, total_opinions, required_quorum
),
));
}
evaluate_compiled(att, policy, now)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReceiptVerificationResult {
Valid,
InsufficientReceipts { required: usize, got: usize },
Duplicity { event_a: Said, event_b: Said },
InvalidSignature { witness_did: CanonicalDid },
}
pub trait WitnessKeyResolver: Send + Sync {
fn get_public_key(&self, witness_did: &str) -> Option<Vec<u8>>;
}
#[cfg(feature = "git-storage")]
pub fn verify_receipts(
receipts: &EventReceipts,
threshold: usize,
key_resolver: Option<&dyn WitnessKeyResolver>,
) -> ReceiptVerificationResult {
let unique = receipts.unique_witness_count();
if unique < threshold {
return ReceiptVerificationResult::InsufficientReceipts {
required: threshold,
got: unique,
};
}
if let Err(e) = check_receipt_consistency(&receipts.receipts) {
return ReceiptVerificationResult::Duplicity {
event_a: receipts.event_said.clone(),
event_b: Said::new_unchecked(format!("conflicting: {}", e)),
};
}
if let Some(resolver) = key_resolver {
for stored in &receipts.receipts {
let witness = stored.witness.as_str();
if let Some(public_key) = resolver.get_public_key(witness) {
let witness_curve = auths_crypto::did_key_decode(witness)
.map(|d| d.curve())
.unwrap_or_default();
let typed_pk =
match auths_verifier::decode_public_key_bytes(&public_key, witness_curve) {
Ok(pk) => pk,
Err(_) => {
#[allow(clippy::disallowed_methods)]
return ReceiptVerificationResult::InvalidSignature {
witness_did: CanonicalDid::new_unchecked(witness),
};
}
};
match verify_receipt_signature(&stored.signed.receipt, &typed_pk) {
Ok(true) => continue,
Ok(false) | Err(_) => {
return ReceiptVerificationResult::InvalidSignature {
#[allow(clippy::disallowed_methods)] witness_did: CanonicalDid::new_unchecked(witness),
};
}
}
}
}
}
ReceiptVerificationResult::Valid
}
#[cfg(feature = "git-storage")]
#[allow(clippy::too_many_arguments)]
pub fn evaluate_with_receipts(
identity: &KeyState,
att: &Attestation,
policy: &CompiledPolicy,
now: DateTime<Utc>,
local_head: EventHash,
witnesses: &[&dyn WitnessProvider],
receipts: &EventReceipts,
threshold: usize,
key_resolver: Option<&dyn WitnessKeyResolver>,
) -> Result<Decision, DidParseError> {
match verify_receipts(receipts, threshold, key_resolver) {
ReceiptVerificationResult::Valid => {}
ReceiptVerificationResult::InsufficientReceipts { required, got } => {
return Ok(Decision::deny(
ReasonCode::WitnessQuorumNotMet,
format!(
"Insufficient receipts: {} required, {} present",
required, got
),
));
}
ReceiptVerificationResult::Duplicity { event_a, event_b } => {
return Ok(Decision::deny(
ReasonCode::WitnessQuorumNotMet,
format!("Duplicity detected: {} vs {}", event_a, event_b),
));
}
ReceiptVerificationResult::InvalidSignature { witness_did } => {
return Ok(Decision::deny(
ReasonCode::WitnessQuorumNotMet,
format!("Invalid receipt signature from witness: {}", witness_did),
));
}
}
evaluate_with_witness(identity, att, policy, now, local_head, witnesses)
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use auths_core::witness::NoOpWitness;
use auths_keri::{CesrKey, Prefix, Said, Threshold};
use auths_verifier::AttestationBuilder;
use chrono::Duration;
struct MockWitness {
head: Option<EventHash>,
quorum: usize,
}
impl WitnessProvider for MockWitness {
fn observe_identity_head(&self, _prefix: &Prefix) -> Option<EventHash> {
self.head
}
fn quorum(&self) -> usize {
self.quorum
}
}
fn make_key_state(prefix: &str) -> KeyState {
KeyState::from_inception(
Prefix::new_unchecked(prefix.to_string()),
vec![CesrKey::new_unchecked("DTestKey".to_string())],
vec![Said::new_unchecked("ENextCommitment".to_string())],
Threshold::Simple(1),
Threshold::Simple(1),
Said::new_unchecked("ETestSaid".to_string()),
vec![],
Threshold::Simple(0),
vec![],
)
}
fn make_attestation(
issuer: &str,
revoked_at: Option<DateTime<Utc>>,
expires_at: Option<DateTime<Utc>>,
) -> Attestation {
AttestationBuilder::default()
.rid("test")
.issuer(issuer)
.subject("did:key:zSubject")
.revoked_at(revoked_at)
.expires_at(expires_at)
.build()
}
fn default_policy() -> CompiledPolicy {
PolicyBuilder::new().not_revoked().not_expired().build()
}
#[test]
fn context_from_attestation_basic() {
let att = make_attestation("did:keri:ETest", None, None);
let now = Utc::now();
let ctx = context_from_attestation(&att, now).unwrap();
assert_eq!(ctx.issuer.as_str(), "did:keri:ETest");
assert_eq!(ctx.subject.as_str(), "did:key:zSubject");
assert!(!ctx.revoked);
}
#[test]
fn context_from_attestation_has_no_capabilities_or_role() {
let att = make_attestation("did:keri:ETest", None, None);
let now = Utc::now();
let ctx = context_from_attestation(&att, now).unwrap();
assert!(
ctx.capabilities.is_empty(),
"attestation caps must not enter the policy context"
);
assert_eq!(
ctx.role, None,
"attestation role must not enter the policy context"
);
}
#[test]
fn caps_absent_without_valid_credential() {
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = PolicyBuilder::new()
.not_revoked()
.require_capability("sign_commit")
.build();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
}
const CRED_ISSUER: &str = "did:keri:EIssuerCredential";
const CRED_SUBJECT: &str = "did:keri:ESubjectCredential";
fn valid_presentation(
caps: &[&str],
role: Option<&str>,
expires_at: Option<DateTime<Utc>>,
) -> PresentationVerdict {
PresentationVerdict::Valid {
issuer: auths_verifier::IdentityDID::parse(CRED_ISSUER).expect("valid test issuer"),
subject: auths_verifier::CanonicalDid::parse(CRED_SUBJECT).expect("valid test subject"),
caps: caps
.iter()
.map(|c| auths_verifier::Capability::parse(c).expect("valid test capability"))
.collect(),
role: role.map(str::to_string),
expires_at,
}
}
#[test]
fn policy_reads_caps_from_credential() {
let presentation = valid_presentation(&["sign_commit"], Some("deployer"), None);
let now = Utc::now();
let ctx = context_from_credential(&presentation, now).unwrap();
assert_eq!(ctx.issuer.as_str(), CRED_ISSUER);
assert_eq!(ctx.subject.as_str(), CRED_SUBJECT);
assert!(!ctx.revoked);
assert_eq!(ctx.capabilities.len(), 1);
assert_eq!(ctx.capabilities[0].as_str(), "sign_commit");
assert_eq!(ctx.role.as_deref(), Some("deployer"));
let policy = PolicyBuilder::new()
.not_revoked()
.require_capability("sign_commit")
.build();
assert_eq!(evaluate_strict(&policy, &ctx).outcome, Outcome::Allow);
}
#[test]
fn raw_acdc_without_presentation_yields_no_authority() {
let now = Utc::now();
for verdict in [
PresentationVerdict::HolderNotCurrentKey,
PresentationVerdict::WrongAudience,
PresentationVerdict::NonceMismatchOrConsumed,
PresentationVerdict::Expired,
PresentationVerdict::SubjectKelInvalid,
PresentationVerdict::CredentialNotValid(
auths_verifier::CredentialVerdict::SaidMismatch,
),
] {
let result = context_from_credential(&verdict, now);
assert!(
matches!(result, Err(PolicyBridgeError::NoHolderProof)),
"non-Valid verdict {verdict:?} must fail closed, got {result:?}"
);
}
}
#[test]
fn capability_round_trips_into_acdc() {
use auths_keri::{AgentScope, decode_agent_scope, encode_agent_scope};
use auths_verifier::Capability;
let raw = "repo:foo-bar_baz";
let scope = AgentScope {
capabilities: vec![Capability::parse(raw).unwrap()],
expires_at: Some(99),
};
let encoded = encode_agent_scope("Eagent", &scope);
let (prefix, decoded) = decode_agent_scope(&encoded).unwrap();
assert_eq!(prefix, "Eagent");
assert_eq!(decoded.capabilities, vec![Capability::parse(raw).unwrap()]);
let acdc_capability_json = decoded
.capabilities
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(",");
assert!(
!acdc_capability_json.contains(','),
"single cap stays comma-free; the join separator must not appear inside a cap"
);
for cap in acdc_capability_json.split(',') {
let canonical = CanonicalCapability::parse(cap).unwrap();
assert_eq!(canonical.as_str(), raw);
}
let att_cap = Capability::parse(raw).unwrap();
let canonical = CanonicalCapability::parse(&att_cap.to_string()).unwrap();
assert_eq!(canonical.as_str(), raw);
assert!(CanonicalCapability::parse("a,b").is_err());
}
#[test]
fn agentscope_seal_vs_acdc_precedence_documented() {
assert_eq!(CapsSource::governing(true, true), Some(CapsSource::Acdc));
assert_eq!(CapsSource::governing(false, true), Some(CapsSource::Acdc));
assert_eq!(
CapsSource::governing(true, false),
Some(CapsSource::AgentScopeSeal)
);
assert_eq!(CapsSource::governing(false, false), None);
}
#[test]
fn evaluate_compiled_allows_valid_attestation() {
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_compiled_denies_revoked() {
let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None);
let policy = default_policy();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::Revoked);
}
#[test]
fn evaluate_compiled_denies_expired() {
let past = Utc::now() - Duration::hours(1);
let att = make_attestation("did:keri:ETestPrefix", None, Some(past));
let policy = default_policy();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::Expired);
}
#[test]
fn evaluate_compiled_allows_not_yet_expired() {
let future = Utc::now() + Duration::hours(1);
let att = make_attestation("did:keri:ETestPrefix", None, Some(future));
let policy = default_policy();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_compiled_denies_issuer_mismatch() {
let att = make_attestation("did:keri:EWrongPrefix", None, None);
let policy = PolicyBuilder::new()
.not_revoked()
.require_issuer("did:keri:ETestPrefix")
.build();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::IssuerMismatch);
}
#[test]
fn evaluate_compiled_denies_missing_capability() {
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = PolicyBuilder::new()
.not_revoked()
.require_capability("sign_commit")
.build();
let now = Utc::now();
let decision = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::CapabilityMissing);
}
#[test]
fn evaluate_compiled_allows_with_capability_from_credential() {
let presentation = valid_presentation(&["sign_commit"], None, None);
let policy = PolicyBuilder::new()
.not_revoked()
.require_capability("sign_commit")
.build();
let now = Utc::now();
let ctx = context_from_credential(&presentation, now).unwrap();
assert_eq!(evaluate_strict(&policy, &ctx).outcome, Outcome::Allow);
}
#[test]
fn evaluate_compiled_is_deterministic() {
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let decision1 = evaluate_compiled(&att, &policy, now).unwrap();
let decision2 = evaluate_compiled(&att, &policy, now).unwrap();
assert_eq!(decision1, decision2);
}
#[test]
fn evaluate_with_witness_no_witnesses_delegates() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, &[]).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_with_witness_noop_delegates() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let noop = NoOpWitness;
let witnesses: &[&dyn WitnessProvider] = &[&noop];
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_with_witness_mismatch_denies() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let different_head =
EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
let witness = MockWitness {
head: Some(different_head),
quorum: 1,
};
let witnesses: &[&dyn WitnessProvider] = &[&witness];
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
}
#[test]
fn evaluate_with_witness_quorum_met_allows() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let witness = MockWitness {
head: Some(local_head),
quorum: 1,
};
let witnesses: &[&dyn WitnessProvider] = &[&witness];
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_with_witness_quorum_met_denies_when_policy_denies() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", Some(Utc::now()), None); let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let witness = MockWitness {
head: Some(local_head),
quorum: 1,
};
let witnesses: &[&dyn WitnessProvider] = &[&witness];
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::Revoked);
}
#[test]
fn evaluate_with_witness_multiple_witnesses_quorum() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let different_head =
EventHash::from_hex("0000000000000000000000000000000000000002").unwrap();
let w1 = MockWitness {
head: Some(local_head),
quorum: 2,
};
let w2 = MockWitness {
head: Some(local_head),
quorum: 2,
};
let w3 = MockWitness {
head: Some(different_head),
quorum: 2,
};
let witnesses: &[&dyn WitnessProvider] = &[&w1, &w2, &w3];
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_with_witness_no_opinions_delegates() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let witness = MockWitness {
head: None,
quorum: 1,
};
let witnesses: &[&dyn WitnessProvider] = &[&witness];
let decision =
evaluate_with_witness(&identity, &att, &policy, now, local_head, witnesses).unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
fn make_test_receipt(
event_said: &str,
witness_did: &str,
seq: u128,
) -> auths_core::witness::StoredReceipt {
auths_core::witness::StoredReceipt {
signed: auths_core::witness::SignedReceipt {
receipt: auths_core::witness::Receipt {
v: auths_keri::VersionString::placeholder(),
t: auths_core::witness::ReceiptTag,
d: Said::new_unchecked(event_said.to_string()),
i: Prefix::new_unchecked("EController".to_string()),
s: auths_keri::KeriSequence::new(seq),
},
signature: vec![],
},
witness: Prefix::new_unchecked(witness_did.to_string()),
}
}
#[test]
fn verify_receipts_meets_threshold() {
let receipts = EventReceipts::new(
"ESAID123",
vec![
make_test_receipt("ESAID123", "did:key:w1", 0),
make_test_receipt("ESAID123", "did:key:w2", 0),
],
);
let result = verify_receipts(&receipts, 2, None);
assert_eq!(result, ReceiptVerificationResult::Valid);
}
#[test]
fn verify_receipts_insufficient() {
let receipts = EventReceipts::new(
"ESAID123",
vec![make_test_receipt("ESAID123", "did:key:w1", 0)],
);
let result = verify_receipts(&receipts, 2, None);
assert!(matches!(
result,
ReceiptVerificationResult::InsufficientReceipts {
required: 2,
got: 1
}
));
}
#[test]
fn verify_receipts_duplicity() {
let receipts = EventReceipts {
event_said: Said::new_unchecked("ESAID_A".to_string()),
receipts: vec![
make_test_receipt("ESAID_A", "did:key:w1", 0),
make_test_receipt("ESAID_B", "did:key:w2", 0), ],
};
let result = verify_receipts(&receipts, 1, None);
assert!(matches!(
result,
ReceiptVerificationResult::Duplicity { .. }
));
}
#[test]
fn evaluate_with_receipts_valid() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let receipts = EventReceipts::new(
"ESAID",
vec![
make_test_receipt("ESAID", "did:key:w1", 0),
make_test_receipt("ESAID", "did:key:w2", 0),
],
);
let decision = evaluate_with_receipts(
&identity,
&att,
&policy,
now,
local_head,
&[],
&receipts,
2,
None,
)
.unwrap();
assert_eq!(decision.outcome, Outcome::Allow);
}
#[test]
fn evaluate_with_receipts_insufficient_denies() {
let identity = make_key_state("ETestPrefix");
let att = make_attestation("did:keri:ETestPrefix", None, None);
let policy = default_policy();
let now = Utc::now();
let local_head = EventHash::from_hex("0000000000000000000000000000000000000001").unwrap();
let receipts =
EventReceipts::new("ESAID", vec![make_test_receipt("ESAID", "did:key:w1", 0)]);
let decision = evaluate_with_receipts(
&identity,
&att,
&policy,
now,
local_head,
&[],
&receipts,
2, None,
)
.unwrap();
assert_eq!(decision.outcome, Outcome::Deny);
assert_eq!(decision.reason, ReasonCode::WitnessQuorumNotMet);
}
}