use serde_json::{Value, json};
use thiserror::Error;
pub fn current_document_from_log(did_log: &str) -> Result<Value, CurrentDocumentError> {
use didwebvh_rs::log_entry::{LogEntry, LogEntryMethods};
let line = did_log
.lines()
.rfind(|l| !l.trim().is_empty())
.ok_or(CurrentDocumentError::EmptyLog)?;
let entry: LogEntry = serde_json::from_str(line)
.map_err(|e| CurrentDocumentError::Parse(format!("DID log line parse: {e}")))?;
Ok(entry.get_state().clone())
}
#[derive(Debug, Error)]
pub enum CurrentDocumentError {
#[error("VTA DID log is empty — cannot read current document")]
EmptyLog,
#[error("{0}")]
Parse(String),
}
pub const DIDCOMM_SERVICE_FRAGMENT: &str = "#vta-didcomm";
pub const REST_SERVICE_FRAGMENT: &str = "#vta-rest";
pub const REST_SERVICE_TYPE: &str = "VTARest";
pub const WEBAUTHN_SERVICE_FRAGMENT: &str = "#vta-webauthn";
pub const WEBAUTHN_SERVICE_TYPE: &str = "WebAuthnRP";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DidcommServiceRef {
pub id: String,
pub mediator_did: String,
pub routing_keys: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RestServiceRef {
pub id: String,
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebauthnServiceRef {
pub id: String,
pub url: String,
}
#[derive(Debug, Error)]
pub enum DocumentPatchError {
#[error("DID document is not a JSON object")]
NotAnObject,
#[error("DID document `id` field is missing or not a string")]
MissingDocumentId,
#[error("mediator DID must be a non-empty string")]
EmptyMediatorDid,
#[error("REST URL must be a non-empty string")]
EmptyRestUrl,
#[error("WebAuthn URL must be a non-empty string")]
EmptyWebauthnUrl,
}
pub fn current_didcomm_service(doc: &Value) -> Option<DidcommServiceRef> {
let services = doc.get("service")?.as_array()?;
for svc in services {
let id = svc.get("id")?.as_str()?;
if id_matches_didcomm(id) {
let endpoint = svc.get("serviceEndpoint")?;
let mediator_did = extract_mediator_did(endpoint)?;
let routing_keys = extract_routing_keys(endpoint);
return Some(DidcommServiceRef {
id: id.to_string(),
mediator_did,
routing_keys,
});
}
}
None
}
pub fn with_didcomm_service(
mut doc: Value,
mediator_did: &str,
) -> Result<Value, DocumentPatchError> {
if mediator_did.is_empty() {
return Err(DocumentPatchError::EmptyMediatorDid);
}
let did_id = doc
.get("id")
.and_then(Value::as_str)
.ok_or(DocumentPatchError::MissingDocumentId)?
.to_string();
let new_entry = json!({
"id": format!("{did_id}{DIDCOMM_SERVICE_FRAGMENT}"),
"type": "DIDCommMessaging",
"serviceEndpoint": [{
"accept": ["didcomm/v2"],
"uri": mediator_did,
}]
});
let obj = doc.as_object_mut().ok_or(DocumentPatchError::NotAnObject)?;
let services = obj
.entry("service")
.or_insert_with(|| json!([]))
.as_array_mut()
.expect("service field must be an array");
if let Some(existing) = services.iter_mut().find(|s| {
s.get("id")
.and_then(Value::as_str)
.is_some_and(id_matches_didcomm)
}) {
*existing = new_entry;
} else {
services.push(new_entry);
}
sort_services_canonical(&mut doc);
Ok(doc)
}
pub fn without_didcomm_service(mut doc: Value) -> Value {
let Some(obj) = doc.as_object_mut() else {
return doc;
};
let Some(services) = obj.get_mut("service").and_then(Value::as_array_mut) else {
return doc;
};
services.retain(|s| {
!s.get("id")
.and_then(Value::as_str)
.is_some_and(id_matches_didcomm)
});
if services.is_empty() {
obj.remove("service");
}
doc
}
fn id_matches_didcomm(id: &str) -> bool {
matches_canonical_fragment(id, DIDCOMM_SERVICE_FRAGMENT)
}
fn id_matches_rest(id: &str) -> bool {
matches_canonical_fragment(id, REST_SERVICE_FRAGMENT)
}
fn matches_canonical_fragment(id: &str, fragment: &str) -> bool {
let Some(prefix) = id.strip_suffix(fragment) else {
return false;
};
!prefix.contains('#')
}
pub fn sort_services_canonical(doc: &mut Value) {
let Some(obj) = doc.as_object_mut() else {
return;
};
let Some(services) = obj.get_mut("service").and_then(Value::as_array_mut) else {
return;
};
services.sort_by_key(|s| {
let id = s.get("id").and_then(Value::as_str).unwrap_or("");
if id_matches_didcomm(id) {
0u8
} else if id_matches_rest(id) {
1u8
} else if id_matches_webauthn(id) {
2u8
} else {
3u8
}
});
}
fn extract_mediator_did(endpoint: &Value) -> Option<String> {
match endpoint {
Value::String(s) => Some(s.clone()),
Value::Object(map) => map.get("uri")?.as_str().map(str::to_string),
Value::Array(arr) => arr.iter().find_map(extract_mediator_did),
_ => None,
}
}
fn extract_routing_keys(endpoint: &Value) -> Vec<String> {
match endpoint {
Value::Object(map) => map
.get("routingKeys")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default(),
Value::Array(arr) => arr
.iter()
.find_map(|inner| match inner {
Value::Object(_) => Some(extract_routing_keys(inner)),
_ => None,
})
.unwrap_or_default(),
_ => Vec::new(),
}
}
fn extract_rest_url(endpoint: &Value) -> Option<String> {
match endpoint {
Value::String(s) => Some(s.clone()),
Value::Object(map) => map.get("uri")?.as_str().map(str::to_string),
Value::Array(arr) => arr.iter().find_map(extract_rest_url),
_ => None,
}
}
pub fn current_rest_service(doc: &Value) -> Option<RestServiceRef> {
let services = doc.get("service")?.as_array()?;
for svc in services {
let id = svc.get("id")?.as_str()?;
if id_matches_rest(id) {
let url = extract_rest_url(svc.get("serviceEndpoint")?)?;
return Some(RestServiceRef {
id: id.to_string(),
url,
});
}
}
None
}
pub fn with_rest_service(mut doc: Value, url: &str) -> Result<Value, DocumentPatchError> {
if url.is_empty() {
return Err(DocumentPatchError::EmptyRestUrl);
}
let did_id = doc
.get("id")
.and_then(Value::as_str)
.ok_or(DocumentPatchError::MissingDocumentId)?
.to_string();
let new_entry = json!({
"id": format!("{did_id}{REST_SERVICE_FRAGMENT}"),
"type": REST_SERVICE_TYPE,
"serviceEndpoint": url,
});
let obj = doc.as_object_mut().ok_or(DocumentPatchError::NotAnObject)?;
let services = obj
.entry("service")
.or_insert_with(|| json!([]))
.as_array_mut()
.expect("service field must be an array");
if let Some(existing) = services.iter_mut().find(|s| {
s.get("id")
.and_then(Value::as_str)
.is_some_and(id_matches_rest)
}) {
*existing = new_entry;
} else {
services.push(new_entry);
}
sort_services_canonical(&mut doc);
Ok(doc)
}
pub fn without_rest_service(mut doc: Value) -> Value {
let Some(obj) = doc.as_object_mut() else {
return doc;
};
let Some(services) = obj.get_mut("service").and_then(Value::as_array_mut) else {
return doc;
};
services.retain(|s| {
!s.get("id")
.and_then(Value::as_str)
.is_some_and(id_matches_rest)
});
if services.is_empty() {
obj.remove("service");
}
doc
}
pub fn current_webauthn_service(doc: &Value) -> Option<WebauthnServiceRef> {
let services = doc.get("service")?.as_array()?;
for svc in services {
let id = svc.get("id")?.as_str()?;
if id_matches_webauthn(id) {
let url = extract_rest_url(svc.get("serviceEndpoint")?)?;
return Some(WebauthnServiceRef {
id: id.to_string(),
url,
});
}
}
None
}
pub fn with_webauthn_service(mut doc: Value, url: &str) -> Result<Value, DocumentPatchError> {
if url.is_empty() {
return Err(DocumentPatchError::EmptyWebauthnUrl);
}
let did_id = doc
.get("id")
.and_then(Value::as_str)
.ok_or(DocumentPatchError::MissingDocumentId)?
.to_string();
let new_entry = json!({
"id": format!("{did_id}{WEBAUTHN_SERVICE_FRAGMENT}"),
"type": WEBAUTHN_SERVICE_TYPE,
"serviceEndpoint": url,
});
let obj = doc.as_object_mut().ok_or(DocumentPatchError::NotAnObject)?;
let services = obj
.entry("service")
.or_insert_with(|| json!([]))
.as_array_mut()
.expect("service field must be an array");
if let Some(existing) = services.iter_mut().find(|s| {
s.get("id")
.and_then(Value::as_str)
.is_some_and(id_matches_webauthn)
}) {
*existing = new_entry;
} else {
services.push(new_entry);
}
sort_services_canonical(&mut doc);
Ok(doc)
}
pub fn without_webauthn_service(mut doc: Value) -> Value {
let Some(obj) = doc.as_object_mut() else {
return doc;
};
let Some(services) = obj.get_mut("service").and_then(Value::as_array_mut) else {
return doc;
};
services.retain(|s| {
!s.get("id")
.and_then(Value::as_str)
.is_some_and(id_matches_webauthn)
});
if services.is_empty() {
obj.remove("service");
}
doc
}
fn id_matches_webauthn(id: &str) -> bool {
matches_canonical_fragment(id, WEBAUTHN_SERVICE_FRAGMENT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fragment_match_is_strict_not_suffix() {
assert!(id_matches_didcomm("did:webvh:foo#vta-didcomm"));
assert!(id_matches_rest("did:webvh:foo#vta-rest"));
assert!(id_matches_webauthn("did:webvh:foo#vta-webauthn"));
assert!(!id_matches_didcomm("did:webvh:foo#extra#vta-didcomm"));
assert!(!id_matches_rest("did:webvh:foo#extra#vta-rest"));
assert!(!id_matches_webauthn("did:webvh:foo#extra#vta-webauthn"));
assert!(!id_matches_didcomm("did:webvh:foo-vta-didcomm"));
assert!(!id_matches_rest("did:webvh:foo-vta-rest"));
assert!(!id_matches_webauthn("did:webvh:foo-vta-webauthn"));
}
#[test]
fn with_webauthn_service_inserts_then_replaces() {
let doc = doc_without_service();
let patched = with_webauthn_service(doc, "https://vta.example.com/auth/portal").unwrap();
let svc = current_webauthn_service(&patched).expect("webauthn entry present");
assert_eq!(svc.url, "https://vta.example.com/auth/portal");
assert_eq!(svc.id, format!("{}#vta-webauthn", vta_did()));
let patched2 =
with_webauthn_service(patched, "https://vta.example.com/auth/portal-v2").unwrap();
let svc2 = current_webauthn_service(&patched2).expect("webauthn entry present");
assert_eq!(svc2.url, "https://vta.example.com/auth/portal-v2");
let services = patched2.get("service").unwrap().as_array().unwrap();
let webauthn_entries = services
.iter()
.filter(|s| id_matches_webauthn(s.get("id").and_then(Value::as_str).unwrap_or("")))
.count();
assert_eq!(webauthn_entries, 1);
}
#[test]
fn with_webauthn_service_rejects_empty_url() {
let err = with_webauthn_service(doc_without_service(), "").unwrap_err();
assert!(matches!(err, DocumentPatchError::EmptyWebauthnUrl));
}
#[test]
fn without_webauthn_service_removes_entry() {
let doc = with_webauthn_service(doc_without_service(), "https://x.example.com").unwrap();
assert!(current_webauthn_service(&doc).is_some());
let stripped = without_webauthn_service(doc);
assert!(current_webauthn_service(&stripped).is_none());
assert!(stripped.get("service").is_none());
}
#[test]
fn sort_canonical_places_webauthn_after_rest_before_other() {
let base = doc_without_service();
let with_d = with_didcomm_service(base, "did:webvh:m").unwrap();
let with_dr = with_rest_service(with_d, "https://r.example.com").unwrap();
let mut with_drw =
with_webauthn_service(with_dr, "https://r.example.com/auth/portal").unwrap();
let services = with_drw
.get_mut("service")
.and_then(Value::as_array_mut)
.unwrap();
services.insert(
0,
json!({
"id": format!("{}#tee-attestation", vta_did()),
"type": "TEEAttestation",
"serviceEndpoint": "https://r.example.com/attestation",
}),
);
sort_services_canonical(&mut with_drw);
let services = with_drw.get("service").unwrap().as_array().unwrap();
assert!(id_matches_didcomm(
services[0].get("id").unwrap().as_str().unwrap()
));
assert!(id_matches_rest(
services[1].get("id").unwrap().as_str().unwrap()
));
assert!(id_matches_webauthn(
services[2].get("id").unwrap().as_str().unwrap()
));
assert_eq!(
services[3].get("id").unwrap().as_str().unwrap(),
format!("{}#tee-attestation", vta_did())
);
}
fn vta_did() -> &'static str {
"did:webvh:abc123:vta.example.com:vta-1"
}
fn doc_with_didcomm(mediator: &str) -> Value {
json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": vta_did(),
"verificationMethod": [
{ "id": format!("{}#key-0", vta_did()), "type": "Multikey",
"controller": vta_did(), "publicKeyMultibase": "zfoo" },
{ "id": format!("{}#key-1", vta_did()), "type": "Multikey",
"controller": vta_did(), "publicKeyMultibase": "zbar" }
],
"authentication": [format!("{}#key-0", vta_did())],
"assertionMethod": [format!("{}#key-0", vta_did())],
"keyAgreement": [format!("{}#key-1", vta_did())],
"service": [{
"id": format!("{}#vta-didcomm", vta_did()),
"type": "DIDCommMessaging",
"serviceEndpoint": [{ "accept": ["didcomm/v2"], "uri": mediator }]
}]
})
}
fn doc_without_service() -> Value {
json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": vta_did(),
"verificationMethod": [
{ "id": format!("{}#key-0", vta_did()), "type": "Multikey",
"controller": vta_did(), "publicKeyMultibase": "zfoo" }
],
"authentication": [format!("{}#key-0", vta_did())],
"assertionMethod": [format!("{}#key-0", vta_did())]
})
}
fn doc_with_only_tee() -> Value {
json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": vta_did(),
"verificationMethod": [
{ "id": format!("{}#key-0", vta_did()), "type": "Multikey",
"controller": vta_did(), "publicKeyMultibase": "zfoo" }
],
"authentication": [format!("{}#key-0", vta_did())],
"service": [{
"id": format!("{}#tee-attestation", vta_did()),
"type": "TeeAttestation",
"serviceEndpoint": "https://vta.example.com/attestation/report"
}]
})
}
#[test]
fn current_finds_didcomm_service() {
let doc = doc_with_didcomm("did:webvh:mediator-A");
let svc = current_didcomm_service(&doc).expect("present");
assert_eq!(svc.id, format!("{}#vta-didcomm", vta_did()));
assert_eq!(svc.mediator_did, "did:webvh:mediator-A");
}
#[test]
fn current_returns_none_when_absent() {
assert!(current_didcomm_service(&doc_without_service()).is_none());
assert!(current_didcomm_service(&doc_with_only_tee()).is_none());
}
#[test]
fn current_tolerates_string_endpoint() {
let doc = json!({
"id": vta_did(),
"service": [{
"id": format!("{}#vta-didcomm", vta_did()),
"type": "DIDCommMessaging",
"serviceEndpoint": "did:webvh:legacy-mediator"
}]
});
let svc = current_didcomm_service(&doc).unwrap();
assert_eq!(svc.mediator_did, "did:webvh:legacy-mediator");
}
#[test]
fn with_didcomm_replaces_existing_entry() {
let doc = doc_with_didcomm("did:webvh:mediator-A");
let patched = with_didcomm_service(doc, "did:webvh:mediator-B").unwrap();
let svc = current_didcomm_service(&patched).unwrap();
assert_eq!(svc.mediator_did, "did:webvh:mediator-B");
let count = patched["service"]
.as_array()
.unwrap()
.iter()
.filter(|s| {
s["id"]
.as_str()
.map(|i| i.ends_with("#vta-didcomm"))
.unwrap_or(false)
})
.count();
assert_eq!(count, 1);
}
#[test]
fn with_didcomm_inserts_when_missing() {
let patched = with_didcomm_service(doc_without_service(), "did:webvh:mediator-A").unwrap();
let svc = current_didcomm_service(&patched).unwrap();
assert_eq!(svc.mediator_did, "did:webvh:mediator-A");
assert_eq!(patched["service"].as_array().unwrap().len(), 1);
}
#[test]
fn with_didcomm_preserves_other_services() {
let patched = with_didcomm_service(doc_with_only_tee(), "did:webvh:mediator-A").unwrap();
let services = patched["service"].as_array().unwrap();
assert_eq!(services.len(), 2, "tee + didcomm");
let tee_present = services
.iter()
.any(|s| s["id"].as_str().unwrap().ends_with("#tee-attestation"));
assert!(tee_present, "TEE attestation service preserved");
}
#[test]
fn with_didcomm_rejects_empty_mediator() {
let err = with_didcomm_service(doc_without_service(), "").unwrap_err();
assert!(matches!(err, DocumentPatchError::EmptyMediatorDid));
}
#[test]
fn with_didcomm_rejects_doc_without_id() {
let bad = json!({ "service": [] });
let err = with_didcomm_service(bad, "did:webvh:m").unwrap_err();
assert!(matches!(err, DocumentPatchError::MissingDocumentId));
}
#[test]
fn without_didcomm_removes_only_didcomm_entry() {
let mut doc = doc_with_didcomm("did:webvh:mediator-A");
doc["service"].as_array_mut().unwrap().push(json!({
"id": format!("{}#tee-attestation", vta_did()),
"type": "TeeAttestation",
"serviceEndpoint": "https://x"
}));
let stripped = without_didcomm_service(doc);
assert!(current_didcomm_service(&stripped).is_none());
let services = stripped["service"].as_array().unwrap();
assert_eq!(services.len(), 1);
assert!(
services[0]["id"]
.as_str()
.unwrap()
.ends_with("#tee-attestation")
);
}
#[test]
fn without_didcomm_drops_empty_service_array() {
let doc = doc_with_didcomm("did:webvh:mediator-A");
let stripped = without_didcomm_service(doc);
assert!(
stripped.get("service").is_none(),
"service array removed when last entry was the DIDComm one"
);
}
#[test]
fn without_didcomm_is_noop_when_absent() {
let original = doc_with_only_tee();
let stripped = without_didcomm_service(original.clone());
assert_eq!(stripped, original);
}
#[test]
fn without_didcomm_handles_no_service_field() {
let original = doc_without_service();
let stripped = without_didcomm_service(original.clone());
assert_eq!(stripped, original);
}
#[test]
fn round_trip_with_then_without_returns_original() {
let original = doc_without_service();
let with_d = with_didcomm_service(original.clone(), "did:webvh:m").unwrap();
let back = without_didcomm_service(with_d);
assert_eq!(back, original, "round-trip with→without is identity");
}
#[test]
fn verification_method_byte_identical_after_replace() {
let original = doc_with_didcomm("did:webvh:mediator-A");
let original_vm = original["verificationMethod"].clone();
let original_auth = original["authentication"].clone();
let original_ka = original["keyAgreement"].clone();
let original_assertion = original["assertionMethod"].clone();
let patched = with_didcomm_service(original, "did:webvh:mediator-B").unwrap();
assert_eq!(patched["verificationMethod"], original_vm);
assert_eq!(patched["authentication"], original_auth);
assert_eq!(patched["keyAgreement"], original_ka);
assert_eq!(patched["assertionMethod"], original_assertion);
}
#[test]
fn verification_method_byte_identical_after_remove() {
let original = doc_with_didcomm("did:webvh:mediator-A");
let original_vm = original["verificationMethod"].clone();
let original_auth = original["authentication"].clone();
let original_ka = original["keyAgreement"].clone();
let original_assertion = original["assertionMethod"].clone();
let stripped = without_didcomm_service(original);
assert_eq!(stripped["verificationMethod"], original_vm);
assert_eq!(stripped["authentication"], original_auth);
assert_eq!(stripped["keyAgreement"], original_ka);
assert_eq!(stripped["assertionMethod"], original_assertion);
}
fn doc_with_rest(url: &str) -> Value {
json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": vta_did(),
"verificationMethod": [
{ "id": format!("{}#key-0", vta_did()), "type": "Multikey",
"controller": vta_did(), "publicKeyMultibase": "zfoo" }
],
"authentication": [format!("{}#key-0", vta_did())],
"assertionMethod": [format!("{}#key-0", vta_did())],
"service": [{
"id": format!("{}#vta-rest", vta_did()),
"type": "VTARest",
"serviceEndpoint": url,
}]
})
}
#[test]
fn current_finds_rest_service() {
let doc = doc_with_rest("https://vta.example.com");
let svc = current_rest_service(&doc).expect("present");
assert_eq!(svc.id, format!("{}#vta-rest", vta_did()));
assert_eq!(svc.url, "https://vta.example.com");
}
#[test]
fn current_rest_returns_none_when_absent() {
assert!(current_rest_service(&doc_without_service()).is_none());
assert!(current_rest_service(&doc_with_only_tee()).is_none());
assert!(current_rest_service(&doc_with_didcomm("did:webvh:m")).is_none());
}
#[test]
fn current_rest_tolerates_object_and_array_endpoints() {
let object_endpoint = json!({
"id": vta_did(),
"service": [{
"id": format!("{}#vta-rest", vta_did()),
"type": "VTARest",
"serviceEndpoint": { "uri": "https://obj.example.com" }
}]
});
assert_eq!(
current_rest_service(&object_endpoint).unwrap().url,
"https://obj.example.com",
);
let array_endpoint = json!({
"id": vta_did(),
"service": [{
"id": format!("{}#vta-rest", vta_did()),
"type": "VTARest",
"serviceEndpoint": ["https://arr.example.com"]
}]
});
assert_eq!(
current_rest_service(&array_endpoint).unwrap().url,
"https://arr.example.com",
);
}
#[test]
fn with_rest_replaces_existing_entry() {
let doc = doc_with_rest("https://old.example.com");
let patched = with_rest_service(doc, "https://new.example.com").unwrap();
let svc = current_rest_service(&patched).unwrap();
assert_eq!(svc.url, "https://new.example.com");
let count = patched["service"]
.as_array()
.unwrap()
.iter()
.filter(|s| {
s["id"]
.as_str()
.map(|i| i.ends_with("#vta-rest"))
.unwrap_or(false)
})
.count();
assert_eq!(count, 1);
}
#[test]
fn with_rest_inserts_when_missing() {
let patched = with_rest_service(doc_without_service(), "https://x.example.com").unwrap();
let svc = current_rest_service(&patched).unwrap();
assert_eq!(svc.url, "https://x.example.com");
assert_eq!(patched["service"].as_array().unwrap().len(), 1);
}
#[test]
fn with_rest_emits_canonical_wire_shape() {
let patched = with_rest_service(doc_without_service(), "https://vta.example.com").unwrap();
let entry = &patched["service"].as_array().unwrap()[0];
assert_eq!(entry["type"], "VTARest");
assert_eq!(entry["serviceEndpoint"], "https://vta.example.com");
assert!(
entry["serviceEndpoint"].is_string(),
"serviceEndpoint must be a plain string per session.rs:1100",
);
}
#[test]
fn with_rest_preserves_didcomm_and_tee_entries() {
let mut doc = doc_with_didcomm("did:webvh:mediator-A");
doc["service"].as_array_mut().unwrap().push(json!({
"id": format!("{}#tee-attestation", vta_did()),
"type": "TeeAttestation",
"serviceEndpoint": "https://x"
}));
let patched = with_rest_service(doc, "https://vta.example.com").unwrap();
let services = patched["service"].as_array().unwrap();
assert_eq!(services.len(), 3, "didcomm + tee + rest");
let didcomm_present = current_didcomm_service(&patched).is_some();
let rest_present = current_rest_service(&patched).is_some();
let tee_present = services
.iter()
.any(|s| s["id"].as_str().unwrap().ends_with("#tee-attestation"));
assert!(didcomm_present && rest_present && tee_present);
}
#[test]
fn with_rest_rejects_empty_url() {
let err = with_rest_service(doc_without_service(), "").unwrap_err();
assert!(matches!(err, DocumentPatchError::EmptyRestUrl));
}
#[test]
fn with_rest_rejects_doc_without_id() {
let bad = json!({ "service": [] });
let err = with_rest_service(bad, "https://x").unwrap_err();
assert!(matches!(err, DocumentPatchError::MissingDocumentId));
}
#[test]
fn without_rest_removes_only_rest_entry() {
let mut doc = doc_with_rest("https://x.example.com");
doc["service"].as_array_mut().unwrap().push(json!({
"id": format!("{}#vta-didcomm", vta_did()),
"type": "DIDCommMessaging",
"serviceEndpoint": [{ "accept": ["didcomm/v2"], "uri": "did:webvh:m" }]
}));
doc["service"].as_array_mut().unwrap().push(json!({
"id": format!("{}#tee-attestation", vta_did()),
"type": "TeeAttestation",
"serviceEndpoint": "https://x"
}));
let stripped = without_rest_service(doc);
assert!(current_rest_service(&stripped).is_none());
assert!(current_didcomm_service(&stripped).is_some());
let tee_present = stripped["service"]
.as_array()
.unwrap()
.iter()
.any(|s| s["id"].as_str().unwrap().ends_with("#tee-attestation"));
assert!(tee_present);
}
#[test]
fn without_rest_drops_empty_service_array() {
let doc = doc_with_rest("https://x.example.com");
let stripped = without_rest_service(doc);
assert!(stripped.get("service").is_none());
}
#[test]
fn without_rest_is_noop_when_absent() {
let original = doc_with_didcomm("did:webvh:m");
let stripped = without_rest_service(original.clone());
assert_eq!(stripped, original);
}
#[test]
fn rest_and_didcomm_patchers_compose() {
let base = doc_without_service();
let with_d = with_didcomm_service(base.clone(), "did:webvh:m").unwrap();
let with_both = with_rest_service(with_d, "https://x.example.com").unwrap();
assert!(current_didcomm_service(&with_both).is_some());
assert!(current_rest_service(&with_both).is_some());
let only_didcomm = without_rest_service(with_both.clone());
assert!(current_didcomm_service(&only_didcomm).is_some());
assert!(current_rest_service(&only_didcomm).is_none());
let only_rest = without_didcomm_service(with_both);
assert!(current_didcomm_service(&only_rest).is_none());
assert!(current_rest_service(&only_rest).is_some());
}
#[test]
fn verification_method_byte_identical_after_rest_patches() {
let original = doc_with_rest("https://old.example.com");
let original_vm = original["verificationMethod"].clone();
let original_auth = original["authentication"].clone();
let patched = with_rest_service(original.clone(), "https://new.example.com").unwrap();
assert_eq!(patched["verificationMethod"], original_vm);
assert_eq!(patched["authentication"], original_auth);
let stripped = without_rest_service(original);
assert_eq!(stripped["verificationMethod"], original_vm);
assert_eq!(stripped["authentication"], original_auth);
}
fn id_at(doc: &Value, idx: usize) -> &str {
doc["service"][idx]["id"].as_str().unwrap()
}
#[test]
fn ordering_didcomm_then_rest_when_didcomm_was_first() {
let base = doc_with_didcomm("did:webvh:m");
let with_both = with_rest_service(base, "https://x.example.com").unwrap();
assert!(id_at(&with_both, 0).ends_with("#vta-didcomm"));
assert!(id_at(&with_both, 1).ends_with("#vta-rest"));
}
#[test]
fn ordering_didcomm_first_when_rest_was_first() {
let base = doc_with_rest("https://x.example.com");
let with_both = with_didcomm_service(base, "did:webvh:m").unwrap();
assert!(
id_at(&with_both, 0).ends_with("#vta-didcomm"),
"DIDComm must be first per spec §3.3, got: {}",
id_at(&with_both, 0),
);
assert!(id_at(&with_both, 1).ends_with("#vta-rest"));
}
#[test]
fn ordering_didcomm_rest_then_tee() {
let mut base = doc_with_didcomm("did:webvh:m");
base["service"].as_array_mut().unwrap().push(json!({
"id": format!("{}#tee-attestation", vta_did()),
"type": "TeeAttestation",
"serviceEndpoint": "https://x"
}));
let with_rest = with_rest_service(base, "https://x.example.com").unwrap();
let services = with_rest["service"].as_array().unwrap();
assert_eq!(services.len(), 3);
assert!(id_at(&with_rest, 0).ends_with("#vta-didcomm"));
assert!(id_at(&with_rest, 1).ends_with("#vta-rest"));
assert!(id_at(&with_rest, 2).ends_with("#tee-attestation"));
}
#[test]
fn sort_services_canonical_is_idempotent() {
let mut doc = doc_with_rest("https://x.example.com");
doc["service"].as_array_mut().unwrap().push(json!({
"id": format!("{}#vta-didcomm", vta_did()),
"type": "DIDCommMessaging",
"serviceEndpoint": [{ "accept": ["didcomm/v2"], "uri": "did:webvh:m" }]
}));
sort_services_canonical(&mut doc);
let after_first = doc.clone();
sort_services_canonical(&mut doc);
assert_eq!(doc, after_first);
}
#[test]
fn sort_services_canonical_handles_missing_service_field() {
let mut doc = doc_without_service();
let original = doc.clone();
sort_services_canonical(&mut doc);
assert_eq!(doc, original);
}
}