Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! CR-001 — SPV mn-list sync readiness.
//!
//! Spec: `tests/e2e/TEST_SPEC.md` (### Core (CR) → CR-001).
//!
//! Pins the SPV-readiness contract: the mn-list manager reaches
//! `SyncState::Synced`, the synced height is > 0, and the SPV runtime
//! is in a started (running) state on return. The spec's informational
//! warm-cache target is 180 s; the helper internally raises every
//! request to its `COLD_CACHE_TIMEOUT_FLOOR` (600 s) so the actual
//! wait can be longer on a cold testnet cache.
//!
//! The harness already calls `wait_for_mn_list_synced` during
//! `E2eContext::build`; this test re-asserts the same contract from the
//! test-body perspective to keep the pin explicit and independently
//! verifiable. The call returns immediately when the harness already
//! cleared the gate.
//!
//! Mirrors DET's `test_spv_sync_and_create_wallet` at
//! `dash-evo-tool/tests/backend-e2e/spv_wallet.rs:14`.

use std::time::Duration;

use crate::framework::config::{spv_disabled_from_env, vars};
use crate::framework::prelude::*;
use crate::framework::spv::wait_for_mn_list_synced;

/// Spec's informational warm-cache target for mn-list sync. NOT a hard
/// ceiling: `wait_for_mn_list_synced` raises every request to its
/// internal `COLD_CACHE_TIMEOUT_FLOOR` (600 s — see
/// `framework::spv::wait_for_mn_list_synced`) and emits an `info!`
/// when it does. The real wait is bounded by the floor, not by this
/// constant.
const MN_LIST_SYNC_TIMEOUT: Duration = Duration::from_secs(180);

#[ignore = "CR-001 — needs testnet + SPV runtime. \
Set PLATFORM_WALLET_E2E_DISABLE_SPV=0 (or unset) and supply \
DAPI endpoints via PLATFORM_WALLET_E2E_DAPI_ADDRESSES. \
Mirrors DET's test_spv_sync_and_create_wallet."]
#[tokio_shared_rt::test(shared, flavor = "multi_thread", worker_threads = 12)]
async fn cr_001_spv_mn_list_sync_readiness() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,platform_wallet=debug".into()),
)
.with_test_writer()
.try_init();

// Respect the operator escape hatch — when SPV is disabled the mn-list
// will never sync; skip with an informative message rather than burn
// the full timeout.
if spv_disabled_from_env() {
tracing::info!(
target: "platform_wallet::e2e::cases::cr_001",
var = vars::DISABLE_SPV,
"SPV disabled via env — skipping CR-001 \
(mn-list will never sync without a live SPV runtime)"
);
return;
}

let s = crate::framework::setup().await.expect("setup failed");

// Step 1: bind the SPV runtime. `E2eContext::build` always starts
// SPV unconditionally, so `ctx.spv()` is `Some` in every supported
// configuration; the `expect` is defence-in-depth in case a future
// harness change makes the runtime optional.
let spv = s
.ctx
.spv()
.expect("PRE-pin violated: ctx.spv() is None — harness must always start SPV");

// Step 2: re-pin mn-list sync from the test body. The harness
// already ran this at init, so the call returns immediately when
// already synced; on a cold cache the helper waits up to its
// `COLD_CACHE_TIMEOUT_FLOOR` (600 s).
wait_for_mn_list_synced(spv, MN_LIST_SYNC_TIMEOUT)
.await
.expect("wait_for_mn_list_synced failed before the cold-cache floor elapsed");

// Step 3: read the mn-list height from the live sync progress.
let progress = spv.sync_progress().await.expect(
"PRE-pin violated: sync_progress() returned None after \
wait_for_mn_list_synced succeeded — SPV client must be running",
);
let mn = progress
.masternodes()
.expect("SyncProgress::masternodes() failed after successful mn-list sync");
let mn_height = mn.current_height();

tracing::info!(
target: "platform_wallet::e2e::cases::cr_001",
mn_height,
state = ?mn.state(),
"CR-001: mn-list synced"
);

// Assertion 1: mn-list height > 0 (proves the client synced real data,
// not just initialised with a zero-height placeholder).
assert!(
mn_height > 0,
"POST-pin violated: mn-list height is 0 after sync — \
the mn-list manager must advance at least one block to report Synced. \
Check SPV peer connectivity and mn-list initial-sync logic."
);

// Assertion 2: SPV runtime is started (running). `is_started()` returns
// `true` when the internal DashSpvClient is initialised and the sync
// loop is live. This is the available proxy for the spec's "Ready state"
// contract (SpvHealth is not yet a public type — see TEST_SPEC.md CR-001
// harness-extensions note).
assert!(
spv.is_started(),
"POST-pin violated: SpvRuntime::is_started() is false after \
wait_for_mn_list_synced returned Ok — the runtime must remain \
started (running) throughout the test session."
);

