use super::*;
fn detail(json: &str) -> AuthorizationDetail {
serde_json::from_str(json).expect("test fixture must parse")
}
#[test]
fn parse_accepts_the_rfc_shape_and_keeps_the_common_fields() {
let details = AuthorizationDetails::parse(
r#"[{"type":"payment_initiation","locations":["https://rs.example/"],"actions":["initiate"],"datatypes":["contacts"],"identifier":"acct-1","privileges":["admin"]}]"#,
)
.expect("the RFC's own shape must parse");
assert_eq!(details.len(), 1);
let d = &details.as_slice()[0];
assert_eq!(&*d.detail_type, "payment_initiation");
assert_eq!(&*d.locations, ["https://rs.example/".into()]);
assert_eq!(&*d.actions, ["initiate".into()]);
assert_eq!(&*d.datatypes, ["contacts".into()]);
assert_eq!(d.identifier.as_deref(), Some("acct-1"));
assert_eq!(&*d.privileges, ["admin".into()]);
assert!(
d.other.is_empty(),
"the section 2.2 common fields must not also land in `other`, or they would be compared \
twice and serialized twice"
);
}
#[test]
fn unknown_members_round_trip_unchanged() {
let raw = r#"[{"type":"payment_initiation","instructedAmount":{"currency":"EUR","amount":"50.00"},"creditorAccount":{"iban":"DE02100100109307118603"},"count":3,"flag":true,"nothing":null}]"#;
let parsed = AuthorizationDetails::parse(raw).expect("must parse");
let json = serde_json::to_string(&parsed).expect("must serialize");
let again = AuthorizationDetails::parse(&json).expect("must re-parse");
assert_eq!(parsed, again, "a round trip must be the identity");
let d = &parsed.as_slice()[0];
assert_eq!(d.other.get("count"), Some(&serde_json::json!(3)));
assert_eq!(d.other.get("flag"), Some(&serde_json::json!(true)));
assert_eq!(
d.other.get("nothing"),
Some(&serde_json::Value::Null),
"an explicit null is a member that was sent; dropping it would change the object"
);
assert_eq!(
d.other.get("creditorAccount"),
Some(&serde_json::json!({"iban": "DE02100100109307118603"}))
);
}
#[test]
fn member_order_does_not_change_identity() {
let a = AuthorizationDetails::parse(r#"[{"type":"t","b":1,"a":2}]"#).unwrap();
let b = AuthorizationDetails::parse(r#"[{"type":"t","a":2,"b":1}]"#).unwrap();
assert_eq!(a, b);
assert_eq!(
serde_json::to_string(&a).unwrap(),
serde_json::to_string(&b).unwrap()
);
}
#[test]
fn the_byte_bound_is_exact_at_the_boundary() {
let head = r#"[{"type":"t","n":""#;
let tail = r#""}]"#;
let filler = MAX_AUTHORIZATION_DETAILS_BYTES - head.len() - tail.len();
let at_limit = format!("{head}{}{tail}", "x".repeat(filler));
assert_eq!(at_limit.len(), MAX_AUTHORIZATION_DETAILS_BYTES);
assert!(
AuthorizationDetails::parse(&at_limit).is_ok(),
"exactly at the limit must be accepted"
);
let over = format!("{head}{}{tail}", "x".repeat(filler + 1));
assert_eq!(over.len(), MAX_AUTHORIZATION_DETAILS_BYTES + 1);
assert_eq!(
AuthorizationDetails::parse(&over).unwrap_err().error,
ErrorCode::InvalidAuthorizationDetails,
"one byte over must be refused"
);
}
#[test]
fn the_element_bound_is_exact_at_the_boundary() {
let one = r#"{"type":"t"}"#;
let at_limit = format!(
"[{}]",
vec![one; MAX_AUTHORIZATION_DETAILS_ELEMENTS].join(",")
);
assert_eq!(
AuthorizationDetails::parse(&at_limit).unwrap().len(),
MAX_AUTHORIZATION_DETAILS_ELEMENTS
);
let over = format!(
"[{}]",
vec![one; MAX_AUTHORIZATION_DETAILS_ELEMENTS + 1].join(",")
);
assert_eq!(
AuthorizationDetails::parse(&over).unwrap_err().error,
ErrorCode::InvalidAuthorizationDetails
);
}
#[test]
fn depth_counts_containers_and_the_member_budget_is_exact() {
assert_eq!(depth(&serde_json::json!(1)), 1);
assert_eq!(depth(&serde_json::json!([])), 1);
assert_eq!(depth(&serde_json::json!([1])), 2);
assert_eq!(depth(&serde_json::json!({"a": {"b": 1}})), 3);
assert_eq!(
depth(&serde_json::json!({"a": 1, "b": [[[2]]]})),
5,
"the deepest child decides, not the first: the object plus three arrays plus the scalar"
);
let nest = |n: usize| {
format!(
r#"[{{"type":"t","n":{}1{}}}]"#,
"[".repeat(n),
"]".repeat(n)
)
};
let at_limit = nest(MAX_MEMBER_DEPTH - 1);
assert!(
AuthorizationDetails::parse(&at_limit).is_ok(),
"exactly at the member depth budget must be accepted"
);
let over = nest(MAX_MEMBER_DEPTH);
let refusal = AuthorizationDetails::parse(&over).unwrap_err();
assert_eq!(
refusal.error,
ErrorCode::InvalidAuthorizationDetails,
"one level over must be refused"
);
assert!(
refusal
.error_description
.as_deref()
.is_some_and(|d| d.contains("nested more deeply")),
"the refusal must come from the member depth rule and not from some other arm of parse \
that answers the same code: {:?}",
refusal.error_description
);
}
#[test]
fn no_declared_types_means_no_type_is_supported() {
let details = AuthorizationDetails::parse(r#"[{"type":"payment_initiation"}]"#).unwrap();
assert_eq!(
details.require_supported_types(None).unwrap_err().error,
ErrorCode::InvalidAuthorizationDetails
);
assert_eq!(
details
.require_supported_types(Some(&[]))
.unwrap_err()
.error,
ErrorCode::InvalidAuthorizationDetails
);
assert!(details
.require_supported_types(Some(&["payment_initiation".to_string()]))
.is_ok());
let mixed =
AuthorizationDetails::parse(r#"[{"type":"payment_initiation"},{"type":"other"}]"#).unwrap();
assert_eq!(
mixed
.require_supported_types(Some(&["payment_initiation".to_string()]))
.unwrap_err()
.error,
ErrorCode::InvalidAuthorizationDetails
);
}
#[test]
fn is_narrowing_of_consults_every_field() {
let granted = detail(
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
);
assert!(granted.is_narrowing_of(&granted));
for (why, narrower) in [
(
"dropping a location",
r#"{"type":"t","locations":["l1"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
),
(
"dropping every action",
r#"{"type":"t","locations":["l1","l2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
),
(
"dropping a privilege and a datatype",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1"],"identifier":"i1","amount":"50"}"#,
),
] {
assert!(
detail(narrower).is_narrowing_of(&granted),
"{why} is a narrowing and must be allowed"
);
}
for (why, wider) in [
(
"a different type",
r#"{"type":"u","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
),
(
"a different identifier",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i2","privileges":["p1"],"amount":"50"}"#,
),
(
"no identifier at all, which asks for the whole class",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"privileges":["p1"],"amount":"50"}"#,
),
(
"an added location",
r#"{"type":"t","locations":["l1","l2","l3"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
),
(
"an added action",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2","a3"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
),
(
"an added datatype",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1","d2"],"identifier":"i1","privileges":["p1"],"amount":"50"}"#,
),
(
"an added privilege",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1","p2"],"amount":"50"}"#,
),
(
"a changed type-defined member",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"5000"}"#,
),
(
"a dropped type-defined member, whose absence this server cannot interpret",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"]}"#,
),
(
"an added type-defined member",
r#"{"type":"t","locations":["l1","l2"],"actions":["a1","a2"],"datatypes":["d1"],"identifier":"i1","privileges":["p1"],"amount":"50","extra":true}"#,
),
] {
assert!(
!detail(wider).is_narrowing_of(&granted),
"{why} is not a narrowing and must be refused"
);
}
}
#[test]
fn an_absent_granted_list_can_only_be_narrowed_to_itself() {
let granted = detail(r#"{"type":"t"}"#);
assert!(detail(r#"{"type":"t"}"#).is_narrowing_of(&granted));
assert!(!detail(r#"{"type":"t","locations":["l1"]}"#).is_narrowing_of(&granted));
assert!(!detail(r#"{"type":"t","actions":["a1"]}"#).is_narrowing_of(&granted));
}
#[test]
fn narrow_covers_every_requested_element_against_the_whole_granted_set() {
let granted =
AuthorizationDetails::parse(r#"[{"type":"a","actions":["x"]},{"type":"b"}]"#).unwrap();
assert_eq!(
granted.narrow(&AuthorizationDetails::none()).unwrap(),
granted,
"no narrowing asked for means the grant passes through"
);
let just_b = AuthorizationDetails::parse(r#"[{"type":"b"}]"#).unwrap();
assert_eq!(granted.narrow(&just_b).unwrap(), just_b);
let ungranted = AuthorizationDetails::parse(r#"[{"type":"c"}]"#).unwrap();
assert_eq!(
granted.narrow(&ungranted).unwrap_err().error,
ErrorCode::InvalidAuthorizationDetails
);
assert_eq!(
AuthorizationDetails::none()
.narrow(&just_b)
.unwrap_err()
.error,
ErrorCode::InvalidAuthorizationDetails
);
}
#[test]
fn is_subset_handles_empties_and_duplicates() {
let l1: Vec<Box<str>> = vec!["a".into()];
let l2: Vec<Box<str>> = vec!["a".into(), "b".into()];
assert!(is_subset(&[], &[]));
assert!(is_subset(&[], &l2));
assert!(is_subset(&l1, &l2));
assert!(!is_subset(&l2, &l1));
let twice: Vec<Box<str>> = vec!["a".into(), "a".into()];
assert!(is_subset(&twice, &l1));
}