#![allow(unused_crate_dependencies, reason = "sibling tests consume them")]
#![allow(
clippy::tests_outside_test_module,
reason = "integration test binaries put #[test] fns at file scope"
)]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::missing_panics_doc,
reason = "idiomatic test-code patterns"
)]
use std::collections::HashMap;
use compact_str::CompactString;
use proptest::collection::{hash_map, vec};
use proptest::prelude::*;
use r402_core::ErrorReason;
use r402_core::chain::ChainId;
use r402_core::wire::{
ExtensionEntry, Extensions, PaymentRequired, PaymentRequirements, ResourceInfo, SettleResponse,
SupportedPaymentKind, SupportedResponse, VerifyResponse,
};
fn arb_compact_string() -> impl Strategy<Value = CompactString> {
"[a-zA-Z0-9_:./@\\- ]{0,64}".prop_map(CompactString::from)
}
fn arb_chain_id() -> impl Strategy<Value = ChainId> {
prop_oneof![
"(eip155|solana|aptos|stellar):[a-z0-9-]{1,32}",
"(eip155|solana):\\*",
]
.prop_filter_map("must parse as ChainId", |raw| raw.parse::<ChainId>().ok())
}
fn arb_resource_info() -> impl Strategy<Value = ResourceInfo> {
(
arb_compact_string(),
proptest::option::of(arb_compact_string()),
proptest::option::of(arb_compact_string()),
)
.prop_map(|(url, description, mime)| {
let mut info = ResourceInfo::new(url);
if let Some(d) = description {
info = info.with_description(d);
}
if let Some(m) = mime {
info = info.with_mime_type(m);
}
info
})
}
fn arb_supported_payment_kind() -> impl Strategy<Value = SupportedPaymentKind> {
let extra_value = prop_oneof![
any::<bool>().prop_map(serde_json::Value::Bool),
any::<i64>().prop_map(serde_json::Value::from),
arb_compact_string().prop_map(|s| serde_json::Value::String(s.to_string())),
];
(
any::<u8>(),
arb_compact_string(),
arb_compact_string(),
proptest::option::of(extra_value),
)
.prop_map(|(version, scheme, network, extra)| {
SupportedPaymentKind::new(version, scheme, network).with_optional_extra(extra)
})
}
fn arb_supported_response() -> impl Strategy<Value = SupportedResponse> {
let kinds = vec(arb_supported_payment_kind(), 0..4);
let extensions = vec(arb_compact_string(), 0..4);
let signers = hash_map(arb_compact_string(), vec(arb_compact_string(), 0..3), 0..4);
(kinds, extensions, signers).prop_map(|(kinds, extensions, signers)| {
let map: HashMap<CompactString, Vec<CompactString>> = signers.into_iter().collect();
SupportedResponse::new()
.with_kinds(kinds)
.with_extensions(extensions)
.with_signers(map)
})
}
fn arb_extensions() -> impl Strategy<Value = Extensions> {
let entry = prop_oneof![
any::<i64>().prop_map(|n| ExtensionEntry::info(serde_json::json!({ "n": n }))),
any::<bool>().prop_map(|b| ExtensionEntry::raw(serde_json::Value::Bool(b))),
Just(ExtensionEntry::with_schema(
serde_json::json!({ "registered": true }),
serde_json::json!({ "type": "object" }),
)),
];
vec(("[a-z][a-z0-9-]{0,16}", entry), 0..3)
.prop_map(|kvs| kvs.into_iter().collect::<Extensions>())
}
fn arb_payment_requirements() -> impl Strategy<Value = PaymentRequirements> {
(
arb_compact_string(), arb_chain_id(),
"[0-9]{1,20}".prop_map(CompactString::from),
arb_compact_string(), arb_compact_string(), any::<u64>(), )
.prop_map(|(scheme, network, amount, pay_to, asset, max_timeout)| {
PaymentRequirements::new(scheme, network, amount, pay_to, asset, max_timeout)
})
}
fn arb_payment_required() -> impl Strategy<Value = PaymentRequired> {
(
arb_resource_info(),
vec(arb_payment_requirements(), 0..3),
proptest::option::of(arb_compact_string()),
arb_extensions(),
)
.prop_map(|(resource, accepts, error, extensions)| {
let mut body = PaymentRequired::new(resource).with_accepts(accepts);
if let Some(err) = error {
body = body.with_error(err);
}
body.with_extensions(extensions)
})
}
fn arb_error_reason() -> impl Strategy<Value = ErrorReason> {
prop_oneof![
Just(ErrorReason::InsufficientFunds),
Just(ErrorReason::InvalidPayload),
Just(ErrorReason::InvalidNetwork),
Just(ErrorReason::UnexpectedSettleError),
Just(ErrorReason::DuplicateSettlement),
Just(ErrorReason::Permit2AllowanceRequired),
Just(ErrorReason::InvalidExactSolanaPayloadMemoMismatch),
"[a-z][a-z0-9_]{1,40}".prop_map(|s| ErrorReason::from_wire(&s)),
]
}
fn arb_verify_response() -> impl Strategy<Value = VerifyResponse> {
prop_oneof![
(arb_compact_string(), arb_extensions())
.prop_map(|(payer, extensions)| { VerifyResponse::Valid { payer, extensions } }),
(
arb_error_reason(),
proptest::option::of(arb_compact_string()),
proptest::option::of(arb_compact_string()),
arb_extensions(),
)
.prop_map(|(reason, message, payer, extensions)| {
VerifyResponse::Invalid {
reason,
message,
payer,
extensions,
}
}),
]
}
fn arb_non_empty_compact_string() -> impl Strategy<Value = CompactString> {
"[a-zA-Z0-9_:./@\\- ]{1,64}".prop_map(CompactString::from)
}
fn arb_settle_response() -> impl Strategy<Value = SettleResponse> {
prop_oneof![
(
arb_compact_string(), arb_non_empty_compact_string(), arb_compact_string(), proptest::option::of("[0-9]{1,20}".prop_map(CompactString::from)),
arb_extensions(),
)
.prop_map(|(payer, transaction, network, amount, extensions)| {
SettleResponse::Success {
payer,
transaction,
network,
amount,
extensions,
}
}),
(
arb_error_reason(),
proptest::option::of(arb_compact_string()),
proptest::option::of(arb_compact_string()),
arb_compact_string(),
arb_extensions(),
)
.prop_map(|(reason, message, payer, network, extensions)| {
SettleResponse::Failure {
reason,
message,
payer,
network,
extensions,
}
}),
]
}
proptest! {
#![proptest_config(ProptestConfig {
// 256 cases per property hits a sweet spot — enough coverage to
// expose serialisation regressions, fast enough to keep CI under
// a few seconds for the entire suite.
cases: 256,
..ProptestConfig::default()
})]
#[test]
fn resource_info_roundtrips(info in arb_resource_info()) {
let json = serde_json::to_vec(&info).unwrap();
let back: ResourceInfo = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, info);
}
#[test]
fn supported_payment_kind_roundtrips(kind in arb_supported_payment_kind()) {
let json = serde_json::to_vec(&kind).unwrap();
let back: SupportedPaymentKind = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, kind);
}
#[test]
fn supported_response_roundtrips(resp in arb_supported_response()) {
let json = serde_json::to_vec(&resp).unwrap();
let back: SupportedResponse = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, resp);
}
#[test]
fn extensions_roundtrip(ext in arb_extensions()) {
let json = serde_json::to_vec(&ext).unwrap();
let back: Extensions = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, ext);
}
#[test]
fn payment_requirements_roundtrips(req in arb_payment_requirements()) {
let json = serde_json::to_vec(&req).unwrap();
let back: PaymentRequirements = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, req);
}
#[test]
fn payment_required_roundtrips(body in arb_payment_required()) {
let json = serde_json::to_vec(&body).unwrap();
let back: PaymentRequired = serde_json::from_slice(&json).unwrap();
let lhs = serde_json::to_value(&body).unwrap();
let rhs = serde_json::to_value(&back).unwrap();
prop_assert_eq!(lhs, rhs);
}
#[test]
fn verify_response_roundtrips(resp in arb_verify_response()) {
let json = serde_json::to_vec(&resp).unwrap();
let back: VerifyResponse = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, resp);
}
#[test]
fn settle_response_roundtrips(resp in arb_settle_response()) {
let json = serde_json::to_vec(&resp).unwrap();
let back: SettleResponse = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, resp);
}
#[test]
fn error_reason_roundtrips(reason in arb_error_reason()) {
let json = serde_json::to_vec(&reason).unwrap();
let back: ErrorReason = serde_json::from_slice(&json).unwrap();
prop_assert_eq!(back, reason);
}
#[test]
fn error_reason_from_wire_is_total(input in "[\\x20-\\x7e]{0,64}") {
let reason = ErrorReason::from_wire(&input);
prop_assert_eq!(reason.as_str(), input.as_str());
}
}