use super::*;
use crate::service::test_rpc::TestRpc;
#[test]
fn public_broadcast_locator_names_the_owner_history_lookup() {
let config = evm::chain_config("1").unwrap();
let order = super::super::tests::build_order_for_record();
let body = broadcast_body(config, &order, &Bytes::from(vec![1, 2, 3]));
let identity = publication_identity(BROADCAST_URL, &body).unwrap();
let displayed = crate::redact::urls(&identity);
assert!(displayed.contains("https://broadcast.intentscan.net"));
assert!(!displayed.contains("[redacted]"));
for text in ["GET /v1/intents", "owner", "chain_ids", "order_hash", "before_ms", "next_before_ms"] {
assert!(displayed.contains(text), "{displayed}");
}
assert!(displayed.contains(&format!("{:?}", order.owner)));
let private = publication_identity("https://rpc.example/SECRET?token=HIDDEN", &body).unwrap();
assert!(!private.contains("SECRET") && !private.contains("HIDDEN"));
}
#[test]
fn audit_fix_broadcast_identity_is_printed_before_publication() {
let config = evm::chain_config("1").unwrap();
let order = super::super::tests::build_order_for_record();
let body = broadcast_body(config, &order, &Bytes::from(vec![1, 2, 3]));
if let Ok(url) = std::env::var("BROADCAST_IDENTITY_CHILD") {
let result = tokio::runtime::Runtime::new().unwrap().block_on(publish(&url, &body)).unwrap();
assert_eq!(result["identity_printed_before_request"], true);
return;
}
let path = std::env::temp_dir().join(format!("broadcast-identity-{}.log", std::process::id()));
let capture = std::fs::File::create(&path).unwrap();
let observed = path.clone();
let rpc = TestRpc::start(move |_| {
let text = std::fs::read_to_string(&observed).unwrap();
let printed = text.contains(&format!("{:?}", order_types::order_id(&order)))
&& text.contains("chain 1") && text.contains("agentswap-v6")
&& text.contains(&format!("{:?}", config.settler));
Some(serde_json::json!({"identity_printed_before_request": printed}))
});
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["audit_fix_broadcast_identity_is_printed_before_publication", "--nocapture"])
.env("BROADCAST_IDENTITY_CHILD", &rpc.url).stderr(capture).output().unwrap();
let stderr = std::fs::read_to_string(&path).unwrap();
std::fs::remove_file(path).unwrap();
assert!(output.status.success(), "{}: {stderr}", String::from_utf8_lossy(&output.stdout));
assert!(stderr.contains(&format!("{:?}", order_types::order_id(&super::super::tests::build_order_for_record()))), "{stderr}");
}
#[tokio::test]
async fn audit_fix_broadcast_error_preserves_safe_bounded_response_body() {
use std::io::{Read, Write};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let worker = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = [0; 4096];
stream.read(&mut request).unwrap();
let body = serde_json::json!({"error": "unsupported chain; https://rpc.example/key/SECRET?token=HIDDEN",
"api_key": "NEVER_PRINT", "auth": "SIGNED_AUTH", "detail": "x".repeat(2048)}).to_string();
write!(stream, "HTTP/1.1 422 Unprocessable Entity\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).unwrap();
});
let error = publish(&url, &serde_json::json!({})).await.unwrap_err().to_string();
worker.join().unwrap();
assert!(error.contains("422") && error.contains("unsupported chain"), "{error}");
assert!(error.contains("[truncated]") && error.len() < 1400, "diagnostic must be bounded");
for secret in ["SECRET", "HIDDEN", "NEVER_PRINT", "SIGNED_AUTH"] { assert!(!error.contains(secret)); }
}
#[test]
fn submission_modes_are_exclusive_and_ethereum_never_uses_sponsored_relay() {
for id in ["1", "8453"] {
let config = evm::chain_config(id).unwrap();
for bits in 0u8..8 {
let mut input = super::super::tests::input("USDC", "1");
input.relay = bits & 1 != 0;
input.self_submit = bits & 2 != 0;
input.broadcast = bits & 4 != 0;
for dry_run in [false, true] {
input.dry_run = dry_run;
let valid = bits.count_ones() <= 1 && (dry_run || bits != 0)
&& !(id == "1" && input.relay);
assert_eq!(validate(config, &input, true).is_ok(), valid, "{id}/{bits}/{dry_run}");
}
}
}
}
#[tokio::test]
async fn a_transport_failure_retains_the_intent_locator_without_the_envelope() {
let config = evm::chain_config("1").unwrap();
let order = super::super::tests::build_order_for_record();
let body = broadcast_body(config, &order, &Bytes::from(vec![7; 65]));
let error = publish("http://127.0.0.1:9/SECRET?token=HIDDEN", &body).await.unwrap_err().to_string();
assert!(error.contains(&format!("{:?}", order_types::order_id(&order))));
assert!(error.contains("chain 1") && error.contains("agentswap-v6"));
assert!(error.contains(&format!("{:?}", config.settler)));
assert!(error.contains("admission is unknown") && error.contains("re-running after an error signs a new intent"));
for secret in ["SECRET", "HIDDEN", body["auth"].as_str().unwrap()] { assert!(!error.contains(secret)); }
}
#[tokio::test]
async fn broadcast_posts_v6_wire_order_with_the_chain_settler() {
for id in ["1", "8453"] {
let config = evm::chain_config(id).unwrap();
let order = super::super::tests::build_order_for_record();
let envelope = Bytes::from(vec![1, 2, 3]);
let body = broadcast_body(config, &order, &envelope);
assert_eq!(body["chainId"], config.id);
assert_eq!(body["standard"], "agentswap-v6");
assert_eq!(body["settler"], format!("{:?}", config.settler));
assert_eq!(body["auth"], "0x010203");
let encoded = hex::decode(body["order"].as_str().unwrap().trim_start_matches("0x")).unwrap();
let decoded = Order::abi_decode(&encoded).unwrap();
assert_eq!(order_types::order_id(&decoded), order_types::order_id(&order));
let expected = body.clone();
let rpc = TestRpc::start(move |received| {
assert_eq!(received, &expected);
Some(serde_json::json!({"accepted": true}))
});
assert_eq!(publish(&rpc.url, &body).await.unwrap()["accepted"], true);
assert_eq!(rpc.called(""), 1);
}
}
#[tokio::test]
async fn broadcast_rejects_redirects_and_accepts_empty_admission_responses() {
use std::io::{Read, Write};
use std::net::TcpListener;
for status in ["201 Created", "302 Found", "422 Unprocessable Entity"] {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let worker = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
stream.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap();
let mut data = vec![0; 4096];
let count = stream.read(&mut data).unwrap();
let request = String::from_utf8_lossy(&data[..count]).to_ascii_lowercase();
assert!(!request.contains("x-api-key:") && !request.contains("x-payment:"));
write!(stream, "HTTP/1.1 {status}\r\nContent-Length: 0\r\nLocation: http://127.0.0.1:1/\r\nConnection: close\r\n\r\n").unwrap();
});
let result = publish(&url, &serde_json::json!({})).await;
if status.starts_with("201") {
assert_eq!(result.unwrap()["accepted"], true);
} else {
assert!(result.unwrap_err().to_string().contains(&format!("broadcast returned HTTP {}", &status[..3])));
}
worker.join().unwrap();
}
}