Reject unknown fields in every proto struct and bound Timestamp
Implemented-By: Laguna S 2.1 (OpenCode)
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
//! Every JSON object in every fixture must reject an unknown key. Do not edit.
|
||||
//!
|
||||
//! The other test files check unknown fields in a few hand-picked places. This one checks all of
|
||||
//! them: it walks each fixture, adds one unknown key to one object at a time, at every depth, and
|
||||
//! requires that the result no longer decodes.
|
||||
|
||||
use proto::{AuditRecord, Envelope, Grant, LogRecord};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Every copy of `value` that has exactly one extra key in exactly one object.
|
||||
fn with_one_unknown_key(value: &Value) -> Vec<Value> {
|
||||
let mut out = Vec::new();
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let mut extended = map.clone();
|
||||
extended.insert("zz_unknown".to_string(), Value::Bool(true));
|
||||
out.push(Value::Object(extended));
|
||||
for (key, child) in map {
|
||||
for changed in with_one_unknown_key(child) {
|
||||
let mut copy = map.clone();
|
||||
copy.insert(key.clone(), changed);
|
||||
out.push(Value::Object(copy));
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for (i, child) in items.iter().enumerate() {
|
||||
for changed in with_one_unknown_key(child) {
|
||||
let mut copy = items.clone();
|
||||
copy[i] = changed;
|
||||
out.push(Value::Array(copy));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Returns how many variations were tried, so callers can check the walk reached nested objects.
|
||||
fn check<T: DeserializeOwned>(what: &str, text: &str) -> usize {
|
||||
let value: Value = serde_json::from_str(text).unwrap_or_else(|e| panic!("{what}: {e}"));
|
||||
assert!(
|
||||
serde_json::from_value::<T>(value.clone()).is_ok(),
|
||||
"{what}: fixture must decode"
|
||||
);
|
||||
let variations = with_one_unknown_key(&value);
|
||||
for changed in &variations {
|
||||
assert!(
|
||||
serde_json::from_value::<T>(changed.clone()).is_err(),
|
||||
"{what}: accepted an unknown key: {changed}"
|
||||
);
|
||||
}
|
||||
variations.len()
|
||||
}
|
||||
|
||||
fn fixture(path: &str) -> String {
|
||||
let full = format!("{}/tests/fixtures/{path}", env!("CARGO_MANIFEST_DIR"));
|
||||
std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{full}: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelopes_reject_unknown_keys_at_every_depth() {
|
||||
for name in [
|
||||
"tool_request.json",
|
||||
"tool_response_pending.json",
|
||||
"tool_response_result.json",
|
||||
"tool_response_failed.json",
|
||||
"tool_response_denied.json",
|
||||
"error.json",
|
||||
] {
|
||||
// Envelope, msg and body: three objects.
|
||||
assert_eq!(
|
||||
check::<Envelope>(name, &fixture(&format!("wire/{name}"))),
|
||||
3,
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_records_reject_unknown_keys_at_every_depth() {
|
||||
for (i, line) in fixture("records/audit.jsonl").lines().enumerate() {
|
||||
// The record and its decision: two objects.
|
||||
assert_eq!(
|
||||
check::<AuditRecord>(&format!("audit.jsonl:{}", i + 1), line),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_records_reject_unknown_keys_at_every_depth() {
|
||||
let mut tried = 0;
|
||||
for (i, line) in fixture("records/session.jsonl").lines().enumerate() {
|
||||
tried += check::<LogRecord>(&format!("session.jsonl:{}", i + 1), line);
|
||||
}
|
||||
// Seven records, plus the one tool call inside the first assistant record.
|
||||
assert_eq!(tried, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grants_reject_unknown_keys_at_every_depth() {
|
||||
let grant: Grant = toml::from_str(&fixture("grant/full.toml")).unwrap();
|
||||
let text = serde_json::to_string(&grant).unwrap();
|
||||
// The grant and its constraints: two objects.
|
||||
assert_eq!(check::<Grant>("full.toml as JSON", &text), 2);
|
||||
}
|
||||
Reference in New Issue
Block a user