test(core): field history integration (capture, prune, round-trip)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
63
crates/idfoto-core/tests/field_history.rs
Normal file
63
crates/idfoto-core/tests/field_history.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
//! Field history end-to-end: capture on update, prune by retention policy,
|
||||||
|
//! survive encrypt/decrypt round-trip.
|
||||||
|
|
||||||
|
use idfoto_core::{
|
||||||
|
Field, FieldValue, HistoryRetention, Item, ItemCore, Section,
|
||||||
|
crypto::KdfParams,
|
||||||
|
derive_master_key, decrypt_item, encrypt_item,
|
||||||
|
};
|
||||||
|
use idfoto_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}").into()))).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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user