mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
Cached crypto→fiat price feed, refreshed by a background task and served from an in-memory snapshot — no upstream call on the request path. Public and feature-flagged: when FEATURE_PRICES is off, /prices is 404 and no upstream is ever contacted. Per-feed control, as a public server needs: PRICES_ASSETS narrows which feeds an instance quotes (unset = all supported; an explicit list serves a subset; empty serves none), validated fail-closed against SUPPORTED_PRICE_ASSETS. Provider (PRICES_PROVIDER, coingecko) and currency (PRICES_CURRENCY, usd) are configurable; the refresh interval is clamped to a provider-friendly minimum. - infra/prices: PriceClient (feed whitelist → provider coin ids), bounded by the HTTP client timeout; non-finite/negative/missing quotes are skipped, not faked. A fetch failure keeps the last good snapshot. Drift guard: a test asserts the provider mapping covers exactly SUPPORTED_PRICE_ASSETS. - main: background refresh task only when enabled and at least one feed is set. - AppState.prices snapshot; handler 404s when disabled. - openapi (prices tag); unit tests + L1 test over both feature-flag states.
40 lines
1.7 KiB
Rust
40 lines
1.7 KiB
Rust
//! L1 integration: the public prices endpoint, both feature-flag states.
|
|
//!
|
|
//! Drives `FEATURE_PRICES` explicitly and rebuilds the app for each state, so
|
|
//! both paths are deterministic without any network call. (The background
|
|
//! refresh task lives in `main`, not `build_router`, so under the harness the
|
|
//! enabled feed simply serves an empty, not-yet-populated snapshot.) The live
|
|
//! upstream fetch is covered by `infra::prices` unit tests and is an L3 concern.
|
|
|
|
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
|
|
#[tokio::test]
|
|
async fn prices_endpoint_respects_the_feature_flag() {
|
|
// Disabled → 404, so a client reading /capabilities never special-cases it.
|
|
std::env::set_var("FEATURE_PRICES", "false");
|
|
let Some(app) = common::try_app().await else {
|
|
return;
|
|
};
|
|
let (status, body) = app.request("GET", "/api/v1/prices", None, None).await;
|
|
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.
|
|
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");
|
|
let (status, body) = app.request("GET", "/api/v1/prices", None, None).await;
|
|
assert_eq!(status, StatusCode::OK);
|
|
assert!(body["currency"].as_str().is_some(), "currency present");
|
|
assert!(body["prices"].is_object(), "prices is a map");
|
|
assert!(body["updated_at"].is_null(), "not yet refreshed");
|
|
|
|
std::env::remove_var("FEATURE_PRICES");
|
|
std::env::remove_var("PRICES_ASSETS");
|
|
}
|