use serde_json::json;
use crate::{Audience, Disclosure, ProductGroupCatalog};
use super::policy::ProductGroupAccessPolicy;
use super::tests::{battery_policy, filter_payload};
#[test]
#[expect(
deprecated,
reason = "this test exists precisely to compare the two constructors"
)]
fn the_schema_policy_matches_the_catalog_policy_today() {
let reg = crate::schemas::VersionedSchemaRegistry::new();
let (version, json) = reg.latest("battery").expect("battery schema exists");
let from_schema = ProductGroupAccessPolicy::from_schema("battery", &version.to_string(), json)
.expect("the current schema yields a policy");
let from_catalog =
ProductGroupAccessPolicy::from_catalog(&ProductGroupCatalog::new(), "battery")
.expect("in catalog");
for (field, class) in &from_catalog.field_disclosure {
assert_eq!(
from_schema.disclosure_for_field(field),
*class,
"'{field}' disagrees between the catalog map and the schema annotation"
);
}
}
#[test]
fn an_unusable_schema_yields_no_policy() {
assert!(ProductGroupAccessPolicy::from_schema("battery", "9.9.9", "not json").is_none());
assert!(
ProductGroupAccessPolicy::from_schema("battery", "9.9.9", r#"{"type":"object"}"#).is_none()
);
}
#[test]
fn an_unrecognised_disclosure_token_falls_through_to_public() {
let schema = r#"{
"properties": {
"safetyMeasures": { "type": "string", "x-disclosure": "restrcted" },
"cathodeMaterial": { "type": "string", "x-disclosure": "restricted" }
}
}"#;
let policy =
ProductGroupAccessPolicy::from_schema("battery", "9.9.9", schema).expect("has properties");
assert!(
!policy.field_disclosure.contains_key("safetyMeasures"),
"a misspelt token must not produce a map entry"
);
assert_eq!(
policy.disclosure_for_field("safetyMeasures"),
Disclosure::Public,
"and the field therefore falls to the public default — the hazard"
);
assert_eq!(
policy.disclosure_for_field("cathodeMaterial"),
Disclosure::Restricted
);
let data = json!({ "safetyMeasures": "Isolate at the service disconnect" });
let served = filter_payload(&data, &policy, Audience::Public).filtered_data;
assert!(
served.get("safetyMeasures").is_some(),
"the public view carries a field the typo declassified"
);
}
#[test]
fn a_nested_property_is_classified_by_its_own_annotation() {
let schema = r#"{
"properties": {
"supplier": {
"type": "object",
"x-disclosure": "public",
"properties": {
"tradingName": { "type": "string", "x-disclosure": "public" },
"internalContact": { "type": "string", "x-disclosure": "restricted" }
}
},
"shipments": {
"type": "array",
"x-disclosure": "public",
"items": {
"type": "object",
"properties": {
"reference": { "type": "string", "x-disclosure": "public" },
"unitCostEur": { "type": "number", "x-disclosure": "conformity" }
}
}
}
}
}"#;
let policy =
ProductGroupAccessPolicy::from_schema("battery", "9.9.9", schema).expect("has properties");
assert_eq!(
policy.disclosure_for_field("internalContact"),
Disclosure::Restricted,
"a nested annotation must reach the policy map"
);
assert_eq!(
policy.disclosure_for_field("unitCostEur"),
Disclosure::Conformity,
"inside array items, too"
);
let data = json!({
"supplier": { "tradingName": "ACME", "internalContact": "ops@example.invalid" },
"shipments": [ { "reference": "S-1", "unitCostEur": 12.5 } ]
});
let public = filter_payload(&data, &policy, Audience::Public).filtered_data;
assert!(
public["supplier"].get("internalContact").is_none(),
"the public view must not carry a nested restricted field"
);
assert_eq!(
public["supplier"]["tradingName"],
json!("ACME"),
"its public sibling survives"
);
assert!(
public["shipments"][0].get("unitCostEur").is_none(),
"conformity content inside an array item is withheld from the public"
);
assert_eq!(public["shipments"][0]["reference"], json!("S-1"));
let authority = filter_payload(&data, &policy, Audience::Authority).filtered_data;
assert_eq!(authority["shipments"][0]["unitCostEur"], json!(12.5));
assert_eq!(
authority["supplier"]["internalContact"],
json!("ops@example.invalid")
);
}
#[test]
fn an_ambiguous_field_name_resolves_the_same_way_every_time() {
let mut answers = std::collections::HashSet::new();
for _ in 0..512 {
let mut policy = ProductGroupAccessPolicy::passport_default();
policy
.field_disclosure
.insert("jws_signature".into(), Disclosure::Public);
answers.insert(policy.disclosure_for_field("jwsSignature"));
}
assert_eq!(
answers.len(),
1,
"one lookup must not depend on hash order; got {answers:?}"
);
assert!(
answers.contains(&Disclosure::Conformity),
"and ambiguity must resolve to the more restrictive class; got {answers:?}"
);
}
#[test]
fn an_older_version_classifies_only_the_fields_it_declared() {
let old = ProductGroupAccessPolicy::for_schema_version("battery", "1.0.0")
.expect("v1.0.0 is a registered battery version");
let current = battery_policy();
assert!(
!old.field_disclosure.contains_key("dueDiligenceUrl"),
"v1.0.0 predates the field and must not classify it"
);
assert!(
current.field_disclosure.contains_key("dueDiligenceUrl"),
"the current version does classify it"
);
assert!(
old.field_disclosure.len() < current.field_disclosure.len(),
"v1.0.0 declares strictly fewer fields, so the two policies are not the same object"
);
assert_eq!(
old.disclosure_for_field("dueDiligenceUrl"),
Disclosure::Public,
"an undeclared field falls to the public default rather than being withheld"
);
}
#[test]
fn one_reclassified_field_is_enough_to_move_the_served_bytes() {
let before = battery_policy();
assert_eq!(
before.field_disclosure.get("sohMethodology"),
Some(&Disclosure::Restricted),
"fixture assumption: sohMethodology is restricted in the current schema"
);
let data = json!({
"gtin": "09506000134352",
"sohMethodology": "IEC 62660-1:2018",
});
let served_before = filter_payload(&data, &before, Audience::Public).filtered_data;
assert!(
served_before.get("sohMethodology").is_none(),
"restricted today, so the public view must not carry it"
);
let mut after = battery_policy();
after
.field_disclosure
.insert("sohMethodology".into(), Disclosure::Public);
let served_after = filter_payload(&data, &after, Audience::Public).filtered_data;
assert_eq!(
served_after.get("sohMethodology"),
Some(&json!("IEC 62660-1:2018")),
"reclassified to public, so the public view must now carry it"
);
assert_ne!(
serde_json::to_string(&served_before).unwrap(),
serde_json::to_string(&served_after).unwrap(),
"if these were equal the version axis would buy nothing"
);
}