Files
khadhroony-solana-project/crates/ksp-wallet-lib/unit_tests/administration.rs
2026-08-19 18:08:01 +02:00

233 lines
14 KiB
Rust

// file: crates/ksp-wallet-lib/unit_tests/administration.rs
// version: 1
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet administration test runtime must build");
}
fn published_vector(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let directory = tempfile::Builder::new().prefix("ksp-wallet-admin-").tempdir().expect("Wallet administration test directory must be creatable");
let path = directory.path().join(name);
crate::persistence::persist_new_wallet_for_test(path.as_path(), FULL_VECTOR).expect("full Wallet vector must publish for administration test");
return (directory, path);
}
#[test]
fn owner_signing_is_deterministic_and_owner_rotation_preserves_solana_identity() {
let (_directory, path) = published_vector("owner-rotation.kspwallet");
let runtime = runtime();
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
let message = b"KSP pre.007 deterministic signing canary";
let signature_before = owner.sign(message).expect("OWNER must sign");
let signature_repeat = owner.sign(message).expect("OWNER repeated signature must succeed");
assert_eq!(signature_before, signature_repeat);
runtime
.block_on(owner.rotate_owner_password(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre007-new-owner-password"))))
.expect("OWNER password rotation must persist");
let old = runtime.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
assert_eq!(old.expect_err("old OWNER password must stop unlocking current file").code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre007-new-owner-password"))))
.expect("new OWNER password must unlock current file");
assert_eq!(*reopened.pubkey(), pubkey);
assert_eq!(reopened.sign(message).expect("rotated OWNER must still sign"), signature_before);
}
#[test]
fn view_self_rotation_changes_only_the_current_view_credential() {
let (_directory, path) = published_vector("view-self-rotation.kspwallet");
let runtime = runtime();
let mut view = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW fixture must open");
let pubkey = *view.pubkey();
let alias = view.alias().map(std::string::String::from);
let notes = view.notes().to_vec();
runtime
.block_on(view.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-new-view-password"))))
.expect("VIEW self-rotation must persist");
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("old VIEW password must stop unlocking current file").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-new-view-password"))))
.expect("new VIEW password must unlock current file");
assert_eq!(*reopened.pubkey(), pubkey);
assert_eq!(reopened.alias(), alias.as_deref());
assert_eq!(reopened.notes(), notes.as_slice());
let owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("VIEW self-rotation must not change OWNER credential");
assert_eq!(*owner.pubkey(), pubkey);
}
#[test]
fn owner_rotates_view_without_old_view_password() {
let (_directory, path) = published_vector("owner-view-rotation.kspwallet");
let runtime = runtime();
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
runtime
.block_on(owner.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-owner-set-view-password"))))
.expect("OWNER must rotate VIEW without old VIEW password");
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("old VIEW password must stop unlocking current file").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-owner-set-view-password"))))
.expect("OWNER-selected VIEW password must unlock");
assert_eq!(*reopened.pubkey(), pubkey);
}
#[test]
fn owner_metadata_administration_is_visible_to_view_but_does_not_change_pubkey() {
let (_directory, path) = published_vector("metadata-admin.kspwallet");
let runtime = runtime();
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
runtime
.block_on(owner.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("pre007-updated-alias"))))
.expect("OWNER alias update must persist");
let note_id = runtime.block_on(owner.add_note(path.as_path(), std::string::String::from("pre007-added-note"))).expect("OWNER note add must persist");
runtime
.block_on(owner.update_note(path.as_path(), note_id.as_str(), std::string::String::from("pre007-updated-note")))
.expect("OWNER note update must persist");
let view = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must read OWNER-updated metadata");
assert_eq!(*view.pubkey(), pubkey);
assert_eq!(view.alias(), std::option::Option::Some("pre007-updated-alias"));
assert!(view.notes().iter().any(|note| return note.id() == note_id.as_str() && note.text() == "pre007-updated-note"));
runtime.block_on(owner.delete_note(path.as_path(), note_id.as_str())).expect("OWNER note delete must persist");
let view_after_delete = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must open after note deletion");
assert!(!view_after_delete.notes().iter().any(|note| return note.id() == note_id.as_str()));
assert_eq!(*view_after_delete.pubkey(), pubkey);
}
#[test]
fn strong_view_disable_and_recreate_change_current_view_authority_without_changing_solana_keypair() {
let (_directory, path) = published_vector("strong-view-recreate.kspwallet");
let runtime = runtime();
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
let message = b"KSP pre.007 strong VIEW rekey canary";
let signature = owner.sign(message).expect("OWNER must sign before VIEW rekey");
let before_bytes = std::fs::read(path.as_path()).expect("current Wallet file must be readable");
let before = crate::KspWalletEnvelopeV1::parse_json(before_bytes.as_slice()).expect("current Wallet must parse");
let old_slot_id = *before.view_descriptor().slot_id().expect("fixture VIEW descriptor must have slot ID");
let old_metadata_ciphertext = before.metadata().ciphertext().to_vec();
runtime.block_on(owner.disable_view(path.as_path())).expect("OWNER strong VIEW disable must persist");
let locked_disabled = runtime.block_on(crate::inspect_locked_wallet_file_v1(path.as_path())).expect("disabled Wallet must inspect");
assert!(!locked_disabled.view_enabled());
let disabled_view =
runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(disabled_view.expect_err("disabled VIEW must not open").code(), crate::ERROR_CODE_CAPABILITY_INSUFFICIENT);
runtime
.block_on(owner.recreate_view(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-recreated-view-password"))))
.expect("OWNER strong VIEW recreate must persist");
let recreated_bytes = std::fs::read(path.as_path()).expect("recreated Wallet file must be readable");
let recreated = crate::KspWalletEnvelopeV1::parse_json(recreated_bytes.as_slice()).expect("recreated Wallet must parse");
let new_slot_id = *recreated.view_descriptor().slot_id().expect("recreated VIEW descriptor must have slot ID");
assert_ne!(new_slot_id, old_slot_id);
assert_ne!(recreated.metadata().ciphertext(), old_metadata_ciphertext.as_slice());
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("historical VIEW password must not unlock recreated current state").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let recreated_view = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-recreated-view-password"))))
.expect("recreated VIEW password must unlock current state");
assert_eq!(*recreated_view.pubkey(), pubkey);
assert_eq!(owner.sign(message).expect("OWNER must still sign after VIEW rekey"), signature);
}
#[test]
fn owner_note_mutation_reports_missing_identifier_without_persisting() {
let (_directory, path) = published_vector("missing-note.kspwallet");
let runtime = runtime();
let before = std::fs::read(path.as_path()).expect("Wallet bytes must be readable before missing-note canary");
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let result = runtime.block_on(owner.update_note(path.as_path(), "AAAAAAAAAAAAAAAAAAAAAA", std::string::String::from("must-not-persist")));
assert_eq!(result.expect_err("missing note identifier must be rejected").code(), crate::ERROR_CODE_NOTE_NOT_FOUND);
assert_eq!(std::fs::read(path.as_path()).expect("Wallet bytes must remain readable"), before);
}
#[test]
fn stale_owner_handle_cannot_overwrite_a_newer_authenticated_wallet_state() {
let (_directory, path) = published_vector("stale-owner.kspwallet");
let runtime = runtime();
let mut first = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("first OWNER handle must open");
let mut stale = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("second OWNER handle must open on the same state");
runtime
.block_on(first.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("first-writer"))))
.expect("first OWNER mutation must persist");
let stale_result = runtime.block_on(stale.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("stale-writer"))));
assert_eq!(stale_result.expect_err("stale OWNER handle must not overwrite newer state").code(), crate::ERROR_CODE_STATE_CONFLICT);
let current = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must open the first writer's state");
assert_eq!(current.alias(), std::option::Option::Some("first-writer"));
}
#[test]
fn owner_handle_cannot_replace_a_different_authenticated_wallet_state() {
let (_first_directory, first_path) = published_vector("wrong-target-first.kspwallet");
let (_second_directory, second_path) = published_vector("wrong-target-second.kspwallet");
let runtime = runtime();
let mut first = runtime
.block_on(crate::open_wallet_owner_file_v1(first_path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("first OWNER handle must open");
let mut second = runtime
.block_on(crate::open_wallet_owner_file_v1(second_path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("second OWNER handle must open");
runtime
.block_on(second.update_alias(second_path.as_path(), std::option::Option::Some(std::string::String::from("different-authenticated-state"))))
.expect("second Wallet must diverge before wrong-target canary");
let wrong_target =
runtime.block_on(first.update_alias(second_path.as_path(), std::option::Option::Some(std::string::String::from("must-not-overwrite-second-wallet"))));
assert_eq!(wrong_target.expect_err("OWNER handle must not replace a different authenticated Wallet state").code(), crate::ERROR_CODE_STATE_CONFLICT);
let unchanged_first = runtime
.block_on(crate::open_wallet_view_file_v1(first_path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("first Wallet must remain unchanged");
let unchanged_second = runtime
.block_on(crate::open_wallet_view_file_v1(second_path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("second Wallet must remain on its own authenticated state");
assert_eq!(unchanged_first.alias(), std::option::Option::Some("pre005-vector-wallet"));
assert_eq!(unchanged_second.alias(), std::option::Option::Some("different-authenticated-state"));
}