//! Field history end-to-end: capture on update, prune by retention policy, //! survive encrypt/decrypt round-trip. use relicario_core::{ Field, FieldValue, HistoryRetention, Item, ItemCore, Section, crypto::KdfParams, derive_master_key, decrypt_item, encrypt_item, }; use relicario_core::item_types::LoginCore; use zeroize::Zeroizing; fn key() -> Zeroizing<[u8; 32]> { derive_master_key(b"x", &[0u8; 32], &[0u8; 32], &KdfParams { argon2_m: 256, argon2_t: 1, argon2_p: 1 }).unwrap() } #[test] fn password_field_history_captured_on_update() { let mut item = Item::new("login".into(), ItemCore::Login(LoginCore::default())); let f = Field::new("password".into(), FieldValue::Password(Zeroizing::new("v0".into()))); let fid = f.id.clone(); item.sections.push(Section { name: None, fields: vec![f] }); item.set_field_value(&fid, FieldValue::Password(Zeroizing::new("v1".into()))).unwrap(); item.set_field_value(&fid, FieldValue::Password(Zeroizing::new("v2".into()))).unwrap(); item.set_field_value(&fid, FieldValue::Password(Zeroizing::new("v3".into()))).unwrap(); let hist = item.field_history.get(&fid).expect("history exists"); assert_eq!(hist.len(), 3); assert_eq!(hist[0].value.as_str(), "v0"); assert_eq!(hist[2].value.as_str(), "v2"); } #[test] fn prune_last_n_keeps_most_recent() { let mut item = Item::new("x".into(), ItemCore::Login(LoginCore::default())); let f = Field::new("p".into(), FieldValue::Password(Zeroizing::new("v0".into()))); let fid = f.id.clone(); item.sections.push(Section { name: None, fields: vec![f] }); for i in 1..=10 { item.set_field_value(&fid, FieldValue::Password(Zeroizing::new(format!("v{i}")))).unwrap(); } item.prune_history(&HistoryRetention::LastN(3), 0); let hist = &item.field_history[&fid]; assert_eq!(hist.len(), 3); // Most recent 3: v7, v8, v9 (v10's predecessor v9 was the latest captured) assert!(hist.last().unwrap().value.as_str().starts_with('v')); } #[test] fn history_survives_encrypt_decrypt() { let mut item = Item::new("x".into(), ItemCore::Login(LoginCore::default())); let f = Field::new("p".into(), FieldValue::Password(Zeroizing::new("v0".into()))); let fid = f.id.clone(); item.sections.push(Section { name: None, fields: vec![f] }); item.set_field_value(&fid, FieldValue::Password(Zeroizing::new("v1".into()))).unwrap(); let blob = encrypt_item(&item, &key()).unwrap(); let decoded = decrypt_item(&blob, &key()).unwrap(); let hist = decoded.field_history.get(&fid).expect("history survived"); assert_eq!(hist.len(), 1); assert_eq!(hist[0].value.as_str(), "v0"); }