use affinidi_openid4vp::DcqlQuery;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use vta_sdk::protocols::vetting::{CheckShape, VettingRequirements};
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use super::schema_exists;
pub const ACCEPTS_PREFIX: &[u8] = b"accepts:";
const VERSION_PREFIX: &str = "accepts-version:";
const SEEDED_KEY: &str = "accepts-seeded";
fn key(id: &str) -> Vec<u8> {
let mut k = ACCEPTS_PREFIX.to_vec();
k.extend_from_slice(id.as_bytes());
k
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
#[derive(utoipa::ToSchema)]
pub enum Admission {
Automatic,
#[default]
Review,
}
impl Admission {
pub fn as_str(self) -> &'static str {
match self {
Admission::Automatic => "automatic",
Admission::Review => "review",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(utoipa::ToSchema)]
pub enum CredentialIssuers {
Any,
Community,
Recognised,
}
impl CredentialIssuers {
pub fn as_str(self) -> &'static str {
match self {
CredentialIssuers::Any => "any",
CredentialIssuers::Community => "community",
CredentialIssuers::Recognised => "recognised",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct AcceptsCriterion {
pub id: String,
#[serde(default)]
pub admission: Admission,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<Object>)]
pub query: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_issuers: Option<CredentialIssuers>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub invitation_required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<vta_sdk::openapi::JoinManifest02VettingRequirements>)]
pub vetting: Option<VettingRequirements>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<Object>)]
pub hidden_vetting: Option<serde_json::Value>,
#[serde(default)]
pub position: u64,
pub created_at: DateTime<Utc>,
pub created_by_did: String,
}
impl AcceptsCriterion {
pub fn new(id: impl Into<String>, admission: Admission, by: impl Into<String>) -> Self {
Self {
id: id.into(),
admission,
query: None,
credential_issuers: None,
invitation_required: false,
description: None,
vetting: None,
hidden_vetting: None,
position: 0,
created_at: Utc::now(),
created_by_did: by.into(),
}
}
pub fn to_wire(&self) -> Value {
let mut out = json!({
"id": self.id,
"admission": self.admission.as_str(),
"createdAt": self.created_at,
"createdByDid": self.created_by_did,
});
let map = out.as_object_mut().expect("object literal");
if let Some(query) = &self.query {
map.insert("query".into(), query.clone());
}
if let Some(issuers) = self.credential_issuers {
map.insert("credentialIssuers".into(), issuers.as_str().into());
}
if self.invitation_required {
map.insert("invitationRequired".into(), true.into());
}
if let Some(description) = &self.description {
map.insert("description".into(), description.clone().into());
}
if let Some(vetting) = &self.vetting {
map.insert(
"vetting".into(),
serde_json::to_value(vetting).unwrap_or(Value::Null),
);
}
out
}
pub fn dcql(&self) -> Result<Option<DcqlQuery>, AppError> {
self.query
.as_ref()
.map(|q| {
DcqlQuery::from_json(q).map_err(|e| {
AppError::Internal(format!(
"stored Accepts criterion `{}` is not a valid DCQL query: {e}",
self.id
))
})
})
.transpose()
}
}
pub(crate) fn referenced_types(query: &DcqlQuery) -> Vec<String> {
let mut out = Vec::new();
for cq in &query.credentials {
if let Some(meta) = &cq.meta
&& let Some(vcts) = meta.get("vct_values").and_then(|v| v.as_array())
{
out.extend(vcts.iter().filter_map(|v| v.as_str()).map(String::from));
}
}
out
}
pub async fn validate_accepts_query(
schemas_ks: &KeyspaceHandle,
query: &Value,
) -> Result<DcqlQuery, AppError> {
let dcql = DcqlQuery::from_json(query)
.map_err(|e| AppError::Validation(format!("invalid DCQL query: {e}")))?;
for cq in &dcql.credentials {
if let Some(authorities) = &cq.trusted_authorities {
check_trusted_authorities(&cq.id, &cq.format, authorities)?;
}
}
for type_uri in referenced_types(&dcql) {
if !schema_exists(schemas_ks, &type_uri).await? {
return Err(AppError::Validation(format!(
"DCQL Accepts criterion references unregistered credential type `{type_uri}` \
— register it in the schema store first"
)));
}
}
Ok(dcql)
}
fn check_trusted_authorities(
id: &str,
format: &str,
authorities: &[affinidi_openid4vp::TrustedAuthoritiesQuery],
) -> Result<(), AppError> {
use affinidi_openid4vp::TrustedAuthorityType;
use base64::Engine as _;
let refuse = |why: String| {
Err(AppError::Validation(format!(
"credential query `{id}`: trusted_authorities {why}"
)))
};
if format != crate::join::criteria::SD_JWT_VC_FORMAT {
return refuse(format!(
"is checked against an SD-JWT VC issuer's certificate chain, and this query asks \
for `{format}`"
));
}
if authorities.is_empty() {
return refuse("is empty; omit it to accept any issuer".into());
}
for entry in authorities {
let unchecked = match entry.authority_type {
TrustedAuthorityType::Aki => None,
TrustedAuthorityType::EtsiTl => Some("etsi_tl"),
TrustedAuthorityType::OpenidFederation => Some("openid_federation"),
};
if let Some(kind) = unchecked {
return refuse(format!(
"of type `{kind}` cannot be checked by this verifier; use `aki` with the key \
identifier of the certification authority, and configure that authority in \
`[oid4vp] x509_trust_anchors`"
));
}
if entry.values.is_empty() {
return refuse("has an `aki` entry with no values".into());
}
for value in &entry.values {
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(value.trim_end_matches('='));
if !matches!(decoded, Ok(ref id) if !id.is_empty()) {
return refuse(format!(
"`aki` value `{value}` is not a base64url key identifier"
));
}
}
}
Ok(())
}
fn check_requirements_shape(criterion: &AcceptsCriterion) -> Result<(), AppError> {
match (&criterion.query, criterion.credential_issuers) {
(Some(_), None) => Err(AppError::Validation(
"a criterion with a query must say whose credentials count (credentialIssuers)".into(),
)),
(None, Some(_)) => Err(AppError::Validation(
"credentialIssuers says whose credentials meet the query, and this criterion has \
no query"
.into(),
)),
_ => Ok(()),
}
}
pub async fn store_accepts(
schemas_ks: &KeyspaceHandle,
criterion: &AcceptsCriterion,
) -> Result<AcceptsCriterion, AppError> {
check_requirements_shape(criterion)?;
if let Some(query) = &criterion.query {
validate_accepts_query(schemas_ks, query).await?;
}
if let Some(vetting) = &criterion.vetting {
vetting
.check_shape()
.map_err(|e| AppError::Validation(format!("vetting: {e}")))?;
}
let mut criterion = criterion.clone();
let existing = get_accepts(schemas_ks, &criterion.id).await?;
criterion.position = match &existing {
Some(old) => old.position,
None => next_position(schemas_ks).await?,
};
let served = crate::routes::join_requests::manifest::manifest_criterion(criterion.clone())?;
if let Some(old) = existing {
supersede(schemas_ks, old, Some(&served.digest)).await?;
}
schemas_ks
.insert(
String::from_utf8(key(&criterion.id)).expect("ascii key"),
&criterion,
)
.await?;
Ok(criterion)
}
async fn next_position(schemas_ks: &KeyspaceHandle) -> Result<u64, AppError> {
Ok(list_accepts(schemas_ks)
.await?
.iter()
.map(|c| c.position)
.max()
.map_or(1, |p| p.saturating_add(1)))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupersededVersion {
pub criterion: AcceptsCriterion,
pub superseded_at: DateTime<Utc>,
}
impl SupersededVersion {
pub fn within_grace(&self, now: DateTime<Utc>) -> bool {
self.criterion
.vetting
.as_ref()
.and_then(|v| v.requirements_grace.as_ref())
.and_then(|d| vta_sdk::protocols::vetting::parse_iso8601_duration(d))
.is_some_and(|grace| now < self.superseded_at + grace)
}
}
async fn supersede(
schemas_ks: &KeyspaceHandle,
old: AcceptsCriterion,
replacement_digest: Option<&str>,
) -> Result<(), AppError> {
let digest = crate::routes::join_requests::manifest::manifest_criterion(old.clone())
.map(|s| s.digest)
.ok();
let Some(digest) = digest else {
return Ok(());
};
if replacement_digest == Some(digest.as_str()) {
return Ok(());
}
schemas_ks
.insert(
format!("{VERSION_PREFIX}{digest}"),
&SupersededVersion {
criterion: old,
superseded_at: Utc::now(),
},
)
.await
}
pub async fn superseded_version(
schemas_ks: &KeyspaceHandle,
digest: &str,
) -> Result<Option<SupersededVersion>, AppError> {
schemas_ks.get(format!("{VERSION_PREFIX}{digest}")).await
}
pub async fn get_accepts(
schemas_ks: &KeyspaceHandle,
id: &str,
) -> Result<Option<AcceptsCriterion>, AppError> {
match schemas_ks.get_raw(key(id)).await? {
Some(bytes) => Ok(Some(serde_json::from_slice(&bytes).map_err(|e| {
AppError::Internal(format!("AcceptsCriterion decode: {e}"))
})?)),
None => Ok(None),
}
}
pub async fn list_accepts(schemas_ks: &KeyspaceHandle) -> Result<Vec<AcceptsCriterion>, AppError> {
let mut pairs = schemas_ks.prefix_iter_raw(ACCEPTS_PREFIX.to_vec()).await?;
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
pairs
.iter()
.map(|(_, v)| {
serde_json::from_slice(v)
.map_err(|e| AppError::Internal(format!("AcceptsCriterion decode: {e}")))
})
.collect()
}
pub async fn published_criteria(
schemas_ks: &KeyspaceHandle,
) -> Result<Vec<AcceptsCriterion>, AppError> {
let mut criteria = list_accepts(schemas_ks).await?;
criteria.sort_by(|a, b| a.position.cmp(&b.position).then_with(|| a.id.cmp(&b.id)));
Ok(criteria)
}
pub async fn delete_accepts(schemas_ks: &KeyspaceHandle, id: &str) -> Result<(), AppError> {
if let Some(old) = get_accepts(schemas_ks, id).await? {
supersede(schemas_ks, old, None).await?;
}
schemas_ks.remove(key(id)).await
}
const SEED_AUTHOR: &str = "did:vtc:system";
pub fn default_criteria(recognises: bool) -> Vec<AcceptsCriterion> {
let mut invited = AcceptsCriterion::new("invited", Admission::Automatic, SEED_AUTHOR);
invited.invitation_required = true;
invited.description = Some("Invited by this community".into());
let mut member = AcceptsCriterion::new("member-credential", Admission::Automatic, SEED_AUTHOR);
member.query = Some(json!({
"credentials": [{
"id": "membership",
"format": crate::join::criteria::LDP_VC_FORMAT,
"meta": { "type_values": [["MembershipCredential"]] }
}]
}));
member.credential_issuers = Some(if recognises {
CredentialIssuers::Recognised
} else {
CredentialIssuers::Community
});
member.description = Some(if recognises {
"A member of this community, or of a community it recognises".into()
} else {
"A member of this community".into()
});
let mut review = AcceptsCriterion::new("review", Admission::Review, SEED_AUTHOR);
review.description = Some("Reviewed by an administrator".into());
vec![invited, member, review]
}
pub async fn seed_default_criteria(
schemas_ks: &KeyspaceHandle,
recognises: bool,
) -> Result<usize, AppError> {
if schemas_ks.get_raw(SEEDED_KEY).await?.is_some() {
return Ok(0);
}
let mut stored = 0;
if list_accepts(schemas_ks).await?.is_empty() {
for criterion in default_criteria(recognises) {
store_accepts(schemas_ks, &criterion).await?;
stored += 1;
}
}
schemas_ks
.insert(SEEDED_KEY, &json!({ "seededAt": Utc::now() }))
.await?;
Ok(stored)
}
#[cfg(test)]
mod tests {
use super::super::{SchemaEntry, SchemaKind, store_schema};
use super::*;
use chrono::Utc;
use serde_json::json;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
const MEMBERSHIP_VCT: &str = "https://openvtc.org/credentials/MembershipCredential";
async fn ks() -> (tempfile::TempDir, Store, KeyspaceHandle) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.unwrap();
let ks = store.keyspace("schemas").unwrap();
(dir, store, ks)
}
async fn register_type(ks: &KeyspaceHandle, type_uri: &str) {
store_schema(
ks,
&SchemaEntry {
type_uri: type_uri.into(),
dtg_type: Some("MembershipCredential".into()),
credential_schema: None,
kind: SchemaKind::Accepts,
description: None,
created_at: Utc::now(),
created_by_did: "did:key:zAdmin".into(),
},
)
.await
.unwrap();
}
async fn register_membership(ks: &KeyspaceHandle) {
register_type(ks, MEMBERSHIP_VCT).await;
}
fn criterion(id: &str, vct: &str) -> AcceptsCriterion {
let mut c = AcceptsCriterion::new(id, Admission::Automatic, "did:key:zAdmin");
c.query = Some(json!({
"credentials": [{
"id": "membership",
"format": "dc+sd-jwt",
"meta": { "vct_values": [vct] },
"claims": [{ "path": ["givenName"] }]
}]
}));
c.credential_issuers = Some(CredentialIssuers::Community);
c.description = Some("join evidence".into());
c
}
#[tokio::test]
async fn stores_a_criterion_that_references_a_registered_type() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let c = criterion("join", MEMBERSHIP_VCT);
let stored = store_accepts(&ks, &c)
.await
.expect("valid criterion stores");
let got = get_accepts(&ks, "join").await.unwrap().unwrap();
assert_eq!(
serde_json::to_value(&got).unwrap(),
serde_json::to_value(&stored).unwrap()
);
assert_eq!(list_accepts(&ks).await.unwrap().len(), 1);
let dcql = got.dcql().unwrap().expect("stored query is valid DCQL");
assert_eq!(dcql.credentials.len(), 1);
delete_accepts(&ks, "join").await.unwrap();
assert!(get_accepts(&ks, "join").await.unwrap().is_none());
}
fn with_trusted_authorities(format: &str, authorities: Value) -> AcceptsCriterion {
let mut c = criterion("pid", MEMBERSHIP_VCT);
let cq = &mut c.query.as_mut().unwrap()["credentials"][0];
cq["format"] = json!(format);
cq["trusted_authorities"] = authorities;
c
}
#[tokio::test]
async fn an_aki_trusted_authority_is_stored_with_the_query() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let c = with_trusted_authorities(
"dc+sd-jwt",
json!([{ "type": "aki", "values": ["s9tIpPmhxdiuNkHMEWNpYim8S8Y"] }]),
);
store_accepts(&ks, &c).await.expect("an aki query stores");
let q = get_accepts(&ks, "pid")
.await
.unwrap()
.unwrap()
.dcql()
.unwrap()
.unwrap();
assert!(q.credentials[0].trusted_authorities.is_some());
}
#[tokio::test]
async fn a_trusted_authority_the_verifier_cannot_check_is_refused() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let aki = json!([{ "type": "aki", "values": ["s9tIpPmhxdiuNkHMEWNpYim8S8Y"] }]);
for (format, authorities, says) in [
(
"dc+sd-jwt",
json!([{ "type": "etsi_tl", "values": ["https://tl.example"] }]),
"etsi_tl",
),
(
"dc+sd-jwt",
json!([{ "type": "openid_federation", "values": ["https://ta.example"] }]),
"openid_federation",
),
(
"dc+sd-jwt",
json!([{ "type": "aki", "values": ["not base64!"] }]),
"base64url",
),
(
"dc+sd-jwt",
json!([{ "type": "aki", "values": [] }]),
"no values",
),
("dc+sd-jwt", json!([]), "empty"),
("ldp_vc", aki.clone(), "ldp_vc"),
] {
let c = with_trusted_authorities(format, authorities);
let err = store_accepts(&ks, &c).await.expect_err(says);
assert!(
matches!(&err, AppError::Validation(m) if m.contains(says)),
"{says}: {err:?}"
);
}
assert!(get_accepts(&ks, "pid").await.unwrap().is_none());
}
#[tokio::test]
async fn stores_a_criterion_that_requires_nothing() {
let (_d, _s, ks) = ks().await;
for admission in [Admission::Automatic, Admission::Review] {
let c = AcceptsCriterion::new("open", admission, "did:key:zAdmin");
let stored = store_accepts(&ks, &c).await.expect("stores");
assert_eq!(stored.admission, admission);
assert!(stored.query.is_none());
}
}
#[tokio::test]
async fn credential_issuers_is_present_exactly_when_the_query_is() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let mut no_issuers = criterion("join", MEMBERSHIP_VCT);
no_issuers.credential_issuers = None;
assert!(matches!(
store_accepts(&ks, &no_issuers).await,
Err(AppError::Validation(_))
));
let mut no_query = AcceptsCriterion::new("join", Admission::Review, "did:key:zAdmin");
no_query.credential_issuers = Some(CredentialIssuers::Any);
assert!(matches!(
store_accepts(&ks, &no_query).await,
Err(AppError::Validation(_))
));
assert!(list_accepts(&ks).await.unwrap().is_empty());
}
#[tokio::test]
async fn rejects_a_criterion_referencing_an_unregistered_type() {
let (_d, _s, ks) = ks().await;
let c = criterion("join", "https://openvtc.org/credentials/Unknown");
let err = store_accepts(&ks, &c)
.await
.expect_err("dangling type reference must be rejected");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
assert!(
get_accepts(&ks, "join").await.unwrap().is_none(),
"not stored"
);
}
#[tokio::test]
async fn rejects_a_structurally_invalid_dcql_query() {
let (_d, _s, ks) = ks().await;
let mut bad = criterion("bad", MEMBERSHIP_VCT);
bad.query = Some(json!({ "credentials": [] }));
let err = store_accepts(&ks, &bad)
.await
.expect_err("invalid DCQL must be rejected");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
}
fn vetting(min: u32, min_by_method: Value) -> VettingRequirements {
serde_json::from_value(json!({
"version": "0.1",
"statementType": "https://registry.trustoverip.org/dtg/vsc/vetted/1",
"minStatements": min,
"minByMethod": min_by_method,
"acceptedMethods": ["inPerson"],
"eligibleVetters": { "role": "vetter" },
"requirementsGrace": "P14D"
}))
.unwrap()
}
#[tokio::test]
async fn stores_and_returns_a_criterion_with_vetting_requirements() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let mut c = criterion("kernel", MEMBERSHIP_VCT);
c.vetting = Some(vetting(2, json!({ "inPerson": 1 })));
store_accepts(&ks, &c)
.await
.expect("evaluable requirements store");
let got = get_accepts(&ks, "kernel").await.unwrap().unwrap();
assert_eq!(
serde_json::to_value(got.vetting).unwrap(),
serde_json::to_value(c.vetting).unwrap()
);
}
#[tokio::test]
async fn refuses_vetting_requirements_no_applicant_could_satisfy() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let mut c = criterion("kernel", MEMBERSHIP_VCT);
c.vetting = Some(vetting(2, json!({ "video": 1 })));
let err = store_accepts(&ks, &c)
.await
.expect_err("a floor on a method the requirements do not accept is refused");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
assert!(get_accepts(&ks, "kernel").await.unwrap().is_none());
}
#[tokio::test]
async fn refuses_a_criterion_the_join_manifest_cannot_publish() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let c = criterion(&"k".repeat(129), MEMBERSHIP_VCT);
let err = store_accepts(&ks, &c)
.await
.expect_err("a criterion id longer than the manifest allows is refused");
assert!(matches!(err, AppError::Validation(_)), "{err:?}");
assert!(list_accepts(&ks).await.unwrap().is_empty());
}
#[tokio::test]
async fn published_order_is_registration_order_and_survives_replacement() {
let (_d, _s, ks) = ks().await;
for id in ["zeta", "alpha", "mid"] {
store_accepts(
&ks,
&AcceptsCriterion::new(id, Admission::Review, "did:key:zAdmin"),
)
.await
.unwrap();
}
let order = |v: Vec<AcceptsCriterion>| v.into_iter().map(|c| c.id).collect::<Vec<_>>();
assert_eq!(
order(published_criteria(&ks).await.unwrap()),
["zeta", "alpha", "mid"]
);
assert_eq!(
order(list_accepts(&ks).await.unwrap()),
["alpha", "mid", "zeta"]
);
store_accepts(
&ks,
&AcceptsCriterion::new("zeta", Admission::Automatic, "did:key:zAdmin"),
)
.await
.unwrap();
assert_eq!(
order(published_criteria(&ks).await.unwrap()),
["zeta", "alpha", "mid"],
"a replacement keeps its place"
);
delete_accepts(&ks, "zeta").await.unwrap();
store_accepts(
&ks,
&AcceptsCriterion::new("zeta", Admission::Automatic, "did:key:zAdmin"),
)
.await
.unwrap();
assert_eq!(
order(published_criteria(&ks).await.unwrap()),
["alpha", "mid", "zeta"],
"delete and register again moves it last"
);
}
#[tokio::test]
async fn a_replaced_version_is_kept_for_its_grace() {
let (_d, _s, ks) = ks().await;
register_membership(&ks).await;
let mut first = criterion("kernel", MEMBERSHIP_VCT);
first.vetting = Some(vetting(2, json!({})));
let first = store_accepts(&ks, &first).await.unwrap();
let first_digest = crate::routes::join_requests::manifest::manifest_criterion(first)
.unwrap()
.digest;
let mut second = criterion("kernel", MEMBERSHIP_VCT);
second.vetting = Some(vetting(3, json!({})));
store_accepts(&ks, &second).await.unwrap();
let kept = superseded_version(&ks, &first_digest)
.await
.unwrap()
.expect("the superseded version is kept");
assert_eq!(
kept.criterion
.vetting
.as_ref()
.map(|v| u64::from(v.min_statements)),
Some(2)
);
assert!(kept.within_grace(Utc::now()));
assert!(!kept.within_grace(Utc::now() + chrono::Duration::days(15)));
}
#[tokio::test]
async fn without_a_declared_grace_a_superseded_version_never_governs() {
let (_d, _s, ks) = ks().await;
let first = store_accepts(
&ks,
&AcceptsCriterion::new("open", Admission::Review, "did:key:zAdmin"),
)
.await
.unwrap();
let digest = crate::routes::join_requests::manifest::manifest_criterion(first)
.unwrap()
.digest;
delete_accepts(&ks, "open").await.unwrap();
let kept = superseded_version(&ks, &digest).await.unwrap().unwrap();
assert!(!kept.within_grace(Utc::now()));
}
#[tokio::test]
async fn seeds_the_defaults_once_and_never_again() {
let (_d, _s, ks) = ks().await;
register_type(&ks, "MembershipCredential").await;
assert_eq!(seed_default_criteria(&ks, true).await.unwrap(), 3);
let published = published_criteria(&ks).await.unwrap();
let summary: Vec<_> = published
.iter()
.map(|c| (c.id.as_str(), c.admission))
.collect();
assert_eq!(
summary,
[
("invited", Admission::Automatic),
("member-credential", Admission::Automatic),
("review", Admission::Review),
],
"review last, so it governs only what meets neither of the others"
);
assert!(published[0].invitation_required);
assert_eq!(
published[1].credential_issuers,
Some(CredentialIssuers::Recognised)
);
for c in published {
delete_accepts(&ks, &c.id).await.unwrap();
}
assert_eq!(seed_default_criteria(&ks, true).await.unwrap(), 0);
assert!(list_accepts(&ks).await.unwrap().is_empty());
}
#[tokio::test]
async fn a_community_that_recognises_nobody_counts_only_its_own_credentials() {
let defaults = default_criteria(false);
assert_eq!(
defaults[1].credential_issuers,
Some(CredentialIssuers::Community)
);
}
#[tokio::test]
async fn a_community_with_its_own_criteria_is_not_seeded() {
let (_d, _s, ks) = ks().await;
store_accepts(
&ks,
&AcceptsCriterion::new("mine", Admission::Review, "did:key:zAdmin"),
)
.await
.unwrap();
assert_eq!(seed_default_criteria(&ks, true).await.unwrap(), 0);
assert_eq!(list_accepts(&ks).await.unwrap().len(), 1);
}
#[test]
fn the_wire_form_is_the_specifications_type() {
let mut c = criterion("kernel", MEMBERSHIP_VCT);
c.invitation_required = true;
c.position = 7;
c.hidden_vetting = Some(json!({ "suite": "x" }));
let wire = c.to_wire();
assert!(wire.get("position").is_none());
assert!(wire.get("hiddenVetting").is_none());
use trust_tasks_rs::specs::vtc::schemas::accepts::{list, register, show};
serde_json::from_value::<register::v0_2::AcceptsCriterion>(wire.clone()).unwrap();
serde_json::from_value::<show::v0_2::AcceptsCriterion>(wire.clone()).unwrap();
serde_json::from_value::<list::v0_2::AcceptsCriterion>(wire).unwrap();
}
}