use crate::Disclosure;
use super::policy::{DocumentScope, ProductGroupAccessPolicy};
#[test]
fn every_declared_property_resolves_to_its_own_class() {
let registry = crate::schemas::VersionedSchemaRegistry::new();
let mut checked = 0usize;
let mut wrong: Vec<String> = Vec::new();
for product_group in registry.product_groups() {
for version in registry.versions_for(product_group) {
let json_text = registry
.get(product_group, version)
.expect("the registry listed it");
let schema: serde_json::Value =
serde_json::from_str(json_text).expect("schema is valid JSON");
let policy = ProductGroupAccessPolicy::from_schema(
product_group,
&version.to_string(),
json_text,
)
.expect("policy builds from a listed schema");
let mut active_refs: Vec<String> = Vec::new();
declared_paths(
&schema,
&[],
&schema,
&mut active_refs,
&mut |path: &[String], class| {
let borrowed: Vec<&str> = path.iter().map(String::as_str).collect();
let answered =
policy.disclosure_for_path(&borrowed, DocumentScope::ProductGroupData);
checked += 1;
if answered != class {
wrong.push(format!(
"{product_group} v{version}: {} declares {class:?} but resolves to {answered:?}",
borrowed.join(".")
));
}
},
);
}
}
assert!(checked > 0, "no declared properties were checked at all");
assert!(
wrong.is_empty(),
"{} of {checked} declared classes do not resolve to themselves: {wrong:#?}",
wrong.len()
);
}
fn declared_paths(
node: &serde_json::Value,
path: &[String],
root: &serde_json::Value,
active_refs: &mut Vec<String>,
visit: &mut impl FnMut(&[String], Disclosure),
) {
let Some(object) = node.as_object() else {
return;
};
if let Some(pointer) = object.get("$ref").and_then(serde_json::Value::as_str)
&& let Some(target) = pointer.strip_prefix('#').and_then(|p| root.pointer(p))
&& !active_refs.iter().any(|seen| seen == pointer)
{
active_refs.push(pointer.to_owned());
declared_paths(target, path, root, active_refs, visit);
active_refs.pop();
}
if let Some(properties) = object.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in properties {
let mut child = path.to_vec();
child.push(name.clone());
if let Some(class) = prop
.get("x-disclosure")
.and_then(serde_json::Value::as_str)
.and_then(parse_class)
{
visit(&child, class);
}
declared_paths(prop, &child, root, active_refs, visit);
}
}
if let Some(node) = object.get("items") {
declared_paths(node, path, root, active_refs, visit);
}
if let Some(node) = object.get("additionalProperties") {
declared_paths(node, &[], root, active_refs, visit);
}
for key in ["definitions", "$defs"] {
if let Some(block) = object.get(key).and_then(|b| b.as_object()) {
for definition in block.values() {
declared_paths(definition, &[], root, active_refs, visit);
}
}
}
for key in ["allOf", "anyOf", "oneOf"] {
if let Some(branches) = object.get(key).and_then(|b| b.as_array()) {
for branch in branches {
declared_paths(branch, path, root, active_refs, visit);
}
}
}
}
fn parse_class(token: &str) -> Option<Disclosure> {
match token {
"public" => Some(Disclosure::Public),
"restricted" => Some(Disclosure::Restricted),
"conformity" => Some(Disclosure::Conformity),
"individual" => Some(Disclosure::Individual),
_ => None,
}
}