open-standards/joule-code/joule-code-rs/crates/joule-moq/tests/rating_vectors.rs
Transaction Science cb793b61da joule-moq rating claims over MOQT (WAI_RATING 0x3A4D) and the relay's serving rule (age-assurance §5.5, §5.7)
joule-moq reads a rating claim as wai-rs does (strict JSON, exactly its members, JCS,
Ed25519 under WAI's strict rule), writes one, and agrees with the age corpus on every claim
and every serving request. A grant head reads its age attribute (§5.4).

The relay takes claims from PUBLISH and SUBSCRIBE_OK track properties and from object
properties, holds what §3.6 lets it honour, bounded per room and track and in all, and serves
each session by its proven grants' threshold: a live object of a group above it is not
forwarded, the streams of groups a newly held claim raises are ended, and a SUBSCRIBE, FETCH
or fill is refused below_threshold at its start or answered up to the first refused group.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:03:43 -04:00

97 lines
5.3 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.
//!
//! 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::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 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;
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}");
let served = q["threshold"].as_u64().expect("a threshold") >= age;
assert_eq!(served, want["outcome"] == "serve", "{name}: a request at group {group}");
requests += 1;
}
}
assert!(requests > 10, "{requests} requests: the corpus was reached");
}