open-standards/jcp/jcp-rs/crates/jcp-conformance/tests/terms_pack.rs
Transaction Science 5793af6808
Some checks failed
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 14m30s
JCP agreed terms: position statements in the published cases and the second reader
What two intermediaries state about one another is a record they
exchange, so it belongs with the cases a second implementation is held
to. terms.json now publishes nine statements and twelve checks: two
parties' statements agreeing, classical and hybrid; one unit apart; not
by the expected signer; in different currencies; altered after signing;
a statement following the one before, restating a total lower, not
naming it, or by another party; and a balance below zero.

verify_terms.py reads them with its own encoding and signature checks
and requires a case to notice each of five more rules removed.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-11 12:08:59 -05:00

141 lines
7.5 KiB
Rust

//! The published agreed-terms pack (`terms.json`), read through the
//! reference library. `conformance/verify_terms.py` reads it without.
use serde_json::Value;
use std::path::PathBuf;
#[test]
fn every_published_case_reconciles_as_the_pack_says() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1/terms.json");
let doc: Value = serde_json::from_slice(&std::fs::read(path).expect("read the pack")).expect("the pack parses");
let bytes = |name: &Value| hex::decode(doc["docs"][name.as_str().unwrap()].as_str().unwrap()).unwrap();
let key: [u8; 32] = hex::decode(doc["pubkey_hex"].as_str().unwrap()).unwrap().try_into().unwrap();
let cases = doc["cases"].as_array().unwrap();
assert!(cases.len() >= 14, "the pack lost cases");
let mut codes = std::collections::BTreeSet::new();
for case in cases {
let signing = match case.get("policy").and_then(Value::as_str) {
Some("require-hybrid") => jcp_terms::Signing::RequireHybrid,
_ => jcp_terms::Signing::AcceptClassical,
};
let got = match jcp_terms::reconcile_under(&bytes(&case["quote"]), &bytes(&case["receipt"]), &bytes(&case["ack"]), doc["steward"].as_str().unwrap(), &key, signing) {
Ok(r) => serde_json::json!({ "debited": r.debited, "received": r.received }),
Err(e) => {
codes.insert(e.code());
Value::String(e.code().to_string())
}
};
assert_eq!(got, case["expect"], "{}: {}", case["name"], case["about"]);
}
// payments in parts
let sessions = doc["sessions"].as_array().unwrap();
assert!(sessions.len() >= 8, "the pack lost sessions");
for case in sessions {
let receipts: Vec<Vec<u8>> = case["receipts"].as_array().unwrap().iter().map(&bytes).collect();
let slices: Vec<&[u8]> = receipts.iter().map(Vec::as_slice).collect();
let got = match jcp_terms::reconcile_running(&bytes(&case["quote"]), &slices, &bytes(&case["ack"]), doc["steward"].as_str().unwrap(), &key, jcp_terms::Signing::AcceptClassical) {
Ok(r) => serde_json::json!({ "debited": r.debited, "received": r.received, "closed": r.closed }),
Err(e) => Value::String(e.code().to_string()),
};
assert_eq!(got, case["expect"], "session {}: {}", case["name"], case["about"]);
}
// routes
use jcp_terms::route::{check_route_converting, read_conversion, Hop, RouteError};
let routes = doc["routes"].as_array().unwrap();
assert!(routes.len() >= 12, "the pack lost routes");
for case in routes {
let hops: Vec<Hop> = case["hops"]
.as_array()
.unwrap()
.iter()
.map(|h| Hop {
currency: h["currency"].as_str().unwrap().into(),
amount: h["amount"].as_u64().unwrap(),
deadline: h["deadline"].as_u64().unwrap(),
condition: jcp_schema::ContentId::from_hex(h["condition"].as_str().unwrap()).unwrap(),
})
.collect();
let read: Result<std::collections::BTreeMap<usize, _>, _> = case["conversions"]
.as_object()
.unwrap()
.iter()
.map(|(at, c)| read_conversion(&hex::decode(c.as_str().unwrap()).unwrap()).map(|c| (at.parse().unwrap(), c)))
.collect();
let got = match read {
Err(e) => e.code().to_string(),
Ok(conversions) => match check_route_converting(&hops, case["margin"].as_u64().unwrap(), case["now"].as_u64().unwrap(), &conversions) {
Ok(()) => "ok".to_string(),
Err(e) => match e {
RouteError::Empty => "route_empty",
RouteError::ConditionDiffers(_) => "route_condition_differs",
RouteError::ValueShort { .. } => "route_value_short",
RouteError::MarginShort { .. } => "route_margin_short",
RouteError::ConversionMissing(_) => "route_conversion_missing",
RouteError::ConversionInvalid(_) => "route_conversion_invalid",
RouteError::ConversionShort { .. } => "route_conversion_short",
RouteError::Lapsed => "route_lapsed",
}
.to_string(),
},
};
assert_eq!(Value::String(got), case["expect"], "route {}: {}", case["name"], case["about"]);
}
// consent
use jcp_terms::consent::{check_decision_under, Outcome};
let consents = doc["consents"].as_array().unwrap();
assert!(consents.len() >= 12, "the pack lost consents");
for case in consents {
let h = |v: &Value| hex::decode(v.as_str().unwrap()).unwrap();
let grant = case["grant"].as_object().map(|g| jcp_schema::SignedGrant {
grant: jcp_schema::from_canonical_cbor(&h(&g["cbor_hex"])).expect("a grant decodes"),
signature: h(&g["signature_hex"]),
});
let by: [u8; 32] = h(&case["issuer_pubkey_hex"]).try_into().unwrap();
let signing = match case["policy"].as_str() {
Some("require-hybrid") => jcp_terms::Signing::RequireHybrid,
_ => jcp_terms::Signing::AcceptClassical,
};
let got = match check_decision_under(&h(&case["request"]), &h(&case["decision"]), grant.as_ref(), &jcp_schema::Did(by), signing) {
Ok(Outcome::Approved(_)) => "approved".to_string(),
Ok(Outcome::Denied) => "denied".to_string(),
Err(e) => e.code().to_string(),
};
assert_eq!(Value::String(got), case["expect"], "consent {}: {}", case["name"], case["about"]);
}
// what two intermediaries state about one another
use jcp_relay::statement::{agree, follows, read, Disagreement};
let p = &doc["positions"];
let st = |name: &Value| hex::decode(p["statements"][name.as_str().unwrap()].as_str().unwrap()).unwrap();
let who = |name: &Value| {
let k: [u8; 32] = hex::decode(p["keys"][name.as_str().unwrap()].as_str().unwrap()).unwrap().try_into().unwrap();
jcp_schema::Did(k)
};
let code = |d: Disagreement| match d {
Disagreement::Unreadable(why) => why.split(':').next().unwrap_or("").trim().to_string(),
Disagreement::ChainBroken => "position_chain_broken".into(),
Disagreement::WentBack { .. } => "position_went_back".into(),
Disagreement::NotBilateral => "position_not_bilateral".into(),
Disagreement::Disagrees { .. } => "position_disagrees".into(),
};
assert!(p["agree"].as_array().unwrap().len() >= 6 && p["follows"].as_array().unwrap().len() >= 4, "the pack lost positions");
for case in p["agree"].as_array().unwrap() {
let got = match agree(&st(&case["upstream"]), &who(&case["upstream_is"]), &st(&case["downstream"]), &who(&case["downstream_is"])) {
Ok(n) => serde_json::json!(n),
Err(d) => Value::String(code(d)),
};
assert_eq!(got, case["expect"], "agree {}: {}", case["name"], case["about"]);
}
for case in p["follows"].as_array().unwrap() {
let got = match follows(&st(&case["earlier"]), &st(&case["later"])) {
Ok(s) => serde_json::json!(s.seq),
Err(d) => Value::String(code(d)),
};
assert_eq!(got, case["expect"], "follows {}: {}", case["name"], case["about"]);
}
for case in p["read"].as_array().unwrap() {
assert_eq!(serde_json::json!(read(&st(&case["statement"])).unwrap().figures.balance), case["balance"], "read {}", case["name"]);
}
assert_eq!(codes.len(), 10, "the pack no longer exercises each way the records disagree: {codes:?}");
}