// file: crates/ksp-wallet-lib/unit_tests/administration.rs // version: 2 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::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")); }