From 1a88995803a979f8be60cdc9683afe870acd5815 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:40:27 -0700 Subject: [PATCH] fix(conformance): exercise Rust HTTP payment requests --- conformance/adapters/rust/Cargo.lock | 132 +++++++++++++++++++++- conformance/adapters/rust/Cargo.toml | 4 +- conformance/adapters/rust/adapter.json | 1 + conformance/adapters/rust/src/main.rs | 112 ++++++++++++++++++ conformance/scripts/test_use_local_sdk.py | 23 +++- conformance/scripts/use_local_sdk.py | 2 +- 6 files changed, 269 insertions(+), 5 deletions(-) diff --git a/conformance/adapters/rust/Cargo.lock b/conformance/adapters/rust/Cargo.lock index 74b839b..1b8cb4b 100644 --- a/conformance/adapters/rust/Cargo.lock +++ b/conformance/adapters/rust/Cargo.lock @@ -118,6 +118,16 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -199,6 +209,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -211,6 +230,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -241,6 +266,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.32" @@ -286,6 +317,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -359,6 +409,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -402,9 +453,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -633,6 +686,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.0" @@ -668,9 +727,11 @@ version = "0.1.0" dependencies = [ "hmac", "mpp", + "reqwest", "serde", "serde_json", "sha2", + "tokio", ] [[package]] @@ -844,7 +905,9 @@ checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", + "h2", "http", "http-body", "http-body-util", @@ -853,6 +916,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", "quinn", @@ -944,7 +1008,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -1014,7 +1078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1200,6 +1264,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1300,6 +1385,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.3" @@ -1539,6 +1638,35 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/conformance/adapters/rust/Cargo.toml b/conformance/adapters/rust/Cargo.toml index 2fb77cc..a816060 100644 --- a/conformance/adapters/rust/Cargo.toml +++ b/conformance/adapters/rust/Cargo.toml @@ -9,8 +9,10 @@ name = "adapter" path = "src/main.rs" [dependencies] -mpp = { version = "=0.11.0" } +mpp = { version = "=0.11.0", features = ["client"] } +reqwest = "0.13" serde = { version = "1", features = ["derive"] } serde_json = "1" hmac = "0.13" sha2 = "0.11" +tokio = { version = "1", features = ["rt"] } diff --git a/conformance/adapters/rust/adapter.json b/conformance/adapters/rust/adapter.json index 4c136e0..a422391 100644 --- a/conformance/adapters/rust/adapter.json +++ b/conformance/adapters/rust/adapter.json @@ -23,6 +23,7 @@ "base64url.encode", "base64url.decode", "challenge.id", + "http.payment_request", "tempo.proof.typed_data" ] } diff --git a/conformance/adapters/rust/src/main.rs b/conformance/adapters/rust/src/main.rs index 36c608c..3878308 100644 --- a/conformance/adapters/rust/src/main.rs +++ b/conformance/adapters/rust/src/main.rs @@ -1,9 +1,11 @@ use hmac::{Hmac, KeyInit, Mac}; +use mpp::client::{Fetch, PaymentProvider}; use mpp::protocol::core::{ base64url_decode, base64url_encode, format_authorization, format_receipt, format_www_authenticate, parse_authorization, parse_receipt, parse_www_authenticate, Base64UrlJson, ChallengeEcho, PaymentChallenge, PaymentCredential, Receipt, }; +use mpp::MppError; use serde_json::{json, Value}; use sha2::Sha256; use std::io::{self, Read, Write}; @@ -422,6 +424,14 @@ fn handle_adapter_request() { let op = request.get("op").and_then(|v| v.as_str()).unwrap_or(""); let input_value = request.get("input").cloned().unwrap_or(json!({})); + if op == "http.payment_request" { + match run_http_payment_request(&input_value) { + Ok(value) => print_adapter_success(value), + Err(e) => print_adapter_error(&e, "http_error"), + } + return; + } + let Some(command) = legacy_command_for_operation(op) else { print_adapter_error( &format!("Unknown operation: {}", op), @@ -440,6 +450,108 @@ fn handle_adapter_request() { print_adapter_from_legacy(op, result); } +#[derive(Clone)] +struct ConformancePaymentProvider { + payload: Value, + source: Option, +} + +impl PaymentProvider for ConformancePaymentProvider { + fn supports(&self, method: &str, intent: &str) -> bool { + method == "tempo" && intent == "charge" + } + + async fn pay(&self, challenge: &PaymentChallenge) -> Result { + Ok(PaymentCredential { + challenge: challenge.to_echo(), + source: self.source.clone(), + payload: self.payload.clone(), + }) + } +} + +fn run_http_payment_request(input: &Value) -> Result { + let url = input + .get("url") + .and_then(Value::as_str) + .ok_or_else(|| "missing url".to_string())?; + let method = input + .get("method") + .and_then(Value::as_str) + .ok_or_else(|| "missing method".to_string())? + .parse::() + .map_err(|e| e.to_string())?; + let headers = input + .get("headers") + .and_then(Value::as_object) + .ok_or_else(|| "missing headers".to_string())?; + let body = input.get("body").and_then(Value::as_str); + let mode = input + .get("mode") + .and_then(Value::as_str) + .ok_or_else(|| "missing mode".to_string())?; + + let client = reqwest::Client::new(); + let mut request = client.request(method, url); + for (name, value) in headers { + let value = value + .as_str() + .ok_or_else(|| format!("header {name} must be a string"))?; + request = request.header(name, value); + } + if let Some(body) = body { + request = request.body(body.to_owned()); + } + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| e.to_string())?; + let response = match mode { + "plain" => runtime + .block_on(request.send()) + .map_err(|e| e.to_string())?, + "payment" | "invalid_payload" => { + let payment = input + .get("payment") + .and_then(Value::as_object) + .ok_or_else(|| "missing payment".to_string())?; + let provider = ConformancePaymentProvider { + payload: payment.get("payload").cloned().unwrap_or_else(|| json!({})), + source: payment + .get("source") + .and_then(Value::as_str) + .map(str::to_owned), + }; + runtime + .block_on(request.send_with_payment(&provider)) + .map_err(|e| e.to_string())? + } + _ => return Err(format!("Unsupported http.payment_request mode: {mode}")), + }; + + let status = response.status().as_u16(); + let headers = response + .headers() + .iter() + .filter_map(|(name, value)| { + value + .to_str() + .ok() + .map(|value| (name.to_string(), Value::String(value.to_owned()))) + }) + .collect::>(); + let body = runtime + .block_on(response.text()) + .map_err(|e| e.to_string())?; + + Ok(json!({ + "status": status, + "headers": headers, + "body": body, + })) +} + fn legacy_command_for_operation(op: &str) -> Option<&'static str> { match op { "challenge.parse" => Some("parse-www-authenticate"), diff --git a/conformance/scripts/test_use_local_sdk.py b/conformance/scripts/test_use_local_sdk.py index d9eb792..86d6eeb 100644 --- a/conformance/scripts/test_use_local_sdk.py +++ b/conformance/scripts/test_use_local_sdk.py @@ -9,7 +9,28 @@ from pathlib import Path from unittest.mock import patch -from use_local_sdk import configure_go, go_sdk_version +from use_local_sdk import configure_go, configure_rust, go_sdk_version + + +class RustSdkConfigurationTest(unittest.TestCase): + def test_preserves_client_feature_for_local_checkout(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + manifest = root / "conformance" / "adapters" / "rust" / "Cargo.toml" + manifest.parent.mkdir(parents=True) + manifest.write_text( + '[dependencies]\nmpp = { version = "=0.11.0", features = ["client"] }\n', + encoding="utf-8", + ) + sdk_path = root / "sdk" + sdk_path.mkdir() + + configure_rust(root / "conformance", sdk_path) + + self.assertIn( + f'mpp = {{ path = "{sdk_path}", features = ["client"] }}', + manifest.read_text(encoding="utf-8"), + ) class GoSdkVersionTest(unittest.TestCase): diff --git a/conformance/scripts/use_local_sdk.py b/conformance/scripts/use_local_sdk.py index ae0eba6..9cf1fb1 100644 --- a/conformance/scripts/use_local_sdk.py +++ b/conformance/scripts/use_local_sdk.py @@ -46,7 +46,7 @@ def configure_rust(conformance_dir: Path, sdk_path: Path) -> None: replace_one( manifest, r"^mpp\s*=\s*.+$", - f"mpp = {{ path = {json_string(sdk_path)} }}", + f"mpp = {{ path = {json_string(sdk_path)}, features = [\"client\"] }}", "Rust mpp dependency", )