#![allow(clippy::unwrap_used)]
use osproxy_core::FieldName;
use osproxy_rewrite::{
construct_id, construct_id_bytes, inject_fields, inject_fields_bytes, strip_fields,
};
use proptest::prelude::*;
use serde_json::{Map, Value};
fn client_object() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
any::<bool>().prop_map(Value::from),
any::<i64>().prop_map(Value::from),
"[a-zA-Z0-9 ]{0,8}".prop_map(Value::from),
];
let value = prop_oneof![
leaf.clone(),
prop::collection::vec(leaf.clone(), 0..3).prop_map(Value::Array),
prop::collection::vec(("[a-z]{1,4}", leaf), 0..3).prop_map(into_object),
];
prop::collection::vec(("[a-z]{1,6}", value), 0..6).prop_map(into_object)
}
fn into_object(entries: Vec<(impl Into<String>, Value)>) -> Value {
let mut obj = Map::new();
for (k, v) in entries {
obj.insert(k.into(), v);
}
Value::Object(obj)
}
fn injected_fields() -> impl Strategy<Value = Vec<(FieldName, Value)>> {
prop::collection::vec(("_[a-z]{1,6}", "[a-z0-9]{0,8}"), 0..4).prop_map(|pairs| {
let mut seen = std::collections::HashSet::new();
pairs
.into_iter()
.filter(|(k, _)| seen.insert(k.clone()))
.map(|(k, v)| (FieldName::from(k.as_str()), Value::from(v)))
.collect()
})
}
proptest! {
#[test]
fn inject_bytes_matches_value_inject(
original in client_object(),
fields in injected_fields(),
) {
let body = serde_json::to_vec(&original).unwrap();
let mut via_tree = original.clone();
inject_fields(&mut via_tree, &fields).unwrap();
let spliced = inject_fields_bytes(&body, &fields).unwrap();
let via_bytes: Value = serde_json::from_slice(&spliced).unwrap();
prop_assert_eq!(via_bytes, via_tree);
}
#[test]
fn inject_bytes_then_strip_is_identity(
original in client_object(),
fields in injected_fields(),
) {
let body = serde_json::to_vec(&original).unwrap();
let spliced = inject_fields_bytes(&body, &fields).unwrap();
let mut doc: Value = serde_json::from_slice(&spliced).unwrap();
let names: Vec<_> = fields.iter().map(|(n, _)| n.clone()).collect();
strip_fields(&mut doc, &names);
prop_assert_eq!(doc, original);
}
#[test]
fn spoofed_reserved_field_is_rejected_like_the_tree(
field in "_[a-z]{1,6}",
escape in any::<bool>(),
) {
let name = FieldName::from(field.as_str());
let body = if escape {
let first = field.as_bytes()[0];
let rest = &field[1..];
format!(r#"{{"\u{first:04x}{rest}":"evil"}}"#).into_bytes()
} else {
format!(r#"{{"{field}":"evil"}}"#).into_bytes()
};
let err = inject_fields_bytes(&body, &[(name, Value::from("acme"))]);
prop_assert!(err.is_err(), "escaped={escape} body must be rejected");
}
}
proptest! {
#[test]
fn construct_id_bytes_matches_value_construct(
key in "[a-z]{1,6}",
natural in "[a-zA-Z0-9]{1,10}",
extra in client_object(),
) {
let mut obj = match extra {
Value::Object(m) => m,
_ => Map::new(),
};
obj.insert(key.clone(), Value::from(natural.clone()));
let doc = Value::Object(obj);
let body = serde_json::to_vec(&doc).unwrap();
let template = format!("{{partition}}:{{body.{key}}}");
let via_tree = construct_id(&template, "acme", &doc).unwrap();
let via_bytes = construct_id_bytes(&template, "acme", &body).unwrap();
prop_assert_eq!(via_bytes, via_tree);
}
}