open-standards/joule-code/joule-code-rs/crates/joule-moq/tests/rating_vectors.rs
Transaction Science 5aacacfa31 joule-moq rating vectors test: is_multiple_of, as clippy asks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 07:56:41 -04:00

133 lines
7.5 KiB
Rust

//! joule-moq's reading of rating claims against WAI's age-assurance corpus
//! (`wai/live-conformance/age/vectors.json`, age-assurance §12), as a third reader beside
//! `wai-rs` and the corpus's Python verifier.
//!
//! - every claim of a `ratings`, `serving` or `predicate` case reads here exactly where the
//! corpus has it read: a case refused at `["claim", i]` with `record_malformed`, `bad_key`
//! or `bad_signature` has its claim `i` refused with that code here, and every other claim
//! of the corpus reads, with the age and the signer the corpus states;
//! - every request of an accepted `serving` case gets the age the corpus states, from the
//! claims the relay holds when it serves it, through [`Claim::covers`], and the track's
//! digest is the corpus's; a request whose session presented a proof of a room key
//! (age-assurance §5.5) is checked as the relay checks it ([`grant_proof::check_proof`] with
//! [`grant_proof::NO_GRANT`]), with the room's authority and clock as the relay's room keys,
//! and gets the outcome the corpus states: served as the room's own service, served by its
//! threshold, or refused with the proof's reason.
//!
//! The corpus lives beside this crate in the monorepo. If it cannot be read the test fails;
//! it never passes on nothing.
use joule_moq::grant_proof;
use joule_moq::rating::{read, track_digest, Claim};
use joule_moq::{TrackName, TrackNamespace};
use serde_json::Value;
const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../../wai/live-conformance/age/vectors.json");
/// The reading refusals, the codes this crate reports (age-assurance §11).
const READING: [&str; 3] = ["record_malformed", "bad_key", "bad_signature"];
fn corpus() -> Value {
serde_json::from_str(&std::fs::read_to_string(CORPUS).expect("the age-assurance corpus")).expect("JSON")
}
fn unhex(s: &str) -> Vec<u8> {
assert!(s.len().is_multiple_of(2), "hexadecimal of whole bytes");
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hexadecimal")).collect()
}
fn key32(s: &str) -> [u8; 32] {
unhex(s).try_into().expect("32 bytes")
}
fn texts(c: &Value) -> Vec<String> {
c["claims"].as_array().map(|a| a.iter().map(|t| t.as_str().expect("a claim's text").to_string()).collect()).unwrap_or_default()
}
#[test]
fn every_claim_reads_as_the_corpus_has_it() {
let doc = corpus();
let (mut read_ok, mut refused) = (0, 0);
for c in doc["cases"].as_array().expect("cases") {
let kind = c["kind"].as_str().expect("a kind");
if !["ratings", "serving", "predicate"].contains(&kind) {
continue;
}
let name = c["name"].as_str().expect("a name");
let e = &c["expect"];
// The claim at which a reading refusal stops the case, and its code.
let stop = (e["outcome"] == "refuse" && e["at"][0] == "claim" && READING.contains(&e["reason"].as_str().unwrap_or(""))).then(|| (e["at"][1].as_u64().expect("an index") as usize, e["reason"].as_str().expect("a code")));
for (i, t) in texts(c).iter().enumerate() {
let got = read(t.as_bytes());
if let Some((at, code)) = stop
&& at == i
{
assert_eq!(got.map(|_| ()).map_err(|r| r.code()), Err(code), "{name}: claim {i}");
refused += 1;
break;
}
// A sink passes over a claim that does not read or verify (§6.2): the corpus
// states no place for it.
if kind == "predicate" {
read_ok += usize::from(got.is_ok());
continue;
}
let cl = got.unwrap_or_else(|r| panic!("{name}: claim {i} refused {} here, read by the corpus", r.code()));
read_ok += 1;
if kind == "ratings" && e["outcome"] == "accept" {
let f = &e["claims"][i];
assert_eq!(cl.age(), f["age"].as_u64().expect("an age"), "{name}: claim {i}'s age");
assert_eq!(joule_moq::rating::to_hex(&cl.signer), f["signer"].as_str().expect("a signer"), "{name}: claim {i}'s signer");
}
}
}
assert!(read_ok > 40 && refused > 10, "{read_ok} claims read, {refused} refused: the corpus was reached");
}
#[test]
fn every_request_of_a_serving_case_gets_its_age() {
let doc = corpus();
let mut requests = 0;
let mut services = 0;
// The relay's room keys: the room's authority and its clock.
let room_keys = [key32(doc["principals"]["authority"].as_str().expect("the authority")), key32(doc["principals"]["clock"].as_str().expect("the clock"))];
for c in doc["cases"].as_array().expect("cases").iter().filter(|c| c["kind"] == "serving" && c["expect"]["outcome"] == "accept") {
let name = c["name"].as_str().expect("a name");
let fields: Vec<String> = c["track"]["namespace"].as_array().expect("a namespace").iter().map(|f| f.as_str().expect("a field").to_string()).collect();
let ns = TrackNamespace::from_strs(fields.iter().map(String::as_str));
let track_name = TrackName::from_str(c["track"]["name"].as_str().expect("a name"));
let digest = track_digest(&ns, &track_name);
assert_eq!(joule_moq::rating::to_hex(&digest), c["expect"]["track"].as_str().expect("a digest"), "{name}: the track's digest");
let room = joule_moq::rating::room_of(&ns);
let claims: Vec<Claim> = texts(c).iter().map(|t| read(t.as_bytes()).expect("an accepted case's claim reads")).collect();
for (q, want) in c["requests"].as_array().expect("requests").iter().zip(c["expect"]["requests"].as_array().expect("outcomes")) {
let group = q["group"].as_u64().expect("a group");
let held = q["held"].as_array().expect("held").iter().map(|i| &claims[i.as_u64().expect("an index") as usize]);
let age = held.filter(|cl| cl.covers(&digest, room.as_ref(), group)).map(Claim::age).max().unwrap_or(0);
assert_eq!(age, want["age"].as_u64().expect("an age"), "{name}: a request at group {group}");
// A proof of a room key, checked as the relay checks it in SETUP (age-assurance §5.5).
let service = match q.get("service") {
None => Ok(false),
Some(s) => {
services += 1;
let proof = unhex(s["proof"].as_str().expect("a proof"));
let challenge = key32(s["challenge"].as_str().expect("a challenge"));
grant_proof::GrantProof::read(&proof)
.and_then(|p| grant_proof::check_proof(&grant_proof::NO_GRANT, &p.key, Some(&proof), &challenge, |_| false, s["context"].as_str().expect("a context")))
.map(|p| room_keys.contains(&p.key))
}
};
match service {
Err(r) => assert_eq!((want["outcome"].as_str(), want["reason"].as_str()), (Some("refuse"), Some(r.code())), "{name}: a request at group {group}"),
Ok(true) => assert_eq!((want["outcome"].as_str(), want["service"].as_bool()), (Some("serve"), Some(true)), "{name}: a request at group {group}"),
Ok(false) => {
let served = q["threshold"].as_u64().expect("a threshold") >= age;
assert_eq!(served, want["outcome"] == "serve", "{name}: a request at group {group}");
assert!(want.get("service").is_none(), "{name}: a request at group {group} is no room service");
}
}
requests += 1;
}
}
assert!(requests > 10 && services >= 10, "{requests} requests, {services} with a proof of a room key: the corpus was reached");
}