use std::collections::HashMap;
use serde_json::json;
use crate::{Audience, Disclosure};
use super::filter::filter_by_audience;
use super::policy::ProductGroupAccessPolicy;
use super::tests::filter_payload;
fn policy_with(name: &str, class: Disclosure) -> ProductGroupAccessPolicy {
let mut field_disclosure = HashMap::new();
field_disclosure.insert(name.to_owned(), class);
ProductGroupAccessPolicy {
name: "test".into(),
product_group: "test".into(),
field_disclosure,
envelope_disclosure: HashMap::new(),
default_disclosure: Disclosure::Public,
public_envelope_fields: Default::default(),
}
}
fn envelope_policy_with(name: &str, class: Disclosure) -> ProductGroupAccessPolicy {
let mut envelope_disclosure = HashMap::new();
envelope_disclosure.insert(name.to_owned(), class);
ProductGroupAccessPolicy {
name: "test".into(),
product_group: "test".into(),
field_disclosure: HashMap::new(),
envelope_disclosure,
default_disclosure: Disclosure::Public,
public_envelope_fields: Default::default(),
}
}
#[test]
fn nested_confidential_field_is_redacted() {
let policy = policy_with("jwsSignature", Disclosure::Conformity);
let data = json!({
"productGroupData": { "ok": 1, "jwsSignature": "leak-me" }
});
let decision = filter_by_audience(&data, &policy, Audience::Public);
assert_eq!(decision.filtered_data["productGroupData"]["ok"], json!(1));
assert!(
decision.filtered_data["productGroupData"]
.get("jwsSignature")
.is_none(),
"nested confidential field must be redacted, got {}",
decision.filtered_data
);
assert!(
decision
.redacted_fields
.contains(&"productGroupData.jwsSignature".to_owned())
);
}
#[test]
fn confidential_field_in_array_is_redacted() {
let policy = policy_with("secret", Disclosure::Conformity);
let data = json!({
"productGroupData": { "items": [ {"id": 1, "secret": "x"}, {"id": 2, "secret": "y"} ] }
});
let decision = filter_by_audience(&data, &policy, Audience::Public);
for el in decision.filtered_data["productGroupData"]["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(&"productGroupData.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_payload(&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 = envelope_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 = ProductGroupAccessPolicy::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_within_a_scope_but_not_across_them() {
let data = json!({
"manufacturer": { "name": "ACME", "address": "Berlin, DE" },
"facility": { "value": "4012345000009", "address": "1 Allee, Berlin" },
"productGroupData": { "supplier": { "address": "Rue X, Paris" } }
});
let mut envelope = ProductGroupAccessPolicy::passport_default();
envelope
.envelope_disclosure
.insert("address".into(), Disclosure::Restricted);
let out = filter_by_audience(&data, &envelope, 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"));
let product_group = policy_with("address", Disclosure::Restricted);
let out = filter_by_audience(&data, &product_group, Audience::Public).filtered_data;
assert!(
out["productGroupData"]["supplier"].get("address").is_none(),
"a product group's class must apply inside its own payload"
);
assert_eq!(
out["manufacturer"]["address"],
json!("Berlin, DE"),
"a product group's schema has no authority over the envelope"
);
assert_eq!(out["facility"]["address"], json!("1 Allee, Berlin"));
}