146 lines
8.5 KiB
Rust
146 lines
8.5 KiB
Rust
// file: crates/ksp-wallet-lib/unit_tests/persistence.rs
|
|
// version: 4
|
|
|
|
fn temp_directory() -> std::io::Result<tempfile::TempDir> {
|
|
return tempfile::tempdir();
|
|
}
|
|
|
|
#[test]
|
|
fn no_clobber_create_keeps_the_first_published_document() {
|
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
|
let destination = directory.path().join("wallet.kspwallet");
|
|
let first = b"first-wallet";
|
|
let second = b"second-wallet";
|
|
crate::persist_new_wallet_for_test(destination.as_path(), first).expect("first no-clobber publication must succeed");
|
|
let second_result = crate::persist_new_wallet_for_test(destination.as_path(), second);
|
|
let error = second_result.expect_err("second publication must not overwrite an existing wallet");
|
|
assert_eq!(error.code(), crate::ERROR_CODE_DESTINATION_EXISTS);
|
|
let persisted = std::fs::read(destination.as_path()).expect("published wallet must remain readable");
|
|
assert_eq!(persisted, first);
|
|
}
|
|
|
|
#[test]
|
|
fn injected_failure_before_publish_leaves_no_destination_or_partial_wallet() {
|
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
|
let destination = directory.path().join("wallet.kspwallet");
|
|
let result = crate::persist_new_wallet_fault_before_publish(destination.as_path(), b"candidate-wallet");
|
|
let error = result.expect_err("fault injection must abort before publication");
|
|
assert_eq!(error.code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
|
|
assert!(!destination.exists());
|
|
let entries = std::fs::read_dir(directory.path()).expect("Wallet persistence test directory must remain readable");
|
|
let count = entries.count();
|
|
assert_eq!(count, 0, "temporary artifacts should be cleaned on ordinary error unwinding");
|
|
}
|
|
|
|
#[test]
|
|
fn concurrent_no_clobber_publish_has_exactly_one_winner() {
|
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
|
let destination = directory.path().join("wallet.kspwallet");
|
|
let barrier = std::sync::Arc::new(std::sync::Barrier::new(8));
|
|
let mut handles = std::vec::Vec::new();
|
|
for index in 0_u8..8 {
|
|
let destination = destination.clone();
|
|
let barrier = std::sync::Arc::clone(&barrier);
|
|
handles.push(std::thread::spawn(move || {
|
|
barrier.wait();
|
|
let content = [index; 32];
|
|
return crate::persist_new_wallet_for_test(destination.as_path(), content.as_slice());
|
|
}));
|
|
}
|
|
let mut success_count = 0_usize;
|
|
let mut exists_count = 0_usize;
|
|
let mut unexpected_code = std::option::Option::None;
|
|
for handle in handles {
|
|
let result = handle.join().expect("Wallet persistence test worker must not panic");
|
|
match result {
|
|
std::result::Result::Ok(()) => success_count += 1,
|
|
std::result::Result::Err(error) if error.code() == crate::ERROR_CODE_DESTINATION_EXISTS => exists_count += 1,
|
|
std::result::Result::Err(error) => unexpected_code = std::option::Option::Some(error.code()),
|
|
}
|
|
}
|
|
assert_eq!(unexpected_code, std::option::Option::None);
|
|
assert_eq!(success_count, 1);
|
|
assert_eq!(exists_count, 7);
|
|
assert_eq!(std::fs::metadata(destination.as_path()).expect("winning wallet must exist").len(), 32);
|
|
}
|
|
|
|
#[test]
|
|
fn bounded_reader_rejects_oversized_wallet_before_parser_allocation() {
|
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
|
let destination = directory.path().join("oversized.kspwallet");
|
|
let oversized = std::vec![b'x'; crate::KSPWALLET_MAX_FILE_BYTES + 1];
|
|
std::fs::write(destination.as_path(), oversized).expect("oversized fixture must be writable");
|
|
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
|
|
let result = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path()));
|
|
let error = result.expect_err("oversized Wallet file must be rejected before parsing");
|
|
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
|
}
|
|
|
|
#[test]
|
|
fn public_file_create_and_locked_inspect_round_trip_without_revealing_identity() {
|
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
|
let destination = directory.path().join("created.kspwallet");
|
|
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
|
|
let owner_password = crate::OwnerPassword::new(std::string::String::from("pre006-owner-password"));
|
|
let created = runtime
|
|
.block_on(crate::create_wallet_file_v1(destination.as_path(), owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default()))
|
|
.expect("native Wallet file creation must succeed");
|
|
let locked = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path())).expect("persisted Wallet must inspect successfully");
|
|
assert!(!locked.view_enabled());
|
|
assert_eq!(created.capability(), crate::WalletCapability::Owner);
|
|
assert!(destination.exists());
|
|
let second_password = crate::OwnerPassword::new(std::string::String::from("pre006-other-owner-password"));
|
|
let second = runtime.block_on(crate::create_wallet_file_v1(
|
|
destination.as_path(),
|
|
second_password,
|
|
std::option::Option::None,
|
|
crate::WalletCreateMetadataV1::default(),
|
|
));
|
|
assert_eq!(second.expect_err("create must remain no-clobber").code(), crate::ERROR_CODE_DESTINATION_EXISTS);
|
|
}
|
|
|
|
#[test]
|
|
fn persisted_full_vector_opens_view_and_owner_through_file_apis() {
|
|
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
|
|
let destination = directory.path().join("vector.kspwallet");
|
|
let vector = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
|
|
crate::persist_new_wallet_for_test(destination.as_path(), vector).expect("full vector must publish through no-clobber persistence");
|
|
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
|
|
let view_password = crate::ViewPassword::new(std::string::String::from("pre005-view-password"));
|
|
let view = runtime
|
|
.block_on(crate::open_wallet_view_file_v1(destination.as_path(), view_password))
|
|
.expect("persisted full vector must open through VIEW file API");
|
|
let owner_password = crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"));
|
|
let owner = runtime
|
|
.block_on(crate::open_wallet_owner_file_v1(destination.as_path(), owner_password))
|
|
.expect("persisted full vector must open through OWNER file API");
|
|
assert_eq!(view.capability(), crate::WalletCapability::View);
|
|
assert_eq!(owner.capability(), crate::WalletCapability::Owner);
|
|
assert_eq!(view.pubkey(), owner.pubkey());
|
|
assert_eq!(view.alias(), std::option::Option::Some("pre005-vector-wallet"));
|
|
assert_eq!(owner.alias(), std::option::Option::Some("pre005-vector-wallet"));
|
|
}
|
|
|
|
#[test]
|
|
fn replacement_fault_before_publish_preserves_the_previous_wallet_bytes() {
|
|
let directory = temp_directory().expect("Wallet replacement test directory must be creatable");
|
|
let destination = directory.path().join("replace-fault.kspwallet");
|
|
let original = b"ORIGINAL-WALLET-CANARY";
|
|
let replacement = b"REPLACEMENT-WALLET-CANARY";
|
|
std::fs::write(destination.as_path(), original).expect("replacement test original must be writable");
|
|
let result = crate::replace_wallet_fault_before_publish(destination.as_path(), replacement);
|
|
assert_eq!(result.expect_err("injected replacement fault must fail").code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
|
|
assert_eq!(std::fs::read(destination.as_path()).expect("original Wallet bytes must remain readable"), original);
|
|
}
|
|
|
|
#[test]
|
|
fn replacement_requires_an_existing_regular_file_and_replaces_complete_content() {
|
|
let directory = temp_directory().expect("Wallet replacement test directory must be creatable");
|
|
let destination = directory.path().join("replace.kspwallet");
|
|
let missing = crate::replace_wallet_for_test(destination.as_path(), b"replacement");
|
|
assert_eq!(missing.expect_err("replacement must not create a missing Wallet destination").code(), crate::ERROR_CODE_IO_FAILED);
|
|
std::fs::write(destination.as_path(), b"old").expect("replacement test original must be writable");
|
|
crate::replace_wallet_for_test(destination.as_path(), b"new-complete-wallet").expect("existing Wallet replacement must succeed");
|
|
assert_eq!(std::fs::read(destination.as_path()).expect("replaced Wallet must be readable"), b"new-complete-wallet");
|
|
}
|