v0.2.11-pre.008

This commit is contained in:
2026-08-26 10:23:42 +02:00
parent 5aeff5ca14
commit 87d7314bf4
13 changed files with 972 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_provider.rs
// version: 4
// version: 5
#[test]
fn provider_id_is_opaque_bounded_and_stable() -> ksp_core_lib::Result<()> {
@@ -130,3 +130,16 @@ fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::R
assert_ne!(state.availability(), crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
return std::result::Result::Ok(());
}
#[test]
fn provider_availability_eligibility_at_respects_retry_deadlines_without_consumer_scheduling() {
let now = crate::MarketPriceTimestamp::from_unix_millis(100);
let future = crate::MarketPriceTimestamp::from_unix_millis(200);
let past = crate::MarketPriceTimestamp::from_unix_millis(50);
assert!(crate::MarketPriceProviderAvailability::Ready.is_refresh_eligible_at(now));
assert!(!crate::MarketPriceProviderAvailability::Disabled.is_refresh_eligible_at(now));
assert!(!crate::MarketPriceProviderAvailability::CoolingDown { retry_at: future }.is_refresh_eligible_at(now));
assert!(crate::MarketPriceProviderAvailability::CoolingDown { retry_at: past }.is_refresh_eligible_at(now));
assert!(crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None }.is_refresh_eligible_at(now));
assert!(crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(past) }.is_refresh_eligible_at(now));
}

View File

@@ -0,0 +1,148 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs
// version: 1
#[test]
fn service_registry_orders_all_eight_v1_setups_and_keeps_runtime_projection_generic() -> ksp_core_lib::Result<()> {
let setups = match all_disabled_setups() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let service = match crate::MarketPriceService::new(setups) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = service.registry();
assert_eq!(registry.len(), 8);
let ids = registry.entries().iter().map(|entry| return entry.descriptor().id().as_str()).collect::<std::vec::Vec<_>>();
assert_eq!(ids, std::vec!["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken",]);
assert!(registry.entries().iter().all(|entry| return entry.state().availability() == crate::MarketPriceProviderAvailability::Disabled));
return std::result::Result::Ok(());
}
#[tokio::test]
async fn refresh_all_projects_disabled_providers_without_network_dispatch() -> ksp_core_lib::Result<()> {
let setups = match all_disabled_setups() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let service = match crate::MarketPriceService::new(setups) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let outcomes = match service.refresh_all().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(outcomes.len(), 8);
assert!(outcomes.iter().all(|outcome| return !outcome.refreshed()));
assert!(outcomes.iter().all(|outcome| return outcome.state().availability() == crate::MarketPriceProviderAvailability::Disabled));
let ids = outcomes.iter().map(|outcome| return outcome.provider_id().as_str()).collect::<std::vec::Vec<_>>();
assert_eq!(ids, std::vec!["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken",]);
return std::result::Result::Ok(());
}
#[tokio::test]
async fn refresh_many_rejects_duplicates_and_unknown_provider_before_dispatch() -> ksp_core_lib::Result<()> {
let settings = match crate::MarketPriceCoinPaprikaSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let service = match crate::MarketPriceService::new(std::vec![crate::MarketPriceProviderSetup::CoinPaprika(settings)]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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 duplicate = service.refresh_many(&[provider_id.clone(), provider_id]).await;
let duplicate_error = match duplicate {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("duplicate")),
};
assert_eq!(duplicate_error.code(), crate::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID);
let unknown_id = match crate::MarketPriceProviderId::new("unknown") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unknown = service.refresh(&unknown_id).await;
let unknown_error = match unknown {
std::result::Result::Err(error) => error,
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("unknown")),
};
assert_eq!(unknown_error.code(), crate::ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND);
return std::result::Result::Ok(());
}
#[test]
fn refresh_error_classification_is_provider_neutral_and_preserves_known_retry_time() {
let auth = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_ACCESS_DENIED, "test");
assert_eq!(
super::availability_from_error(&auth, crate::MarketPriceProviderAuthMode::RequiredApiKey),
crate::MarketPriceProviderAvailability::AuthenticationUnavailable
);
assert!(matches!(
super::availability_from_error(&auth, crate::MarketPriceProviderAuthMode::None),
crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None }
));
let rate = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RATE_LIMITED, "test").with_context("retry_after_seconds", "2");
assert!(matches!(
super::availability_from_error(&rate, crate::MarketPriceProviderAuthMode::None),
crate::MarketPriceProviderAvailability::CoolingDown { .. }
));
let temporary = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_TEMPORARY_FAILURE, "test").with_context("retry_after_seconds", "3");
let availability = super::availability_from_error(&temporary, crate::MarketPriceProviderAuthMode::None);
assert!(matches!(availability, crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(_) }));
let local = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_INVALID, "test");
assert_eq!(super::availability_from_error(&local, crate::MarketPriceProviderAuthMode::None), crate::MarketPriceProviderAvailability::Misconfigured);
}
fn all_disabled_setups() -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderSetup>> {
let birdeye = match crate::MarketPriceBirdeyeSettings::new(false, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinbase = match crate::MarketPriceCoinbaseExchangeSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coingecko = match crate::MarketPriceCoinGeckoSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinmarketcap = match crate::MarketPriceCoinMarketCapSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinpaprika = match crate::MarketPriceCoinPaprikaSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dexscreener = match crate::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let jupiter = match crate::MarketPriceJupiterSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kraken = match crate::MarketPriceKrakenSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![
crate::MarketPriceProviderSetup::Birdeye(birdeye),
crate::MarketPriceProviderSetup::CoinbaseExchange(coinbase),
crate::MarketPriceProviderSetup::CoinGecko(coingecko),
crate::MarketPriceProviderSetup::CoinMarketCap(coinmarketcap),
crate::MarketPriceProviderSetup::CoinPaprika(coinpaprika),
crate::MarketPriceProviderSetup::DexScreener(dexscreener),
crate::MarketPriceProviderSetup::Jupiter(jupiter),
crate::MarketPriceProviderSetup::Kraken(kraken),
]);
}
fn test_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID, "Market-price service test expectation failed")
.with_context("field", field);
}