s.teardown().await.expect("teardown");
}
1 change: 1 addition & 0 deletions packages/rs-platform-wallet/tests/e2e/cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! TEST_SPEC.md priorities (P1, P2, ID-, DP-, DPNS-, TK-, …) follow
//! in subsequent PRs.

pub mod cr_001_spv_mn_list_sync_readiness;
pub mod cr_003_asset_lock_funded_registration;
pub mod dpns_001_register_name;
pub mod id_001_register_identity_from_addresses;
Expand Down
70 changes: 70 additions & 0 deletions packages/rs-platform-wallet/tests/e2e/framework/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ pub mod vars {
/// that don't need Core duffs; any positive integer overrides the
/// timeout (in seconds).
pub const BANK_CORE_GATE: &str = "PLATFORM_WALLET_E2E_BANK_CORE_GATE";
/// Operator escape hatch for SPV-gated cases (CR-001, anything
/// asserting on `SpvRuntime` post-conditions). When truthy
/// (`1` / `true` / `yes` / `on`, case-insensitive), the case body
/// skips with an informative log. The harness itself does NOT
/// read this flag — `E2eContext::build` always starts SPV; the
/// gate is consumed test-side via [`super::spv_disabled_from_env`].
/// See `TEST_SPEC.md` CR-001 for the SPEC-level reference.
pub const DISABLE_SPV: &str = "PLATFORM_WALLET_E2E_DISABLE_SPV";
}

/// Default deadline for the bank Core funding gate when the env var is
Expand Down Expand Up @@ -359,6 +367,34 @@ pub(crate) fn parse_bank_core_gate(raw: Option<&str>) -> (Option<Duration>, Bank
}
}

/// Returns `true` when [`vars::DISABLE_SPV`] is set to a truthy value
/// (`1` / `true` / `yes` / `on`, case-insensitive, surrounding
/// whitespace ignored). Any other value — including unset, empty, or
/// unrecognised — returns `false`.
///
/// SPV-gated cases (e.g. CR-001) call this at the top of the test body
/// and `return` early when it reports `true`, so the operator can opt
/// out of SPV-only assertions without burning the cold-cache timeout.
/// The harness itself never reads the flag: `E2eContext::build` always
/// starts SPV.
pub fn spv_disabled_from_env() -> bool {
is_truthy_env(vars::DISABLE_SPV)
}

/// Truthy-env helper shared by SPV-style boolean flags. Reads `key`
/// from the process environment and returns `true` for `1` / `true` /
/// `yes` / `on` (case-insensitive, trimmed); everything else — unset,
/// empty, or unrecognised — returns `false`.
fn is_truthy_env(key: &str) -> bool {
matches!(
std::env::var(key).ok().as_deref().map(str::trim),
Some(v) if v == "1"
|| v.eq_ignore_ascii_case("true")
|| v.eq_ignore_ascii_case("yes")
|| v.eq_ignore_ascii_case("on")
)
}

/// Parse a network string supporting the canonical dashcore names
/// plus the test-harness `local` alias for regtest and an empty
/// shorthand for testnet. Used only at [`Config`] construction;
Expand Down Expand Up @@ -434,4 +470,38 @@ mod tests {
assert_eq!(timeout, Some(DEFAULT_BANK_CORE_GATE_TIMEOUT));
assert_eq!(src, BankCoreGateSource::EnvInvalidFallback);
}

/// Process-wide env-var flag used to exercise [`is_truthy_env`].
/// Distinct from any production var so cargo-test parallelism with
/// the `from_env` callers can never collide. The truthy/falsy
/// matrix is exercised in a single test so the two halves don't
/// race over the same key under parallel cargo-test execution.
const TRUTHY_PROBE_VAR: &str = "PLATFORM_WALLET_E2E_TEST_TRUTHY_PROBE";

#[test]
fn is_truthy_env_matrix() {
// SAFETY: single-threaded — the probe key is unique to this
// test, so no parallel test can mutate it underneath us.
std::env::remove_var(TRUTHY_PROBE_VAR);
assert!(!is_truthy_env(TRUTHY_PROBE_VAR), "unset must be falsy");

for raw in [
"1", "true", "TRUE", "True", "yes", "Yes", "YES", "on", "ON", " on ", " 1\t",
] {
std::env::set_var(TRUTHY_PROBE_VAR, raw);
assert!(
is_truthy_env(TRUTHY_PROBE_VAR),
"{raw:?} should be recognised as truthy"
);
}

for raw in ["", " ", "0", "false", "no", "off", "disabled", "abc"] {
std::env::set_var(TRUTHY_PROBE_VAR, raw);
assert!(
!is_truthy_env(TRUTHY_PROBE_VAR),
"{raw:?} must NOT be recognised as truthy"
);
}
std::env::remove_var(TRUTHY_PROBE_VAR);
}
}
Loading