#![cfg(all(feature = "transaction", feature = "http"))]
use bsv_rs::transaction::{
FeeModel, LivePolicy, LivePolicyConfig, Transaction, DEFAULT_CACHE_TTL_SECS,
DEFAULT_FALLBACK_RATE, DEFAULT_POLICY_URL,
};
use std::time::Duration;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_live_policy_refresh_success() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 1,
"bytes": 1000
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 1);
assert_eq!(fee_model.cached_rate(), Some(1));
assert_eq!(fee_model.effective_rate(), 1);
}
#[tokio::test]
async fn test_live_policy_refresh_higher_fee_rate() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 50,
"bytes": 1000
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 50);
}
#[tokio::test]
async fn test_live_policy_refresh_nested_policy() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"policy": {
"miningFee": {
"satoshis": 5,
"bytes": 1000
}
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 5);
}
#[tokio::test]
async fn test_live_policy_refresh_with_api_key() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.and(header("Authorization", "Bearer test-api-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 1,
"bytes": 1000
}
})))
.expect(1)
.mount(&mock_server)
.await;
let config = LivePolicyConfig {
policy_url: format!("{}/v1/policy", mock_server.uri()),
api_key: Some("test-api-key".to_string()),
cache_ttl: Duration::from_secs(DEFAULT_CACHE_TTL_SECS),
fallback_rate: DEFAULT_FALLBACK_RATE,
timeout_ms: 10_000,
};
let fee_model = LivePolicy::with_config(config);
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 1);
}
#[tokio::test]
async fn test_live_policy_refresh_server_error() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let result = fee_model.refresh().await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("HTTP 500"));
}
#[tokio::test]
async fn test_live_policy_refresh_not_found() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(404))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let result = fee_model.refresh().await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("HTTP 404"));
}
#[tokio::test]
async fn test_live_policy_refresh_malformed_json() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_string("not json at all"))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let result = fee_model.refresh().await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("parse policy"));
}
#[tokio::test]
async fn test_live_policy_refresh_missing_mining_fee() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"someOtherField": true
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let result = fee_model.refresh().await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("No mining fee"));
}
#[tokio::test]
async fn test_live_policy_fallback_when_not_refreshed() {
let fee_model = LivePolicy::new();
assert_eq!(fee_model.effective_rate(), DEFAULT_FALLBACK_RATE);
assert!(fee_model.cached_rate().is_none());
}
#[tokio::test]
async fn test_live_policy_compute_fee_after_refresh() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 1,
"bytes": 1
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
fee_model.refresh().await.unwrap();
assert_eq!(fee_model.effective_rate(), 1000);
let tx = Transaction::new();
let fee = fee_model.compute_fee(&tx).unwrap();
assert_eq!(fee, 10);
}
#[tokio::test]
async fn test_live_policy_compute_fee_with_fallback() {
let fee_model = LivePolicy::new();
let tx = Transaction::new();
let fee = fee_model.compute_fee(&tx).unwrap();
assert_eq!(fee, 1);
}
#[tokio::test]
async fn test_live_policy_set_rate_overrides_fallback() {
let fee_model = LivePolicy::new();
fee_model.set_rate(500);
assert_eq!(fee_model.cached_rate(), Some(500));
assert_eq!(fee_model.effective_rate(), 500);
let tx = Transaction::new();
let fee = fee_model.compute_fee(&tx).unwrap();
assert_eq!(fee, 5);
}
#[tokio::test]
async fn test_live_policy_cache_expiry() {
let config = LivePolicyConfig {
policy_url: "https://not-used.example.com/v1/policy".to_string(),
api_key: None,
cache_ttl: Duration::from_millis(50), fallback_rate: 100,
timeout_ms: 10_000,
};
let fee_model = LivePolicy::with_config(config);
fee_model.set_rate(500);
assert_eq!(fee_model.cached_rate(), Some(500));
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(fee_model.cached_rate().is_none());
assert_eq!(fee_model.effective_rate(), 100);
}
#[tokio::test]
async fn test_live_policy_refresh_updates_cache() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 1,
"bytes": 1000
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate1 = fee_model.refresh().await.unwrap();
assert_eq!(rate1, 1);
assert_eq!(fee_model.effective_rate(), 1);
}
#[tokio::test]
async fn test_live_policy_custom_fallback_rate() {
let config = LivePolicyConfig {
policy_url: "https://not-used.example.com/v1/policy".to_string(),
api_key: None,
cache_ttl: Duration::from_secs(300),
fallback_rate: 250,
timeout_ms: 10_000,
};
let fee_model = LivePolicy::with_config(config);
assert_eq!(fee_model.effective_rate(), 250);
}
#[tokio::test]
async fn test_live_policy_default_policy_url() {
let fee_model = LivePolicy::new();
assert_eq!(fee_model.policy_url(), DEFAULT_POLICY_URL);
}
#[tokio::test]
async fn test_live_policy_default_cache_ttl() {
let fee_model = LivePolicy::new();
assert_eq!(
fee_model.cache_ttl(),
Duration::from_secs(DEFAULT_CACHE_TTL_SECS)
);
}
#[tokio::test]
async fn test_live_policy_refresh_converts_sat_per_byte_to_per_kb() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 1,
"bytes": 2
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 500);
}
#[tokio::test]
async fn test_live_policy_refresh_defaults_missing_satoshis() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"bytes": 1000
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 1);
}
#[tokio::test]
async fn test_live_policy_refresh_defaults_missing_bytes() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/policy"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"miningFee": {
"satoshis": 5
}
})))
.expect(1)
.mount(&mock_server)
.await;
let fee_model = LivePolicy::with_url(&format!("{}/v1/policy", mock_server.uri()));
let rate = fee_model.refresh().await.unwrap();
assert_eq!(rate, 5000);
}