v0.2.11-pre.004

This commit is contained in:
2026-08-25 21:47:47 +02:00
parent f4413ebbb0
commit 60afb51451
23 changed files with 1862 additions and 117 deletions

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -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(());
}

View File

@@ -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(());
}

View File

@@ -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(());
}