use std::collections::HashMap;
use crate::{Audience, Disclosure, SectorCatalog};
use serde_json::json;
use super::filter::filter_by_audience;
use super::policy::SectorAccessPolicy;
fn current_policy(sector: &str) -> SectorAccessPolicy {
let reg = crate::schemas::VersionedSchemaRegistry::new();
let (version, _) = reg.latest(sector).expect("sector has a schema");
SectorAccessPolicy::for_schema_version(sector, &version.to_string())
.expect("current version yields a policy")
}
fn textile_policy() -> SectorAccessPolicy {
current_policy("textile")
}
fn battery_policy() -> SectorAccessPolicy {
current_policy("battery")
}
fn sample_textile_data() -> serde_json::Value {
json!({
"fibreComposition": [
{ "fibre": "cotton", "pct": 70.0 },
{ "fibre": "polyester", "pct": 30.0 }
],
"countryOfManufacturing": "BD",
"careInstructions": "Machine wash 40°C",
"carbonFootprintKgCo2e": 8.5,
"durabilityScore": 7.5,
"svhcSubstances": [
{ "casNumber": "80-05-7", "substanceName": "Bisphenol A", "concentrationPct": 0.15 }
],
"disassemblyInstructions": "Remove buttons, separate layers by colour",
"sparePartsAvailable": true,
"jwsSignature": "eyJhbGciOiJFZERTQSJ9...",
"complianceReport": { "status": "compliant", "auditor": "TUV" }
})
}
#[test]
fn public_tier_redacts_professional_and_confidential() {
let policy = textile_policy();
let data = sample_textile_data();
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert!(decision.filtered_data["fibreComposition"].is_array());
assert!(decision.filtered_data["countryOfManufacturing"].is_string());
assert!(decision.filtered_data["carbonFootprintKgCo2e"].is_number());
assert!(decision.filtered_data["durabilityScore"].is_number());
assert!(decision.filtered_data.get("svhcSubstances").is_none());
assert!(
decision
.filtered_data
.get("disassemblyInstructions")
.is_none()
);
assert!(decision.filtered_data.get("sparePartsAvailable").is_none());
assert!(decision.filtered_data.get("jwsSignature").is_none());
assert!(decision.filtered_data.get("complianceReport").is_none());
assert!(
decision
.redacted_fields
.contains(&"svhcSubstances".to_owned())
);
assert!(
decision
.redacted_fields
.contains(&"jwsSignature".to_owned())
);
}
#[test]
fn professional_tier_sees_professional_fields() {
let policy = textile_policy();
let data = sample_textile_data();
let decision = filter_by_audience(&data, &policy, Audience::LegitimateInterest);
assert!(decision.filtered_data["svhcSubstances"].is_array());
assert!(decision.filtered_data["disassemblyInstructions"].is_string());
assert!(decision.filtered_data["sparePartsAvailable"].is_boolean());
assert!(decision.filtered_data.get("jwsSignature").is_none());
assert!(decision.filtered_data.get("complianceReport").is_none());
assert!(
!decision
.redacted_fields
.contains(&"svhcSubstances".to_owned())
);
assert!(
decision
.redacted_fields
.contains(&"jwsSignature".to_owned())
);
}
#[test]
fn confidential_tier_sees_everything() {
let policy = textile_policy();
let data = sample_textile_data();
let decision = filter_by_audience(&data, &policy, Audience::Authority);
assert!(decision.redacted_fields.is_empty());
assert!(decision.filtered_data["svhcSubstances"].is_array());
assert!(decision.filtered_data["jwsSignature"].is_string());
assert!(decision.filtered_data["complianceReport"].is_object());
}
#[test]
fn unknown_fields_default_to_public() {
let policy = textile_policy();
assert_eq!(
policy.disclosure_for_field("fibreComposition"),
Disclosure::Public
);
assert_eq!(
policy.disclosure_for_field("unknownField"),
Disclosure::Public
);
}
#[test]
fn battery_policy_public_keeps_point_1_and_drops_point_2() {
let policy = battery_policy();
let data = json!({
"gtin": "09506000134352",
"batteryChemistry": "LFP",
"nominalVoltageV": 400.0,
"co2ePerUnitKg": 150.0,
"dueDiligenceUrl": "https://example.com/due-diligence",
"criticalRawMaterials": [{"casNumber": "7440-48-4", "name": "Cobalt"}],
"disassemblyInstructionsUrl": "https://example.com/disassembly"
});
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert!(decision.filtered_data.get("gtin").is_some());
assert!(
decision.filtered_data.get("dueDiligenceUrl").is_some(),
"Annex XIII point 1(d) is publicly accessible"
);
assert!(
decision.filtered_data.get("criticalRawMaterials").is_some(),
"Annex XIII point 1(b) is publicly accessible"
);
assert!(
decision
.filtered_data
.get("disassemblyInstructionsUrl")
.is_none(),
"Annex XIII point 2(c) is withheld from the general public"
);
}
#[test]
fn passport_policy_public_redacts_jws() {
let policy = SectorAccessPolicy::passport_default();
let data = json!({
"id": "abc-123",
"productName": "Widget",
"status": "active",
"jwsSignature": "eyJhbGciOiJFZERTQSJ9...",
"batchId": "BATCH-42"
});
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert!(decision.filtered_data.get("productName").is_some());
assert!(decision.filtered_data.get("jwsSignature").is_none());
assert!(decision.filtered_data.get("batchId").is_none());
}
#[test]
fn passport_default_keeps_lint_result_out_of_public_view() {
let policy = SectorAccessPolicy::passport_default();
let data = json!({
"id": "abc-123",
"productName": "Widget",
"status": "published",
"lintResult": {
"findings": [{ "code": "implausible_mass", "message": "net mass exceeds gross" }],
"assessedAt": "2026-07-19T00:00:00Z"
}
});
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert!(
decision.filtered_data.get("lintResult").is_none(),
"lintResult is rewritten post-publish; it must not sit inside the signed public view"
);
assert_eq!(
policy.disclosure_for_field("lintResult"),
Disclosure::Restricted
);
assert_eq!(
policy.disclosure_for_field("lint_result"),
Disclosure::Restricted
);
assert_eq!(policy.disclosure_for_field("status"), Disclosure::Public);
assert!(decision.filtered_data.get("status").is_some());
}
#[test]
fn non_object_input_returned_unchanged() {
let policy = textile_policy();
let data = json!("just a string");
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert_eq!(decision.filtered_data, json!("just a string"));
assert!(decision.redacted_fields.is_empty());
}
#[test]
fn policy_round_trip() {
let policy = textile_policy();
let json = serde_json::to_value(&policy).unwrap();
let back: SectorAccessPolicy = serde_json::from_value(json).unwrap();
assert_eq!(back.name, "textile-1.2.0");
assert_eq!(back.sector, "textile");
assert_eq!(
back.disclosure_for_field("svhcSubstances"),
Disclosure::Restricted
);
}
#[test]
fn custom_policy_overrides_defaults() {
let mut policy = textile_policy();
policy
.field_disclosure
.insert("durabilityScore".into(), Disclosure::Restricted);
let data = sample_textile_data();
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert!(decision.filtered_data.get("durabilityScore").is_none());
assert!(
decision
.redacted_fields
.contains(&"durabilityScore".to_owned())
);
}
fn policy_with(name: &str, class: Disclosure) -> SectorAccessPolicy {
let mut field_disclosure = HashMap::new();
field_disclosure.insert(name.to_owned(), class);
SectorAccessPolicy {
name: "test".into(),
sector: "test".into(),
field_disclosure,
default_disclosure: Disclosure::Public,
}
}
#[test]
fn nested_confidential_field_is_redacted() {
let policy = policy_with("jwsSignature", Disclosure::Conformity);
let data = json!({
"sectorData": { "ok": 1, "jwsSignature": "leak-me" }
});
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert_eq!(decision.filtered_data["sectorData"]["ok"], json!(1));
assert!(
decision.filtered_data["sectorData"]
.get("jwsSignature")
.is_none(),
"nested confidential field must be redacted, got {}",
decision.filtered_data
);
assert!(
decision
.redacted_fields
.contains(&"sectorData.jwsSignature".to_owned())
);
}
#[test]
fn confidential_field_in_array_is_redacted() {
let policy = policy_with("secret", Disclosure::Conformity);
let data = json!({ "items": [ {"id": 1, "secret": "x"}, {"id": 2, "secret": "y"} ] });
let decision = filter_by_audience(&data, &policy, Audience::Public);
for el in decision.filtered_data["items"].as_array().unwrap() {
assert!(
el.get("secret").is_none(),
"array-nested secret must be redacted"
);
assert!(el.get("id").is_some());
}
assert!(
decision
.redacted_fields
.contains(&"items[0].secret".to_owned())
);
}
#[test]
fn casing_and_separator_drift_does_not_bypass() {
let policy = policy_with("disassemblyInstructions", Disclosure::Restricted);
let data = json!({ "disassembly_instructions": "secret", "public": 1 });
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert!(
decision
.filtered_data
.get("disassembly_instructions")
.is_none(),
"snake_case payload key must match camelCase policy key"
);
assert!(decision.filtered_data.get("public").is_some());
}
#[test]
fn fail_closed_default_disclosure_redacts_unlisted() {
let mut policy = policy_with("publicField", Disclosure::Public);
policy.default_disclosure = Disclosure::Conformity;
let data = json!({ "publicField": "ok", "unclassified": "should-not-leak" });
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert_eq!(decision.filtered_data["publicField"], json!("ok"));
assert!(
decision.filtered_data.get("unclassified").is_none(),
"fail-closed: unlisted field must be redacted"
);
}
#[test]
fn passport_default_keeps_facility_and_manufacturer_public() {
let policy = SectorAccessPolicy::passport_default();
let data = json!({
"id": "x",
"manufacturer": { "name": "GreenCell GmbH", "address": "Berlin, DE" },
"facility": { "scheme": "gln", "value": "4012345000009",
"name": "Default Plant", "country": "DE", "address": "1 Allee, Berlin" },
"operatorIdentifier": "DE123456789"
});
let out = filter_by_audience(&data, &policy, Audience::Public).filtered_data;
assert_eq!(out["facility"]["value"], json!("4012345000009"));
assert_eq!(out["facility"]["address"], json!("1 Allee, Berlin"));
assert_eq!(out["manufacturer"]["address"], json!("Berlin, DE"));
assert_eq!(out["operatorIdentifier"], json!("DE123456789"));
}
#[test]
fn generic_leaf_key_collides_across_objects() {
let mut policy = SectorAccessPolicy::passport_default();
policy
.field_disclosure
.insert("address".into(), Disclosure::Restricted);
let data = json!({
"manufacturer": { "name": "ACME", "address": "Berlin, DE" },
"facility": { "value": "4012345000009", "address": "1 Allee, Berlin" }
});
let out = filter_by_audience(&data, &policy, Audience::Public).filtered_data;
assert!(
out["manufacturer"].get("address").is_none(),
"collision: gating `address` also drops manufacturer.address"
);
assert!(
out["facility"].get("address").is_none(),
"collision: gating `address` also drops facility.address"
);
assert_eq!(out["facility"]["value"], json!("4012345000009"));
}
#[test]
fn every_property_declares_a_valid_disclosure_class() {
let reg = crate::schemas::VersionedSchemaRegistry::new();
let (version, json) = reg.latest("battery").expect("battery schema exists");
let schema: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
let bad = undeclared_properties(&schema);
assert!(
bad.is_empty(),
"battery v{version}: these properties declare no usable x-disclosure class, so they \
would default to public: {bad:?}"
);
}
const VALID_DISCLOSURE_TOKENS: [&str; 4] = ["public", "restricted", "conformity", "individual"];
fn undeclared_properties(schema: &serde_json::Value) -> Vec<(String, String)> {
fn walk(node: &serde_json::Value, path: &str, out: &mut Vec<(String, String)>) {
let Some(object) = node.as_object() else {
return;
};
if let Some(properties) = object.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in properties {
let child = if path.is_empty() {
name.clone()
} else {
format!("{path}.{name}")
};
let class = prop
.get("x-disclosure")
.and_then(serde_json::Value::as_str)
.unwrap_or("<missing>");
if !VALID_DISCLOSURE_TOKENS.contains(&class) {
out.push((child.clone(), class.to_owned()));
}
walk(prop, &child, out);
}
}
for key in ["items", "additionalProperties"] {
if let Some(child) = object.get(key) {
walk(child, &format!("{path}[]"), out);
}
}
for key in ["definitions", "$defs"] {
if let Some(block) = object.get(key).and_then(|b| b.as_object()) {
for (name, definition) in block {
walk(definition, &format!("{key}.{name}"), out);
}
}
}
for key in ["allOf", "anyOf", "oneOf"] {
if let Some(branches) = object.get(key).and_then(|b| b.as_array()) {
for branch in branches {
walk(branch, path, out);
}
}
}
}
let mut out = Vec::new();
walk(schema, "", &mut out);
out
}
#[test]
fn no_schema_declares_one_field_name_in_two_classes() {
use std::collections::HashMap;
let reg = crate::schemas::VersionedSchemaRegistry::new();
for sector in reg.sectors() {
for version in reg.versions_for(sector) {
let json = reg.get(sector, version).expect("registry listed it");
let schema: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
let mut seen: HashMap<String, (String, Disclosure)> = HashMap::new();
let mut clashes: Vec<String> = Vec::new();
collect_declared(&schema, "", &mut |path: &str, name: &str, class| {
let key: String = name
.chars()
.filter(char::is_ascii_alphanumeric)
.map(|c| c.to_ascii_lowercase())
.collect();
if let Some((first_path, first)) = seen.get(&key) {
if *first != class {
clashes.push(format!(
"'{name}' is {first:?} at {first_path} and {class:?} at {path}"
));
}
} else {
seen.insert(key, (path.to_owned(), class));
}
});
assert!(
clashes.is_empty(),
"{sector} v{version}: one field name declared in two classes, so the policy \
cannot express both: {clashes:?}"
);
}
}
}
fn collect_declared(
node: &serde_json::Value,
path: &str,
visit: &mut impl FnMut(&str, &str, Disclosure),
) {
let Some(object) = node.as_object() else {
return;
};
if let Some(properties) = object.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in properties {
let child = if path.is_empty() {
name.clone()
} else {
format!("{path}.{name}")
};
if let Some(class) = prop
.get("x-disclosure")
.and_then(serde_json::Value::as_str)
.and_then(|t| match t {
"public" => Some(Disclosure::Public),
"restricted" => Some(Disclosure::Restricted),
"conformity" => Some(Disclosure::Conformity),
"individual" => Some(Disclosure::Individual),
_ => None,
})
{
visit(&child, name, class);
}
collect_declared(prop, &child, visit);
}
}
for key in ["items", "additionalProperties"] {
if let Some(child) = object.get(key) {
collect_declared(child, &format!("{path}[]"), visit);
}
}
for key in ["definitions", "$defs"] {
if let Some(block) = object.get(key).and_then(|b| b.as_object()) {
for (name, definition) in block {
collect_declared(definition, &format!("{key}.{name}"), visit);
}
}
}
for key in ["allOf", "anyOf", "oneOf"] {
if let Some(branches) = object.get(key).and_then(|b| b.as_array()) {
for branch in branches {
collect_declared(branch, path, visit);
}
}
}
}
#[test]
fn every_sector_version_yields_a_fully_classified_policy() {
let reg = crate::schemas::VersionedSchemaRegistry::new();
let mut checked = 0usize;
for sector in reg.sectors() {
for version in reg.versions_for(sector) {
let json = reg.get(sector, version).expect("registry listed it");
let schema: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) else {
continue;
};
let _ = properties;
let undeclared = undeclared_properties(&schema);
assert!(
undeclared.is_empty(),
"{sector} v{version}: unclassified properties {undeclared:?}"
);
assert!(
SectorAccessPolicy::for_schema_version(sector, &version.to_string()).is_some(),
"{sector} v{version} yields no policy"
);
checked += 1;
}
}
assert!(checked > 20, "only {checked} schema versions walked");
}
#[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 = SectorAccessPolicy::from_schema("battery", &version.to_string(), json)
.expect("the current schema yields a policy");
let from_catalog =
SectorAccessPolicy::from_catalog(&SectorCatalog::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!(SectorAccessPolicy::from_schema("battery", "9.9.9", "not json").is_none());
assert!(SectorAccessPolicy::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 =
SectorAccessPolicy::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_by_audience(&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 =
SectorAccessPolicy::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_by_audience(&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_by_audience(&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 = SectorAccessPolicy::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 = SectorAccessPolicy::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_by_audience(&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_by_audience(&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"
);
}
#[test]
fn the_annex_xiii_point_4_tier_is_withheld_through_the_real_catalog_policy() {
let policy = battery_policy();
let data = json!({
"gtin": "09506000134352",
"dynamicPerformance": { "ratedCapacityAh": 92.0, "capacityFadePct": 8.0 },
"batteryStatus": "repurposed",
"usageHistory": { "chargeDischargeCycles": 412 },
});
let point_4 = ["dynamicPerformance", "batteryStatus", "usageHistory"];
for audience in [Audience::Public, Audience::Authority] {
let view = filter_by_audience(&data, &policy, audience);
for key in point_4 {
assert!(
view.filtered_data.get(key).is_none(),
"{audience:?} can see '{key}', which Annex XIII point 4 reserves \
to holders of a legitimate interest"
);
}
assert!(
view.filtered_data.get("gtin").is_some(),
"{audience:?} lost a public field"
);
}
let holder = filter_by_audience(&data, &policy, Audience::LegitimateInterest);
for key in point_4 {
assert!(
holder.filtered_data.get(key).is_some(),
"a legitimate-interest holder cannot see '{key}', which point 4 grants them"
);
}
}
#[test]
fn individual_item_data_is_withheld_from_authorities() {
let mut field_disclosure = HashMap::new();
field_disclosure.insert("dismantlingInfo".into(), Disclosure::Restricted);
field_disclosure.insert("testReport".into(), Disclosure::Conformity);
field_disclosure.insert("cycleHistory".into(), Disclosure::Individual);
let policy = SectorAccessPolicy {
name: "lattice-test".into(),
sector: "battery".into(),
field_disclosure,
default_disclosure: Disclosure::Public,
};
let data = json!({
"productName": "Cell",
"dismantlingInfo": "…",
"testReport": "…",
"cycleHistory": [1, 2, 3],
});
let authority = filter_by_audience(&data, &policy, Audience::Authority);
assert!(authority.filtered_data.get("testReport").is_some());
assert!(
authority.filtered_data.get("cycleHistory").is_none(),
"Art. 77(2)(b) does not grant authorities Annex XIII point 4"
);
let interest = filter_by_audience(&data, &policy, Audience::LegitimateInterest);
assert!(interest.filtered_data.get("cycleHistory").is_some());
assert!(
interest.filtered_data.get("testReport").is_none(),
"Art. 77(2)(c) does not grant legitimate interest Annex XIII point 3"
);
assert!(authority.filtered_data.get("dismantlingInfo").is_some());
assert!(interest.filtered_data.get("dismantlingInfo").is_some());
let public = filter_by_audience(&data, &policy, Audience::Public);
assert_eq!(public.filtered_data.as_object().unwrap().len(), 1);
assert!(public.filtered_data.get("productName").is_some());
}
#[test]
fn points_two_three_and_four_each_reach_a_different_audience() {
let policy = battery_policy();
let data = json!({
"gtin": "09506000134352",
"safetyMeasures": "Do not puncture",
"testReportResults": "Report 42: pass",
"batteryStatus": "original",
});
let public = filter_by_audience(&data, &policy, Audience::Public).filtered_data;
for key in ["safetyMeasures", "testReportResults", "batteryStatus"] {
assert!(
public.get(key).is_none(),
"the public view carries '{key}', which is not point 1 content"
);
}
let authority = filter_by_audience(&data, &policy, Audience::Authority).filtered_data;
assert!(authority.get("safetyMeasures").is_some(), "point 2");
assert!(authority.get("testReportResults").is_some(), "point 3");
assert!(
authority.get("batteryStatus").is_none(),
"point 4 is withheld from authorities — Art. 77(2)(b) does not reach it"
);
let holder = filter_by_audience(&data, &policy, Audience::LegitimateInterest).filtered_data;
assert!(holder.get("safetyMeasures").is_some(), "point 2");
assert!(holder.get("batteryStatus").is_some(), "point 4");
assert!(
holder.get("testReportResults").is_none(),
"point 3 is authorities only — a legitimate interest does not reach test reports"
);
}