Renames crate directories and sweeps identifiers so Plan 1B can reference
the post-rename names throughout.
- git mv crates/idfoto-{core,cli,wasm} → crates/relicario-{core,cli,wasm}
- sed sweep: idfoto_core/idfoto-core/IdfotoError/IDFOTO_IMAGE/.idfoto/ etc.
- All 128 relicario-core tests pass post-sweep
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
2.6 KiB
Rust
64 lines
2.6 KiB
Rust
//! 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");
|
|
}
|