v0.2.11-pre.002
This commit is contained in:
59
crates/ksp-offchain-transport-lib/unit_tests/decimal.rs
Normal file
59
crates/ksp-offchain-transport-lib/unit_tests/decimal.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/decimal.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn decimal_normalizes_fractional_and_scientific_forms_without_f64() -> ksp_core_lib::Result<()> {
|
||||
let cases = [
|
||||
("123.4500", "123.45", 12_345_u128, 2_u8),
|
||||
("1.2345e2", "123.45", 12_345, 2),
|
||||
("12345e-2", "123.45", 12_345, 2),
|
||||
("1e3", "1000", 1_000, 0),
|
||||
("1e-3", "0.001", 1, 3),
|
||||
];
|
||||
for (source, expected, coefficient, scale) in cases {
|
||||
let value = match crate::PriceDecimal::parse(source) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(value.to_canonical_string(), expected);
|
||||
assert_eq!(value.coefficient(), coefficient);
|
||||
assert_eq!(value.scale(), scale);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_rejects_zero_negative_nonfinite_excessive_scale_and_overflow() {
|
||||
let invalid = ["0", "0.000", "-1", "+1", "NaN", "inf", "1e-19", "1e129", "340282366920938463463374607431768211456", " 1", "1 ", ".1", "1."];
|
||||
for source in invalid {
|
||||
let result = crate::PriceDecimal::parse(source);
|
||||
assert!(result.is_err());
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_PRICE_DECIMAL_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_serde_is_canonical_string_and_round_trips_exactly() -> ksp_core_lib::Result<()> {
|
||||
let value = match crate::PriceDecimal::parse("123.4500") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let encoded = match serde_json::to_string(&value) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PRICE_DECIMAL_INVALID, "test serialization failed"));
|
||||
},
|
||||
};
|
||||
assert_eq!(encoded, "\"123.45\"");
|
||||
let decoded: crate::PriceDecimal = match serde_json::from_str(encoded.as_str()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PRICE_DECIMAL_INVALID, "test deserialization failed"));
|
||||
},
|
||||
};
|
||||
assert_eq!(decoded, value);
|
||||
assert!(serde_json::from_str::<crate::PriceDecimal>("123.45").is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
69
crates/ksp-offchain-transport-lib/unit_tests/observation.rs
Normal file
69
crates/ksp-offchain-transport-lib/unit_tests/observation.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/observation.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn observation_preserves_pair_exact_price_semantics_timestamps_and_safe_provenance() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::PriceProviderId::new("kraken") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let price = match crate::PriceDecimal::parse("204.125") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provenance = match crate::PriceProvenance::new("market=SOL/USD") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let started = crate::PriceTimestamp::from_unix_millis(10_000);
|
||||
let received = crate::PriceTimestamp::from_unix_millis(10_250);
|
||||
let observation = match crate::SolUsdPriceObservation::new(
|
||||
provider_id,
|
||||
price,
|
||||
crate::PriceSemantics::ExchangeLastTrade,
|
||||
started,
|
||||
received,
|
||||
std::option::Option::None,
|
||||
provenance,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(observation.pair(), crate::PricePair::SolUsd);
|
||||
assert_eq!(observation.pair().code(), "SOL/USD");
|
||||
assert_eq!(observation.price(), price);
|
||||
assert_eq!(observation.semantics(), crate::PriceSemantics::ExchangeLastTrade);
|
||||
assert_eq!(observation.request_started_at(), started);
|
||||
assert_eq!(observation.received_at(), received);
|
||||
assert_eq!(observation.provider_timestamp(), std::option::Option::None);
|
||||
assert_eq!(observation.provenance().as_str(), "market=SOL/USD");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observation_rejects_reversed_ksp_timestamps_and_unsafe_provenance() -> ksp_core_lib::Result<()> {
|
||||
assert!(crate::PriceProvenance::new("line\nbreak").is_err());
|
||||
let provider_id = match crate::PriceProviderId::new("coingecko") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let price = match crate::PriceDecimal::parse("200") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provenance = match crate::PriceProvenance::new("asset=solana") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = crate::SolUsdPriceObservation::new(
|
||||
provider_id,
|
||||
price,
|
||||
crate::PriceSemantics::AggregatedMarket,
|
||||
crate::PriceTimestamp::from_unix_millis(2),
|
||||
crate::PriceTimestamp::from_unix_millis(1),
|
||||
std::option::Option::None,
|
||||
provenance,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
77
crates/ksp-offchain-transport-lib/unit_tests/provider.rs
Normal file
77
crates/ksp-offchain-transport-lib/unit_tests/provider.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/provider.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn provider_id_is_opaque_bounded_and_stable() -> ksp_core_lib::Result<()> {
|
||||
let id = match crate::PriceProviderId::new("coingecko-main") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(id.as_str(), "coingecko-main");
|
||||
assert_eq!(id.to_string(), "coingecko-main");
|
||||
for invalid in ["", "CoinGecko", "coin gecko", "coin/gecko", "é"] {
|
||||
assert!(crate::PriceProviderId::new(invalid).is_err());
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_descriptor_preserves_semantics_auth_limits_and_informational_quota() -> ksp_core_lib::Result<()> {
|
||||
let id = match crate::PriceProviderId::new("provider-a") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rate_limit = match crate::PriceProviderRateLimit::fixed(1, 2, std::option::Option::None, crate::PriceProviderRateLimitScope::Ip) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let quota = match crate::PriceProviderLongTermQuota::new(10_000, crate::PriceProviderQuotaPeriod::Month, crate::PriceProviderQuotaUnit::Credits) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let descriptor = match crate::PriceProviderDescriptor::new(
|
||||
id,
|
||||
"Provider A",
|
||||
crate::PriceSemantics::AggregatedMarket,
|
||||
crate::PriceProviderAuthMode::OptionalApiKey,
|
||||
rate_limit,
|
||||
std::option::Option::Some(quota),
|
||||
true,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(descriptor.display_name(), "Provider A");
|
||||
assert_eq!(descriptor.semantics(), crate::PriceSemantics::AggregatedMarket);
|
||||
assert_eq!(descriptor.auth_mode(), crate::PriceProviderAuthMode::OptionalApiKey);
|
||||
assert_eq!(descriptor.rate_limit(), rate_limit);
|
||||
assert_eq!(descriptor.long_term_quota(), std::option::Option::Some(quota));
|
||||
assert!(descriptor.supports_sol_usd());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_limit_descriptors_reject_zero_and_model_dynamic_scope() {
|
||||
assert!(crate::PriceProviderRateLimit::fixed(0, 1, std::option::Option::None, crate::PriceProviderRateLimitScope::Ip).is_err());
|
||||
assert!(crate::PriceProviderRateLimit::fixed(1, 0, std::option::Option::None, crate::PriceProviderRateLimitScope::Ip).is_err());
|
||||
assert!(crate::PriceProviderRateLimit::fixed(1, 1, std::option::Option::Some(0), crate::PriceProviderRateLimitScope::Ip).is_err());
|
||||
let dynamic = crate::PriceProviderRateLimit::dynamic(crate::PriceProviderRateLimitScope::Ip);
|
||||
assert_eq!(dynamic.kind(), crate::PriceProviderRateLimitKind::Dynamic);
|
||||
assert_eq!(dynamic.scope(), crate::PriceProviderRateLimitScope::Ip);
|
||||
assert_eq!(dynamic.requests(), std::option::Option::None);
|
||||
assert_eq!(dynamic.window_seconds(), std::option::Option::None);
|
||||
assert_eq!(dynamic.burst(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::Result<()> {
|
||||
let id = match crate::PriceProviderId::new("jupiter") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let retry_at = crate::PriceTimestamp::from_unix_millis(1_777_777_777_000);
|
||||
let state = crate::PriceProviderState::new(id, crate::PriceProviderAvailability::CoolingDown { retry_at });
|
||||
assert_eq!(state.availability(), crate::PriceProviderAvailability::CoolingDown { retry_at });
|
||||
assert_ne!(state.availability(), crate::PriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
23
crates/ksp-offchain-transport-lib/unit_tests/settings.rs
Normal file
23
crates/ksp-offchain-transport-lib/unit_tests/settings.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/settings.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn common_settings_contain_only_generic_identity_and_enablement() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::PriceProviderId::new("coinpaprika") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let settings = crate::PriceProviderCommonSettings::new(provider_id, true);
|
||||
assert!(settings.enabled());
|
||||
assert_eq!(settings.provider_id().as_str(), "coinpaprika");
|
||||
let serialized = match serde_json::to_value(&settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PROVIDER_SETTINGS_INVALID, "test serialization failed"));
|
||||
},
|
||||
};
|
||||
assert!(serialized.get("api_key").is_none());
|
||||
assert!(serialized.get("endpoint").is_none());
|
||||
assert!(serialized.get("rate_limit").is_none());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
Reference in New Issue
Block a user