use proptest::prelude::*;
use serde_json::{Map as JsonMap, Value as JsonValue};
use super::*;
fn app_id() -> PaykitAppId {
PaykitAppId::new("test-app").unwrap()
}
macro_rules! declare_private_message_kind_routing {
($($variant:ident => $routed:literal),+ $(,)?) => {
const PRIVATE_MESSAGE_KIND_ROUTING: &[(PrivateMessageKind, bool)] =
&[$((PrivateMessageKind::$variant, $routed)),+];
#[allow(dead_code)]
fn private_message_kind_routing_is_exhaustive(kind: PrivateMessageKind) -> bool {
match kind {
$(PrivateMessageKind::$variant => $routed,)+
}
}
};
}
declare_private_message_kind_routing! {
PrivatePaymentList => false,
DeliveryConfirmation => false,
ReceiptAccess => false,
PaymentRequest => true,
PaymentRequestAcceptance => true,
PaymentRequestRejection => true,
PaymentRequestCancellation => true,
PaymentProof => true,
PaymentConversionQuote => true,
AllowanceProposal => false,
AllowanceAcceptance => false,
AllowanceRejection => false,
AllowanceEnd => false,
}
fn routed_kind_strings() -> Vec<&'static str> {
PRIVATE_MESSAGE_KIND_ROUTING
.iter()
.filter(|&&(_, routed)| routed)
.map(|&(kind, _)| kind.as_str())
.collect()
}
fn utc_timestamp() -> impl Strategy<Value = String> {
(
2000u32..2100,
1u32..=12,
1u32..=28,
0u32..=23,
0u32..=59,
0u32..=59,
)
.prop_map(|(y, mo, d, h, mi, s)| format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z"))
}
fn uuid_v4() -> impl Strategy<Value = String> {
any::<[u8; 16]>().prop_map(|bytes| {
uuid::Builder::from_random_bytes(bytes)
.into_uuid()
.to_string()
})
}
fn event_id() -> impl Strategy<Value = EventId> {
uuid_v4().prop_map(|id| EventId::new(id).expect("uuid v4 is a valid Event ID"))
}
fn payment_request_id() -> impl Strategy<Value = PaymentRequestId> {
uuid_v4()
.prop_map(|id| PaymentRequestId::new(id).expect("uuid v4 is a valid Payment Request ID"))
}
fn payment_amount() -> impl Strategy<Value = PaymentAmount> {
("[0-9]{1,12}(\\.[0-9]{1,8})?", "[a-zA-Z0-9]{1,10}")
.prop_map(|(value, asset)| PaymentAmount::new(value, asset).unwrap())
}
fn payment_reference() -> impl Strategy<Value = PaymentReference> {
"[a-zA-Z0-9 ._:/-]{1,32}"
.prop_map(|s| PaymentReference::new(s).expect("generated reference is valid"))
}
fn payment_endpoint_identifier() -> impl Strategy<Value = PaymentEndpointIdentifier> {
"ep-[a-zA-Z0-9]{1,12}(-[a-zA-Z0-9]{1,8}){0,2}"
.prop_map(|s| PaymentEndpointIdentifier::new(s).expect("generated identifier is valid"))
}
fn recurrence_unit() -> impl Strategy<Value = RecurrenceUnit> {
prop_oneof![
Just(RecurrenceUnit::Minute),
Just(RecurrenceUnit::Hour),
Just(RecurrenceUnit::Day),
Just(RecurrenceUnit::Week),
Just(RecurrenceUnit::Month),
Just(RecurrenceUnit::Year),
]
}
fn recurrence_window() -> impl Strategy<Value = (String, Option<String>)> {
(
2000u32..2050,
1u32..=12,
1u32..=28,
0u32..=23,
0u32..=59,
0u32..=59,
proptest::option::of(1u32..=50),
)
.prop_map(|(y, mo, d, h, mi, s, end_year_offset)| {
let starts_at = format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z");
let ends_at = end_year_offset
.map(|offset| format!("{:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z", y + offset));
(starts_at, ends_at)
})
}
fn recurrence() -> impl Strategy<Value = Recurrence> {
(
1u32..=1000,
recurrence_unit(),
recurrence_window(),
utc_timestamp(),
)
.prop_map(|(every, unit, (starts_at, ends_at), anchor)| {
Recurrence::try_from(crate::RecurrenceConfig {
every,
unit,
starts_at,
anchor,
ends_at,
})
.unwrap()
})
}
fn json_string_map() -> impl Strategy<Value = JsonMap<String, JsonValue>> {
proptest::collection::hash_map("[a-zA-Z0-9_]{1,12}", "[a-zA-Z0-9 ]{0,16}", 0..4).prop_map(
|entries| {
entries
.into_iter()
.map(|(k, v)| (k, JsonValue::String(v)))
.collect()
},
)
}
fn payment_request_terms() -> impl Strategy<Value = PaymentRequestTerms> {
(
payment_amount(),
payment_reference(),
proptest::option::of(utc_timestamp()),
proptest::option::of(recurrence()),
proptest::collection::vec(payment_endpoint_identifier(), 1..=4),
json_string_map(),
)
.prop_map(
|(amount, payment_reference, proposal_expires_at, recurrence, ids, metadata)| {
PaymentRequestTerms::builder(amount, payment_reference, ids)
.proposal_expires_at(proposal_expires_at)
.recurrence(recurrence)
.metadata(metadata)
.build()
.unwrap()
},
)
}
fn payment_request() -> impl Strategy<Value = PaymentRequest> {
(event_id(), payment_request_id(), payment_request_terms())
.prop_map(|(event_id, id, terms)| PaymentRequest::new(event_id, id, terms))
}
fn billing_period() -> impl Strategy<Value = BillingPeriod> {
(2000u32..2050, 1u32..=12, 1u32..=28).prop_map(|(y, mo, d)| {
BillingPeriod::new(
format!("{y:04}-{mo:02}-{d:02}T00:00:00Z"),
format!("{:04}-{mo:02}-{d:02}T00:00:00Z", y + 1),
)
.unwrap()
})
}
fn payment_proof() -> impl Strategy<Value = PaymentProof> {
(
event_id(),
payment_request_id(),
payment_reference(),
proptest::option::of(billing_period()),
payment_endpoint_identifier(),
json_string_map(),
)
.prop_map(
|(event_id, id, reference, billing_period, endpoint, proof)| {
PaymentProof::new(
event_id,
id,
reference,
billing_period,
app_id(),
endpoint,
proof,
)
},
)
}
fn optional_reason() -> impl Strategy<Value = Option<String>> {
proptest::option::of("[a-zA-Z0-9 _-]{1,24}")
}
fn payment_request_event() -> impl Strategy<Value = PaymentRequestEvent> {
prop_oneof![
payment_request().prop_map(PaymentRequestEvent::Request),
(event_id(), payment_request_id()).prop_map(|(e, p)| PaymentRequestEvent::Acceptance(
PaymentRequestAcceptance::new(e, p)
)),
(event_id(), payment_request_id(), optional_reason()).prop_map(|(e, p, r)| {
PaymentRequestEvent::Rejection(PaymentRequestRejection::new(e, p, r))
}),
(event_id(), payment_request_id(), optional_reason()).prop_map(|(e, p, r)| {
PaymentRequestEvent::Cancellation(PaymentRequestCancellation::new(e, p, r))
}),
payment_proof().prop_map(PaymentRequestEvent::Proof),
]
}
fn arb_json() -> impl Strategy<Value = JsonValue> {
let leaf = prop_oneof![
Just(JsonValue::Null),
any::<bool>().prop_map(JsonValue::Bool),
any::<i64>().prop_map(|n| JsonValue::Number(n.into())),
".*".prop_map(JsonValue::String),
];
leaf.prop_recursive(4, 32, 6, |inner| {
prop_oneof![
proptest::collection::vec(inner.clone(), 0..6).prop_map(JsonValue::Array),
proptest::collection::hash_map("[a-zA-Z0-9_]{0,12}", inner, 0..6)
.prop_map(|m| JsonValue::Object(m.into_iter().collect())),
]
})
}
fn arb_kinded_json() -> impl Strategy<Value = String> {
(prop::sample::select(routed_kind_strings()), arb_json()).prop_map(|(kind, body)| {
let mut object = match body {
JsonValue::Object(map) => map,
other => {
let mut map = JsonMap::new();
map.insert("body".to_string(), other);
map
}
};
object.insert("kind".to_string(), JsonValue::String(kind.to_string()));
JsonValue::Object(object).to_string()
})
}
fn exercise(raw_json: String) {
let message = PrivateApplicationMessage {
version: None,
kind: None,
app_id: None,
raw_json,
};
let _ = format!("{message:?}");
if let Some(parsed) = parse_payment_request_event_message(&message) {
let _ = parsed.is_valid();
let _ = parsed.kind();
let _ = parsed.event_id();
let _ = parsed.payment_request_id();
let _ = parsed.validation_error();
let _ = parsed.parsed_event();
let _ = format!("{parsed:?}");
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn valid_event_round_trips(event in payment_request_event()) {
let serialized = serialize_payment_request_event(&app_id(), &event)
.expect("construction-valid events must serialize");
let message = PrivateApplicationMessage {
version: Some(1),
kind: Some(event.kind().as_str().to_string()),
app_id: Some(app_id().as_str().to_string()),
raw_json: serialized,
};
let parsed = parse_payment_request_event_message(&message)
.expect("a recognized Payment Request kind must be routed");
prop_assert!(parsed.is_valid(), "validation error: {:?}", parsed.validation_error());
prop_assert_eq!(parsed.parsed_event(), Some(&event));
}
#[test]
fn arbitrary_strings_never_panic(raw in any::<String>()) {
exercise(raw);
}
#[test]
fn arbitrary_json_never_panics(value in arb_json()) {
exercise(value.to_string());
}
#[test]
fn kinded_json_never_panics(raw in arb_kinded_json()) {
exercise(raw);
}
}
#[test]
fn test_payment_request_routing_covers_all_private_message_kinds() {
for &(kind, expected_routed) in PRIVATE_MESSAGE_KIND_ROUTING {
let message = PrivateApplicationMessage {
version: None,
kind: None,
app_id: None,
raw_json: format!(r#"{{"kind":"{}"}}"#, kind.as_str()),
};
assert_eq!(
message.known_kind(),
Some(kind),
"PrivateMessageKind::parse does not round-trip {kind}"
);
let parsed = parse_payment_request_event_message(&message);
if let Some(event_message) = &parsed {
let _ = event_message.is_valid();
let _ = event_message.kind();
let _ = event_message.event_id();
let _ = event_message.payment_request_id();
let _ = event_message.validation_error();
let _ = event_message.parsed_event();
let _ = format!("{event_message:?}");
}
assert_eq!(
parsed.is_some(),
expected_routed,
"parser routing for {kind} disagreed with PRIVATE_MESSAGE_KIND_ROUTING"
);
}
}