#![cfg(all(feature = "overlay", feature = "http"))]
use bsv_rs::overlay::{
HttpsOverlayBroadcastFacilitator, HttpsOverlayLookupFacilitator, LookupAnswer, LookupQuestion,
OverlayBroadcastFacilitator, OverlayLookupFacilitator, TaggedBEEF,
};
use wiremock::matchers::{body_string_contains, header, header_regex, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_broadcast_success() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tm_topic1": {
"outputsToAdmit": [0],
"coinsToRetain": [],
"coinsRemoved": []
}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01, 0x02, 0x03], vec!["tm_topic1".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let steak = result.unwrap();
assert!(steak.contains_key("tm_topic1"));
let instructions = steak.get("tm_topic1").unwrap();
assert_eq!(instructions.outputs_to_admit, vec![0]);
}
#[tokio::test]
async fn test_broadcast_success_multiple_topics() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tm_topic1": {
"outputsToAdmit": [0, 1],
"coinsToRetain": [2]
},
"tm_topic2": {
"outputsToAdmit": [],
"coinsToRetain": [],
"coinsRemoved": [3]
}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(
vec![0x01, 0x02, 0x03],
vec!["tm_topic1".to_string(), "tm_topic2".to_string()],
);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let steak = result.unwrap();
assert_eq!(steak.len(), 2);
assert_eq!(steak["tm_topic1"].outputs_to_admit, vec![0, 1]);
assert_eq!(steak["tm_topic1"].coins_to_retain, vec![2]);
assert_eq!(steak["tm_topic2"].coins_removed, Some(vec![3]));
}
#[tokio::test]
async fn test_broadcast_content_type_header() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.and(header("Content-Type", "application/octet-stream"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_broadcast_x_topics_header() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.and(header("X-Topics", "[\"tm_test_topic\"]"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test_topic".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_broadcast_x_topics_header_multiple() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.and(header_regex("X-Topics", r#".*tm_foo.*tm_bar.*"#))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_foo".to_string(), "tm_bar".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_broadcast_sends_beef_in_body() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tm_test": {
"outputsToAdmit": [0],
"coinsToRetain": []
}
})))
.expect(1)
.mount(&mock_server)
.await;
let beef_data = vec![0xBE, 0xEF, 0xCA, 0xFE];
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(beef_data, vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_broadcast_with_off_chain_values() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.and(header("x-includes-off-chain-values", "true"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tm_test": {
"outputsToAdmit": [0],
"coinsToRetain": []
}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::with_off_chain_values(
vec![0x01, 0x02, 0x03],
vec!["tm_test".to_string()],
vec![0xFF, 0xFE],
);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_broadcast_without_off_chain_values_no_header() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tm_test": {
"outputsToAdmit": [],
"coinsToRetain": []
}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_broadcast_error_400_bad_request() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(400).set_body_string("Bad Request: invalid BEEF"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("400"),
"Expected error to contain status 400, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_broadcast_error_404_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("404"),
"Expected error to contain status 404, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_broadcast_error_500_server_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("500"),
"Expected error to contain status 500, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_broadcast_empty_steak_response() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let steak = result.unwrap();
assert!(steak.is_empty(), "Expected empty STEAK");
}
#[tokio::test]
async fn test_broadcast_malformed_json_response() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_string("this is not valid json"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(
result.is_err(),
"Expected error on malformed JSON, got: {:?}",
result
);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("STEAK"),
"Expected STEAK parse error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_broadcast_rejects_http_when_not_allowed() {
let facilitator = HttpsOverlayBroadcastFacilitator::new(false);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator
.send("http://insecure.host/", &tagged_beef)
.await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("https"),
"Expected HTTPS requirement error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_broadcast_allows_http_when_configured() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(
result.is_ok(),
"Expected success with allow_http=true, got: {:?}",
result
);
}
#[tokio::test]
async fn test_broadcast_url_trailing_slash_trimmed() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let url_with_slash = format!("{}/", mock_server.uri());
let result = facilitator.send(&url_with_slash, &tagged_beef).await;
assert!(
result.is_ok(),
"Expected success with trailing slash URL, got: {:?}",
result
);
}
#[tokio::test]
async fn test_broadcast_steak_with_coins_removed() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"tm_test": {
"outputsToAdmit": [],
"coinsToRetain": [],
"coinsRemoved": [0, 1, 2]
}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let steak = result.unwrap();
let instructions = steak.get("tm_test").unwrap();
assert_eq!(instructions.coins_removed, Some(vec![0, 1, 2]));
assert!(instructions.outputs_to_admit.is_empty());
assert!(instructions.coins_to_retain.is_empty());
}
#[tokio::test]
async fn test_broadcast_facilitator_default() {
let facilitator = HttpsOverlayBroadcastFacilitator::default();
let tagged_beef = TaggedBEEF::new(vec![0x01], vec!["tm_test".to_string()]);
let result = facilitator
.send("http://insecure.host/", &tagged_beef)
.await;
assert!(
result.is_err(),
"Default facilitator should reject HTTP URLs"
);
}
#[tokio::test]
async fn test_lookup_success_output_list() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": [
{
"beef": [1, 2, 3, 4],
"outputIndex": 0
}
]
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({"key": "value"}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].beef, vec![1, 2, 3, 4]);
assert_eq!(outputs[0].output_index, 0);
} else {
panic!("Expected OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_success_freeform() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "freeform",
"result": {"status": "ok", "data": [1, 2, 3]}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::Freeform { result: r } = answer {
assert_eq!(r["status"], "ok");
} else {
panic!("Expected Freeform, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_success_formula() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "formula",
"formulas": [
{
"outpoint": "abc123.0",
"historyFn": "get_history"
}
]
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::Formula { formulas } = answer {
assert_eq!(formulas.len(), 1);
assert_eq!(formulas[0].outpoint, "abc123.0");
assert_eq!(formulas[0].history_fn, "get_history");
} else {
panic!("Expected Formula, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_empty_output_list() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert!(outputs.is_empty());
} else {
panic!("Expected empty OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_content_type_header() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.and(header("Content-Type", "application/json"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_lookup_x_aggregation_header() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.and(header("X-Aggregation", "yes"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_lookup_request_body_contains_service_and_query() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.and(body_string_contains("ls_myservice"))
.and(body_string_contains("test_key"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new(
"ls_myservice",
serde_json::json!({"test_key": "test_value"}),
);
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_lookup_error_400_bad_request() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(400).set_body_string("Bad Request: invalid query"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("400"),
"Expected error to contain status 400, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_lookup_error_404_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(404).set_body_string("Not Found"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("404"),
"Expected error to contain status 404, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_lookup_error_500_server_error() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("500"),
"Expected error to contain status 500, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_lookup_malformed_json_response() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("this is not valid JSON at all {{{")
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(
result.is_err(),
"Expected error on malformed JSON, got: {:?}",
result
);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("JSON") || err_msg.contains("parse"),
"Expected JSON parse error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_lookup_rejects_http_when_not_allowed() {
let facilitator = HttpsOverlayLookupFacilitator::new(false);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup("http://insecure.host/", &question, None)
.await;
assert!(result.is_err(), "Expected error, got: {:?}", result);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("https"),
"Expected HTTPS requirement error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_lookup_allows_http_when_configured() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(
result.is_ok(),
"Expected success with allow_http=true, got: {:?}",
result
);
}
#[tokio::test]
async fn test_lookup_url_trailing_slash_trimmed() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let url_with_slash = format!("{}/", mock_server.uri());
let result = facilitator.lookup(&url_with_slash, &question, None).await;
assert!(
result.is_ok(),
"Expected success with trailing slash URL, got: {:?}",
result
);
}
#[tokio::test]
async fn test_lookup_output_list_with_context() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": [
{
"beef": [1, 2, 3],
"outputIndex": 0,
"context": [0xAA, 0xBB]
},
{
"beef": [4, 5, 6],
"outputIndex": 1
}
]
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].beef, vec![1, 2, 3]);
assert_eq!(outputs[0].output_index, 0);
assert_eq!(outputs[0].context, Some(vec![0xAA, 0xBB]));
assert_eq!(outputs[1].beef, vec![4, 5, 6]);
assert_eq!(outputs[1].output_index, 1);
assert!(outputs[1].context.is_none());
} else {
panic!("Expected OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_output_list_beef_as_hex_string() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": [
{
"beef": "0102030405",
"outputIndex": 0
}
]
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].beef, vec![0x01, 0x02, 0x03, 0x04, 0x05]);
} else {
panic!("Expected OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_unknown_answer_type() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "unknown-type",
"data": {}
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(
result.is_err(),
"Expected error for unknown answer type, got: {:?}",
result
);
let err = result.unwrap_err();
let err_msg = err.to_string();
assert!(
err_msg.contains("unknown-type") || err_msg.contains("Unknown"),
"Expected unknown type error, got: {}",
err_msg
);
}
#[tokio::test]
async fn test_lookup_default_type_is_output_list() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"outputs": [
{
"beef": [1, 2],
"outputIndex": 0
}
]
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
assert!(
matches!(answer, LookupAnswer::OutputList { .. }),
"Expected OutputList as default, got: {:?}",
answer
);
}
#[tokio::test]
async fn test_lookup_binary_response() {
let mock_server = MockServer::start().await;
let mut binary_response = Vec::new();
binary_response.push(1u8);
binary_response.extend_from_slice(&[0u8; 32]);
binary_response.push(0u8);
binary_response.push(0u8);
binary_response.extend_from_slice(&[0xBE, 0xEF]);
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(binary_response)
.append_header("Content-Type", "application/octet-stream"),
)
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].output_index, 0);
assert!(outputs[0].context.is_none());
assert_eq!(outputs[0].beef, vec![0xBE, 0xEF]);
} else {
panic!(
"Expected OutputList from binary response, got: {:?}",
answer
);
}
}
#[tokio::test]
async fn test_lookup_binary_response_with_context() {
let mock_server = MockServer::start().await;
let mut binary_response = Vec::new();
binary_response.push(1u8);
binary_response.extend_from_slice(&[0xABu8; 32]);
binary_response.push(2u8);
binary_response.push(3u8);
binary_response.extend_from_slice(&[0x01, 0x02, 0x03]);
binary_response.extend_from_slice(&[0xCA, 0xFE]);
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(binary_response)
.append_header("Content-Type", "application/octet-stream"),
)
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].output_index, 2);
assert_eq!(outputs[0].context, Some(vec![0x01, 0x02, 0x03]));
assert_eq!(outputs[0].beef, vec![0xCA, 0xFE]);
} else {
panic!(
"Expected OutputList from binary response, got: {:?}",
answer
);
}
}
#[tokio::test]
async fn test_lookup_binary_response_multiple_outpoints() {
let mock_server = MockServer::start().await;
let mut binary_response = Vec::new();
binary_response.push(2u8);
binary_response.extend_from_slice(&[0x11u8; 32]);
binary_response.push(0u8); binary_response.push(0u8);
binary_response.extend_from_slice(&[0x22u8; 32]);
binary_response.push(1u8); binary_response.push(0u8);
binary_response.extend_from_slice(&[0xDE, 0xAD]);
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(binary_response)
.append_header("Content-Type", "application/octet-stream"),
)
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].output_index, 0);
assert_eq!(outputs[1].output_index, 1);
assert_eq!(outputs[0].beef, vec![0xDE, 0xAD]);
assert_eq!(outputs[1].beef, vec![0xDE, 0xAD]);
} else {
panic!(
"Expected OutputList from binary response, got: {:?}",
answer
);
}
}
#[tokio::test]
async fn test_lookup_slap_service_discovery() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.and(body_string_contains("ls_slap"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_slap", serde_json::json!({"service": "ls_myservice"}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_lookup_ship_host_discovery() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.and(body_string_contains("ls_ship"))
.and(body_string_contains("tm_mytopic"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_ship", serde_json::json!({"topics": ["tm_mytopic"]}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[tokio::test]
async fn test_lookup_facilitator_default_rejects_http() {
let facilitator = HttpsOverlayLookupFacilitator::default();
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup("http://insecure.host/", &question, None)
.await;
assert!(
result.is_err(),
"Default facilitator should reject HTTP URLs"
);
}
#[tokio::test]
async fn test_lookup_output_list_skips_malformed_items() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list",
"outputs": [
{
"beef": [1, 2],
"outputIndex": 0
},
{
"beef": [3, 4]
},
{
"outputIndex": 1
},
{
"beef": [5, 6],
"outputIndex": 2
}
]
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].output_index, 0);
assert_eq!(outputs[1].output_index, 2);
} else {
panic!("Expected OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_output_list_missing_outputs_field() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "output-list"
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert!(
outputs.is_empty(),
"Expected empty outputs when field missing"
);
} else {
panic!("Expected OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_freeform_null_result() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "freeform"
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::Freeform { result: r } = answer {
assert!(r.is_null(), "Expected null result, got: {:?}", r);
} else {
panic!("Expected Freeform, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_formula_empty_formulas() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"type": "formula",
"formulas": []
})))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::Formula { formulas } = answer {
assert!(formulas.is_empty());
} else {
panic!("Expected Formula, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_empty_binary_response() {
let mock_server = MockServer::start().await;
let binary_response = vec![0u8];
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(binary_response)
.append_header("Content-Type", "application/octet-stream"),
)
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let answer = result.unwrap();
if let LookupAnswer::OutputList { outputs } = answer {
assert!(outputs.is_empty(), "Expected empty outputs for 0 outpoints");
} else {
panic!("Expected OutputList, got: {:?}", answer);
}
}
#[tokio::test]
async fn test_lookup_empty_json_response_body() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/lookup"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("")
.append_header("Content-Type", "application/json"),
)
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayLookupFacilitator::new(true);
let question = LookupQuestion::new("ls_test", serde_json::json!({}));
let result = facilitator
.lookup(&mock_server.uri(), &question, None)
.await;
assert!(
result.is_err(),
"Expected error on empty response body, got: {:?}",
result
);
}
#[tokio::test]
async fn test_broadcast_steak_serialization_roundtrip() {
let mock_server = MockServer::start().await;
let steak_json = serde_json::json!({
"tm_topic1": {
"outputsToAdmit": [0, 1, 2],
"coinsToRetain": [3, 4],
"coinsRemoved": [5]
},
"tm_topic2": {
"outputsToAdmit": [],
"coinsToRetain": [],
"coinsRemoved": []
},
"tm_topic3": {
"outputsToAdmit": [0],
"coinsToRetain": []
}
});
Mock::given(method("POST"))
.and(path("/submit"))
.respond_with(ResponseTemplate::new(200).set_body_json(steak_json))
.expect(1)
.mount(&mock_server)
.await;
let facilitator = HttpsOverlayBroadcastFacilitator::new(true);
let tagged_beef = TaggedBEEF::new(
vec![0x01],
vec![
"tm_topic1".to_string(),
"tm_topic2".to_string(),
"tm_topic3".to_string(),
],
);
let result = facilitator.send(&mock_server.uri(), &tagged_beef).await;
assert!(result.is_ok(), "Expected success, got: {:?}", result);
let steak = result.unwrap();
assert_eq!(steak.len(), 3);
let t1 = steak.get("tm_topic1").unwrap();
assert_eq!(t1.outputs_to_admit, vec![0, 1, 2]);
assert_eq!(t1.coins_to_retain, vec![3, 4]);
assert_eq!(t1.coins_removed, Some(vec![5]));
let t2 = steak.get("tm_topic2").unwrap();
assert!(t2.outputs_to_admit.is_empty());
let t3 = steak.get("tm_topic3").unwrap();
assert_eq!(t3.outputs_to_admit, vec![0]);
assert!(t3.coins_removed.is_none());
}