v0.2.11-pre.004
This commit is contained in:
@@ -55,7 +55,6 @@ fn provider_retry_after_extends_but_cannot_pathologically_lock_cooldown() -> ksp
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(controller.policy(), crate::HttpAdmissionPolicy::Dynamic);
|
||||
let applied = controller.record_rate_limited(std::option::Option::Some(std::time::Duration::from_secs(99_999)));
|
||||
assert_eq!(applied, crate::HTTP_MAX_RETRY_AFTER);
|
||||
let remaining = controller.cooldown_remaining();
|
||||
@@ -69,5 +68,4 @@ fn admission_policy_rejects_zero_values_and_invalid_fallback() {
|
||||
assert!(crate::HttpAdmissionPolicy::fixed(0, std::time::Duration::from_secs(1), std::option::Option::None).is_err());
|
||||
assert!(crate::HttpAdmissionPolicy::fixed(1, std::time::Duration::ZERO, std::option::Option::None).is_err());
|
||||
assert!(crate::HttpAdmissionPolicy::fixed(1, std::time::Duration::from_secs(1), std::option::Option::Some(0)).is_err());
|
||||
assert!(crate::HttpAdmissionController::new(crate::HttpAdmissionPolicy::Unlimited, std::option::Option::Some(std::time::Duration::ZERO)).is_err());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_coingecko.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn coingecko_modes_map_exact_free_capabilities_and_redact_demo_key() -> ksp_core_lib::Result<()> {
|
||||
let keyless = match crate::MarketPriceCoinGeckoSettings::keyless(true) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let keyless_provider = match crate::MarketPriceCoinGeckoProvider::new(keyless) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(keyless_provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::None);
|
||||
assert_eq!(keyless_provider.descriptor().rate_limit().kind(), crate::MarketPriceProviderRateLimitKind::Dynamic);
|
||||
assert_eq!(keyless_provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Ip);
|
||||
assert_eq!(keyless_provider.descriptor().long_term_quota(), std::option::Option::None);
|
||||
let demo = match crate::MarketPriceCoinGeckoSettings::demo(true, std::option::Option::Some("demo-secret-canary".to_owned())) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(!format!("{demo:?}").contains("demo-secret-canary"));
|
||||
let request = match super::build_request(&demo) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(request.url_for_test().host_str(), std::option::Option::Some("api.coingecko.com"));
|
||||
assert!(request.url_for_test().as_str().contains("ids=solana"));
|
||||
assert!(request.url_for_test().as_str().contains("vs_currencies=usd"));
|
||||
assert!(request.has_header_for_test(super::COINGECKO_DEMO_API_KEY_HEADER));
|
||||
let demo_provider = match crate::MarketPriceCoinGeckoProvider::new(demo) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(demo_provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::RequiredApiKey);
|
||||
assert_eq!(demo_provider.descriptor().rate_limit().requests(), std::option::Option::Some(100));
|
||||
assert_eq!(demo_provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(60));
|
||||
let quota = match demo_provider.descriptor().long_term_quota() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response("coingecko", "quota")),
|
||||
};
|
||||
assert_eq!(quota.amount(), 10_000);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coingecko_fixture_maps_exact_price_and_real_provider_timestamp() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new("coingecko") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let start = crate::MarketPriceTimestamp::from_unix_millis(1_800_000_000_000);
|
||||
let received = crate::MarketPriceTimestamp::from_unix_millis(1_800_000_000_100);
|
||||
let observation = match super::parse_response(br#"{"solana":{"usd":151.123456789012345678,"last_updated_at":1800000000}}"#, provider_id, start, received) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(observation.price().to_canonical_string(), "151.123456789012345678");
|
||||
assert_eq!(observation.provider_timestamp(), std::option::Option::Some(start));
|
||||
assert_eq!(observation.provenance().as_str(), "coingecko:solana:usd");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coingecko_keyless_rejects_credential_and_enabled_demo_requires_one() {
|
||||
let keyless =
|
||||
crate::MarketPriceCoinGeckoSettings::new(true, crate::MarketPriceCoinGeckoAccessMode::Keyless, std::option::Option::Some("unexpected".to_owned()));
|
||||
assert!(keyless.is_err());
|
||||
assert!(crate::MarketPriceCoinGeckoSettings::demo(true, std::option::Option::None).is_err());
|
||||
assert!(crate::MarketPriceCoinGeckoSettings::demo(false, std::option::Option::None).is_ok());
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_coinmarketcap.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn coinmarketcap_modes_use_v2_and_map_exact_free_capabilities() -> ksp_core_lib::Result<()> {
|
||||
let keyless = match crate::MarketPriceCoinMarketCapSettings::keyless(true) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let request = match super::build_request(&keyless) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(request.url_for_test().path(), "/public-api/v2/simple/price");
|
||||
assert!(!request.has_header_for_test(super::COINMARKETCAP_API_KEY_HEADER));
|
||||
let keyless_provider = match crate::MarketPriceCoinMarketCapProvider::new(keyless) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(keyless_provider.descriptor().rate_limit().kind(), crate::MarketPriceProviderRateLimitKind::Dynamic);
|
||||
let basic = match crate::MarketPriceCoinMarketCapSettings::basic(true, std::option::Option::Some("cmc-secret-canary".to_owned())) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(!format!("{basic:?}").contains("cmc-secret-canary"));
|
||||
let request = match super::build_request(&basic) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(request.url_for_test().path(), "/v2/simple/price");
|
||||
assert!(request.has_header_for_test(super::COINMARKETCAP_API_KEY_HEADER));
|
||||
let basic_provider = match crate::MarketPriceCoinMarketCapProvider::new(basic) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(basic_provider.descriptor().rate_limit().requests(), std::option::Option::Some(50));
|
||||
let quota = match basic_provider.descriptor().long_term_quota() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response("coinmarketcap", "quota")),
|
||||
};
|
||||
assert_eq!(quota.amount(), 15_000);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coinmarketcap_v2_fixture_normalizes_string_status_and_exact_price() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new("coinmarketcap") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let start = crate::MarketPriceTimestamp::from_unix_millis(1_775_000_000_000);
|
||||
let received = crate::MarketPriceTimestamp::from_unix_millis(1_775_000_000_100);
|
||||
let observation = match super::parse_response(
|
||||
concat!(
|
||||
r#"{"data":[{"id":5426,"symbol":"SOL","quotes":[{"symbol":"USD","price":151.987654321012345678,"#,
|
||||
r#"last_updated":"2026-04-01T00:00:00.000Z"}]}],"status":{"error_code":"0"}}"#,
|
||||
)
|
||||
.as_bytes(),
|
||||
provider_id,
|
||||
start,
|
||||
received,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(observation.price().to_canonical_string(), "151.987654321012345678");
|
||||
assert_eq!(observation.provenance().as_str(), "coinmarketcap:5426:usd:v2");
|
||||
assert!(observation.provider_timestamp().is_some());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coinmarketcap_rejects_nonzero_status_and_wrong_identity() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new("coinmarketcap") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(1);
|
||||
let nonzero = super::parse_response(br#"{"data":[],"status":{"error_code":1001}}"#, provider_id.clone(), timestamp, timestamp);
|
||||
assert!(nonzero.is_err());
|
||||
let wrong = super::parse_response(
|
||||
br#"{"data":[{"id":1,"symbol":"BTC","quotes":[{"symbol":"USD","price":1,"last_updated":"2026-04-01T00:00:00Z"}]}],"status":{"error_code":0}}"#,
|
||||
provider_id,
|
||||
timestamp,
|
||||
timestamp,
|
||||
);
|
||||
assert!(wrong.is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_coinpaprika.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn coinpaprika_free_descriptor_maps_ip_rate_and_monthly_request_quota() -> ksp_core_lib::Result<()> {
|
||||
let settings = match crate::MarketPriceCoinPaprikaSettings::new(true) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let request = match super::build_request() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(request.url_for_test().host_str(), std::option::Option::Some("api.coinpaprika.com"));
|
||||
assert_eq!(request.url_for_test().path(), "/v1/tickers/sol-solana");
|
||||
assert!(request.url_for_test().as_str().contains("quotes=USD"));
|
||||
let provider = match crate::MarketPriceCoinPaprikaProvider::new(settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::None);
|
||||
assert_eq!(provider.descriptor().rate_limit().requests(), std::option::Option::Some(10));
|
||||
assert_eq!(provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(1));
|
||||
assert_eq!(provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Ip);
|
||||
let quota = match provider.descriptor().long_term_quota() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response("coinpaprika", "quota")),
|
||||
};
|
||||
assert_eq!(quota.amount(), 20_000);
|
||||
assert_eq!(quota.unit(), crate::MarketPriceProviderQuotaUnit::Requests);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coinpaprika_fixture_validates_sol_identity_and_preserves_timestamp() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new("coinpaprika") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(1_775_000_000_000);
|
||||
let observation = match super::parse_response(
|
||||
br#"{"id":"sol-solana","symbol":"SOL","last_updated":"2026-04-01T00:00:00Z","quotes":{"USD":{"price":151.010203040506070809}}}"#,
|
||||
provider_id.clone(),
|
||||
timestamp,
|
||||
timestamp,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(observation.price().to_canonical_string(), "151.010203040506070809");
|
||||
assert_eq!(observation.provenance().as_str(), "coinpaprika:sol-solana:usd");
|
||||
let wrong = super::parse_response(
|
||||
br#"{"id":"btc-bitcoin","symbol":"BTC","last_updated":"2026-04-01T00:00:00Z","quotes":{"USD":{"price":1}}}"#,
|
||||
provider_id,
|
||||
timestamp,
|
||||
timestamp,
|
||||
);
|
||||
assert!(wrong.is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_decimal.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn decimal_normalizes_fractional_and_scientific_forms_without_f64() -> ksp_core_lib::Result<()> {
|
||||
@@ -57,3 +57,30 @@ fn decimal_serde_is_canonical_string_and_round_trips_exactly() -> ksp_core_lib::
|
||||
assert!(serde_json::from_str::<crate::MarketPriceDecimal>("123.45").is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_parses_raw_json_number_and_string_without_f64_round_trip() -> ksp_core_lib::Result<()> {
|
||||
let number = match serde_json::from_str::<std::boxed::Box<serde_json::value::RawValue>>("151.123456789012345678") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID, "test raw number failed"));
|
||||
},
|
||||
};
|
||||
let parsed = match crate::MarketPriceDecimal::parse_json_raw(number.as_ref()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(parsed.to_canonical_string(), "151.123456789012345678");
|
||||
let string = match serde_json::from_str::<std::boxed::Box<serde_json::value::RawValue>>(r#""151.2300""#) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID, "test raw string failed"));
|
||||
},
|
||||
};
|
||||
let parsed = match crate::MarketPriceDecimal::parse_json_raw(string.as_ref()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(parsed.to_canonical_string(), "151.23");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user