mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
fix(prices,config): harden per-feed control + fail-closed gaps (adversarial review)
From a multi-lens adversarial review of the capabilities + prices work: - PRICES_ASSETS="" now means NONE, not all (real bug): from_env used env_opt, which collapses unset and present-but-empty into None, so the documented "empty = none" silently served all five feeds. Read via env::var so unset => all, present (even empty) => the listed subset or none. - PRICES_CURRENCY is now validated against a curated SUPPORTED_PRICE_CURRENCIES allowlist. Previously a typo (e.g. "usdd") booted, advertised prices:true, and served an empty feed forever (the provider returns empty quotes for an unknown currency) — a fail-open path inconsistent with the adjacent provider/asset checks. - PRICES_PROVIDER is lowercased at read, matching the asset/currency normalization (mixed-case no longer trips the allowlist). - validate() now requires an Electrum URL for an enabled BTC/LTC chain (hard error in prod, warn in dev), symmetric with the xmr/wow/grin secret checks. Keeps /capabilities honest: a chain it can't serve won't boot advertising itself as available. Tests: config regressions (currency, utxo-electrum); a PriceClient feed-subset selection unit test; the /prices integration test now populates the snapshot and asserts the served quotes + RFC-3339 timestamp (the prior PRICES_ASSETS env-var was inert under the harness); capabilities asserts network presence/null across all five chains. Dismissed after verification: coingecko json() was already switched to a streaming read_capped; fleet refresh-jitter (no API key, single-node, self-healing on 429); prices capability staying a bare bool (/prices carries the currency + per-asset map).
This commit is contained in:
+13
-3
@@ -19,9 +19,19 @@ async fn capabilities_is_public_and_well_shaped() {
|
||||
"chain {chain} reports enabled"
|
||||
);
|
||||
}
|
||||
// UTXO chains carry a network; CryptoNote/Grin don't.
|
||||
assert!(body["chains"]["btc"]["network"].as_str().is_some());
|
||||
assert!(body["chains"]["xmr"]["network"].is_null());
|
||||
// UTXO chains carry a network; CryptoNote/Grin report null.
|
||||
for chain in ["btc", "ltc"] {
|
||||
assert!(
|
||||
body["chains"][chain]["network"].as_str().is_some(),
|
||||
"{chain} carries a network"
|
||||
);
|
||||
}
|
||||
for chain in ["xmr", "wow", "grin"] {
|
||||
assert!(
|
||||
body["chains"][chain]["network"].is_null(),
|
||||
"{chain} network is null"
|
||||
);
|
||||
}
|
||||
// Feature flags are present.
|
||||
for feat in ["grin_relay", "prices", "nostr_identity", "tips"] {
|
||||
assert!(
|
||||
|
||||
+21
-4
@@ -21,10 +21,9 @@ async fn prices_endpoint_respects_the_feature_flag() {
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
assert_eq!(body["code"], "NOT_FOUND");
|
||||
|
||||
// Enabled → 200 with the documented shape. No background task in the harness,
|
||||
// so prices are empty and updated_at is null until the first refresh.
|
||||
// Enabled, before any refresh → 200 with the documented empty shape (the
|
||||
// background task lives in `main`, not the harness, so updated_at is null).
|
||||
std::env::set_var("FEATURE_PRICES", "true");
|
||||
std::env::set_var("PRICES_ASSETS", "btc,xmr");
|
||||
let app = common::try_app()
|
||||
.await
|
||||
.expect("app builds with prices enabled");
|
||||
@@ -34,6 +33,24 @@ async fn prices_endpoint_respects_the_feature_flag() {
|
||||
assert!(body["prices"].is_object(), "prices is a map");
|
||||
assert!(body["updated_at"].is_null(), "not yet refreshed");
|
||||
|
||||
// Enabled, with a populated snapshot → the handler serves the quotes and a
|
||||
// non-null RFC 3339 timestamp. Drives the serving path the inert
|
||||
// PRICES_ASSETS env-var never could; feed *selection* is unit-tested in
|
||||
// infra::prices.
|
||||
{
|
||||
let mut snap = app.state.prices.write().await;
|
||||
snap.prices.insert("btc".into(), 64000.0);
|
||||
snap.prices.insert("xmr".into(), 150.0);
|
||||
snap.updated_at = Some(chrono::Utc::now());
|
||||
}
|
||||
let (status, body) = app.request("GET", "/api/v1/prices", None, None).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["prices"]["btc"], 64000.0);
|
||||
assert_eq!(body["prices"]["xmr"], 150.0);
|
||||
assert!(
|
||||
body["updated_at"].as_str().is_some(),
|
||||
"timestamp serialized"
|
||||
);
|
||||
|
||||
std::env::remove_var("FEATURE_PRICES");
|
||||
std::env::remove_var("PRICES_ASSETS");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user