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]>
97 lines
5.3 KiB
Rust
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");
|
|
}
|