Some checks failed
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 14m30s
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]>
141 lines
7.5 KiB
Rust
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:?}");
|
|
}
|