diff --git a/src/bitcoin_price.rs b/src/bitcoin_price.rs index d8236e2d..200e25ce 100644 --- a/src/bitcoin_price.rs +++ b/src/bitcoin_price.rs @@ -21,16 +21,17 @@ impl BitcoinPriceManager { crate::price::get_bitcoin_price(currency) } - /// Test-only: seed the in-memory price cache so unit tests in other - /// modules can exercise price-dependent paths (e.g. range-order bond - /// sizing) deterministically without hitting the network. Use a unique - /// `currency` per test to avoid cross-test interference on the shared - /// static. + /// Test-only: seed the price-override map consulted by + /// [`crate::price::get_bitcoin_price`] so unit tests in other modules + /// can exercise price-dependent paths (e.g. range-order bond sizing) + /// deterministically without hitting the network or installing the + /// global `PriceManager`. Use a unique `currency` per test to avoid + /// cross-test interference on the shared map. #[cfg(test)] pub(crate) fn set_price_for_test(currency: &str, price: f64) { - BITCOIN_PRICES + crate::price::test_price_overrides() .write() - .expect("price cache write lock") + .expect("price override write lock") .insert(currency.to_string(), price); } } diff --git a/src/price/mod.rs b/src/price/mod.rs index 3cde5fa4..4c6bf8eb 100644 --- a/src/price/mod.rs +++ b/src/price/mod.rs @@ -27,6 +27,22 @@ pub use store::{AggregatedPrice, PriceError, PriceStore}; use mostro_core::error::{MostroError, ServiceError}; +/// Test-only per-currency price overrides consulted by +/// [`get_bitcoin_price`] before the global manager. Unit tests in other +/// modules (e.g. range-order bond sizing in `util.rs`) seed this via +/// `BitcoinPriceManager::set_price_for_test` instead of installing the +/// global [`PriceManager`] — its `OnceLock` would leak one test's +/// configuration into every other test in the binary. Tests use a unique +/// currency code each to avoid cross-test interference on the shared map. +#[cfg(test)] +pub(crate) fn test_price_overrides( +) -> &'static std::sync::RwLock> { + static OVERRIDES: std::sync::OnceLock< + std::sync::RwLock>, + > = std::sync::OnceLock::new(); + OVERRIDES.get_or_init(|| std::sync::RwLock::new(std::collections::HashMap::new())) +} + /// Read a currency's per-BTC price from the global [`PriceManager`]. /// /// This is the Phase 1 entry point for consumers (`util::get_bitcoin_price` @@ -36,6 +52,14 @@ use mostro_core::error::{MostroError, ServiceError}; /// legacy code returned when `BITCOIN_PRICES` was empty, so callers behave /// identically. pub fn get_bitcoin_price(currency: &str) -> Result { + #[cfg(test)] + if let Some(price) = test_price_overrides() + .read() + .expect("price override read lock") + .get(currency) + { + return Ok(*price); + } match PriceManager::global() { Some(m) => m.get_price(currency), None => Err(MostroError::MostroInternalErr(ServiceError::NoAPIResponse)), diff --git a/src/util.rs b/src/util.rs index e7a43c9b..36d1ec89 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1598,6 +1598,7 @@ pub async fn notify_taker_reputation( #[cfg(test)] mod tests { use super::*; + use crate::bitcoin_price::BitcoinPriceManager; use mostro_core::message::{Message, MessageKind}; use mostro_core::order::Order; use sqlx::sqlite::SqlitePoolOptions;