use std::path::Path;
use citadel::{
read_audit_log, scan_corrupted_audit_log, verify_audit_log, AuditConfig, AuditEventType,
Database, DatabaseBuilder, KdfAlgorithm,
};
fn create_test_db(dir: &Path, passphrase: &[u8]) -> Database {
DatabaseBuilder::new(dir.join("test.citadel"))
.passphrase(passphrase)
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.create()
.unwrap()
}
fn open_test_db(dir: &Path, passphrase: &[u8]) -> Database {
DatabaseBuilder::new(dir.join("test.citadel"))
.passphrase(passphrase)
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.open()
.unwrap()
}
fn audit_path(dir: &Path) -> std::path::PathBuf {
dir.join("test.citadel.citadel-audit")
}
fn get_audit_key(dir: &Path, passphrase: &[u8]) -> [u8; 32] {
use citadel::core::KEY_FILE_SIZE;
use citadel::crypto::key_manager::open_key_file;
let key_path = dir.join("test.citadel.citadel-keys");
let key_data = std::fs::read(&key_path).unwrap();
let key_buf: [u8; KEY_FILE_SIZE] = key_data.try_into().unwrap();
let (_kf, keys) = open_key_file(&key_buf, passphrase, 0).unwrap_or_else(|_| {
let data_path = dir.join("test.citadel");
let mut file = std::fs::File::open(&data_path).unwrap();
use std::io::{Read, Seek, SeekFrom};
let mut header_buf = [0u8; citadel::core::FILE_HEADER_SIZE];
file.seek(SeekFrom::Start(0)).unwrap();
file.read_exact(&mut header_buf).unwrap();
let header = citadel::io::file_manager::FileHeader::deserialize(&header_buf).unwrap();
open_key_file(&key_buf, passphrase, header.file_id).unwrap()
});
keys.audit_key
}
#[test]
fn audit_log_created_on_db_create() {
let dir = tempfile::tempdir().unwrap();
let _db = create_test_db(dir.path(), b"pass");
assert!(audit_path(dir.path()).exists());
}
#[test]
fn audit_log_records_create_event() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
assert!(entries.len() >= 2);
assert_eq!(entries[0].event_type, AuditEventType::DatabaseCreated);
assert_eq!(entries[0].detail.len(), 2);
}
#[test]
fn audit_log_records_open_event() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let db = open_test_db(dir.path(), b"pass");
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let open_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::DatabaseOpened)
.collect();
assert_eq!(open_events.len(), 1);
}
#[test]
fn audit_log_records_close_event() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let close_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::DatabaseClosed)
.collect();
assert_eq!(close_events.len(), 1);
}
#[test]
fn audit_log_passphrase_change() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"old");
db.change_passphrase(b"old", b"new").unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let change_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::PassphraseChanged)
.collect();
assert_eq!(change_events.len(), 1);
}
#[test]
fn audit_log_key_backup_export() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("backup.citadel-key-backup");
db.export_key_backup(b"pass", b"backup-pass", &backup_path)
.unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let export_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::KeyBackupExported)
.collect();
assert_eq!(export_events.len(), 1);
assert!(!export_events[0].detail.is_empty());
}
#[test]
fn audit_log_backup() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_dest = dir.path().join("backup.citadel");
db.backup(&backup_dest).unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let backup_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::BackupCreated)
.collect();
assert_eq!(backup_events.len(), 1);
assert!(!backup_events[0].detail.is_empty());
}
#[test]
fn audit_log_compact() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key1", b"value1").unwrap();
wtx.commit().unwrap();
let compact_dest = dir.path().join("compact.citadel");
db.compact(&compact_dest).unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let compact_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::CompactionPerformed)
.collect();
assert_eq!(compact_events.len(), 1);
}
#[test]
fn audit_log_integrity_check() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let report = db.integrity_check().unwrap();
assert!(report.errors.is_empty());
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let check_events: Vec<_> = entries
.iter()
.filter(|e| e.event_type == AuditEventType::IntegrityCheckPerformed)
.collect();
assert_eq!(check_events.len(), 1);
let error_count = u32::from_le_bytes(check_events[0].detail[..4].try_into().unwrap());
assert_eq!(error_count, 0);
}
#[test]
fn audit_chain_verification_valid() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"k", b"v").unwrap();
wtx.commit().unwrap();
db.integrity_check().unwrap();
drop(db);
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&audit_path(dir.path()), &audit_key).unwrap();
assert!(result.chain_valid);
assert!(result.entries_verified >= 3);
assert!(result.chain_break_at.is_none());
}
#[test]
fn audit_chain_tamper_detected() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
drop(db);
let ap = audit_path(dir.path());
let mut data = std::fs::read(&ap).unwrap();
if data.len() > 74 {
data[73] ^= 0x01;
std::fs::write(&ap, &data).unwrap();
}
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&ap, &audit_key).unwrap();
assert!(!result.chain_valid);
assert!(result.chain_break_at.is_some());
}
#[test]
fn audit_wrong_key_fails_verification() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let wrong_key = [0xFFu8; 32];
let result = verify_audit_log(&audit_path(dir.path()), &wrong_key).unwrap();
assert!(!result.chain_valid);
}
#[test]
fn audit_disabled_no_file() {
let dir = tempfile::tempdir().unwrap();
let config = AuditConfig {
enabled: false,
max_file_size: 10 * 1024 * 1024,
max_rotated_files: 3,
};
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"pass")
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.audit_config(config)
.create()
.unwrap();
drop(db);
assert!(!audit_path(dir.path()).exists());
}
#[test]
fn audit_in_memory_no_log() {
let dir = tempfile::tempdir().unwrap();
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"pass")
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.create_in_memory()
.unwrap();
drop(db);
assert!(!audit_path(dir.path()).exists());
}
#[test]
fn audit_sequence_numbers_monotonic() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
db.integrity_check().unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
assert!(entries.len() >= 2);
for i in 1..entries.len() {
assert!(
entries[i].sequence_no > entries[i - 1].sequence_no,
"sequence numbers must be strictly increasing"
);
}
}
#[test]
fn audit_timestamps_nondecreasing() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
db.integrity_check().unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
for i in 1..entries.len() {
assert!(
entries[i].timestamp >= entries[i - 1].timestamp,
"timestamps must be non-decreasing"
);
}
}
#[test]
fn read_audit_log_without_key() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
assert!(entries.len() >= 2);
for entry in &entries {
assert!(entry.timestamp > 0);
assert!(entry.sequence_no > 0);
}
}
#[test]
fn audit_persists_across_reopen() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
drop(db);
let db = open_test_db(dir.path(), pass);
db.integrity_check().unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let event_types: Vec<_> = entries.iter().map(|e| e.event_type).collect();
assert!(event_types.contains(&AuditEventType::DatabaseCreated));
assert!(event_types.contains(&AuditEventType::DatabaseOpened));
assert!(event_types.contains(&AuditEventType::IntegrityCheckPerformed));
let close_count = event_types
.iter()
.filter(|&&et| et == AuditEventType::DatabaseClosed)
.count();
assert_eq!(close_count, 2);
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&audit_path(dir.path()), &audit_key).unwrap();
assert!(result.chain_valid);
}
#[test]
fn audit_multiple_operations() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key1", b"value1").unwrap();
wtx.commit().unwrap();
db.change_passphrase(pass, b"newpass").unwrap();
let backup_path = dir.path().join("backup.citadel");
db.backup(&backup_path).unwrap();
db.integrity_check().unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
assert!(entries.len() >= 5);
let event_types: Vec<_> = entries.iter().map(|e| e.event_type).collect();
assert!(event_types.contains(&AuditEventType::DatabaseCreated));
assert!(event_types.contains(&AuditEventType::PassphraseChanged));
assert!(event_types.contains(&AuditEventType::BackupCreated));
assert!(event_types.contains(&AuditEventType::IntegrityCheckPerformed));
assert!(event_types.contains(&AuditEventType::DatabaseClosed));
}
#[test]
fn audit_file_format_magic() {
let dir = tempfile::tempdir().unwrap();
let _db = create_test_db(dir.path(), b"pass");
let data = std::fs::read(audit_path(dir.path())).unwrap();
let magic = u32::from_le_bytes(data[0..4].try_into().unwrap());
assert_eq!(magic, 0x4155_4454);
}
#[test]
fn audit_entry_detail_data() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let create_entry = entries
.iter()
.find(|e| e.event_type == AuditEventType::DatabaseCreated)
.unwrap();
assert_eq!(create_entry.detail.len(), 2);
assert_eq!(create_entry.detail[0], 0); assert_eq!(create_entry.detail[1], 1); }
#[test]
fn audit_rotation_triggers_on_size() {
let dir = tempfile::tempdir().unwrap();
let config = AuditConfig {
enabled: true,
max_file_size: 200,
max_rotated_files: 2,
};
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"pass")
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.audit_config(config)
.create()
.unwrap();
for _ in 0..10 {
db.integrity_check().unwrap();
}
drop(db);
let rotated_1 = dir.path().join("test.citadel.citadel-audit.1");
assert!(rotated_1.exists());
assert!(audit_path(dir.path()).exists());
}
#[test]
fn audit_rotation_deletes_old_files() {
let dir = tempfile::tempdir().unwrap();
let config = AuditConfig {
enabled: true,
max_file_size: 200,
max_rotated_files: 1,
};
let db = DatabaseBuilder::new(dir.path().join("test.citadel"))
.passphrase(b"pass")
.kdf_algorithm(KdfAlgorithm::Pbkdf2HmacSha256)
.pbkdf2_iterations(600_000)
.cache_size(64)
.audit_config(config)
.create()
.unwrap();
for _ in 0..20 {
db.integrity_check().unwrap();
}
drop(db);
let rotated_1 = dir.path().join("test.citadel.citadel-audit.1");
let rotated_2 = dir.path().join("test.citadel.citadel-audit.2");
assert!(rotated_1.exists());
assert!(!rotated_2.exists());
}
#[test]
fn audit_log_path_method() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let path = db.audit_log_path();
assert!(path.is_some());
assert_eq!(path.unwrap(), audit_path(dir.path()));
}
#[test]
fn scenario_full_lifecycle_audit_trail() {
let dir = tempfile::tempdir().unwrap();
let pass = b"initial-pass";
let db = create_test_db(dir.path(), pass);
let mut wtx = db.begin_write().unwrap();
for i in 0..100u32 {
wtx.insert(&i.to_be_bytes(), &[i as u8; 64]).unwrap();
}
wtx.commit().unwrap();
db.change_passphrase(pass, b"new-pass").unwrap();
let pass = b"new-pass";
let report = db.integrity_check().unwrap();
assert!(report.errors.is_empty());
let backup_path = dir.path().join("backup.citadel");
db.backup(&backup_path).unwrap();
let key_backup_path = dir.path().join("key.backup");
db.export_key_backup(pass, b"backup-pw", &key_backup_path)
.unwrap();
let compact_path = dir.path().join("compact.citadel");
db.compact(&compact_path).unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let event_types: Vec<AuditEventType> = entries.iter().map(|e| e.event_type).collect();
assert!(event_types.contains(&AuditEventType::DatabaseCreated));
assert!(event_types.contains(&AuditEventType::PassphraseChanged));
assert!(event_types.contains(&AuditEventType::IntegrityCheckPerformed));
assert!(event_types.contains(&AuditEventType::BackupCreated));
assert!(event_types.contains(&AuditEventType::KeyBackupExported));
assert!(event_types.contains(&AuditEventType::CompactionPerformed));
assert!(event_types.contains(&AuditEventType::DatabaseClosed));
for i in 1..entries.len() {
assert!(entries[i].sequence_no == entries[i - 1].sequence_no + 1);
}
assert_eq!(event_types[0], AuditEventType::DatabaseCreated);
assert_eq!(*event_types.last().unwrap(), AuditEventType::DatabaseClosed);
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&audit_path(dir.path()), &audit_key).unwrap();
assert!(result.chain_valid);
assert_eq!(result.entries_verified, entries.len() as u64);
let db = open_test_db(dir.path(), pass);
db.integrity_check().unwrap();
drop(db);
let entries_after = read_audit_log(&audit_path(dir.path())).unwrap();
assert!(entries_after.len() > entries.len());
let result = verify_audit_log(&audit_path(dir.path()), &audit_key).unwrap();
assert!(result.chain_valid);
let last_old_seq = entries.last().unwrap().sequence_no;
let first_new_seq = entries_after[entries.len()].sequence_no;
assert_eq!(first_new_seq, last_old_seq + 1);
}
#[test]
fn scenario_tamper_middle_of_chain_detected() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
db.integrity_check().unwrap();
db.integrity_check().unwrap();
db.integrity_check().unwrap();
drop(db);
let ap = audit_path(dir.path());
let entries_before = read_audit_log(&ap).unwrap();
assert!(entries_before.len() >= 5);
let data = std::fs::read(&ap).unwrap();
let audit_key = get_audit_key(dir.path(), pass);
let pre_result = verify_audit_log(&ap, &audit_key).unwrap();
assert!(pre_result.chain_valid);
let mut offset = 64usize;
for _ in 0..2 {
offset += 4; let entry_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += entry_len;
}
let mut tampered = data.clone();
tampered[offset + 4 + 5] ^= 0xFF;
std::fs::write(&ap, &tampered).unwrap();
let result = verify_audit_log(&ap, &audit_key).unwrap();
assert!(!result.chain_valid);
assert_eq!(result.entries_verified, 2);
assert_eq!(result.chain_break_at, Some(3));
}
#[test]
fn scenario_truncation_detected_by_count() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
db.integrity_check().unwrap();
drop(db);
let ap = audit_path(dir.path());
let entries = read_audit_log(&ap).unwrap();
let original_count = entries.len();
assert!(original_count >= 3);
let data = std::fs::read(&ap).unwrap();
let last_entry = &entries[original_count - 1];
assert_eq!(last_entry.event_type, AuditEventType::DatabaseClosed);
let mut offset = 64usize;
for _ in 0..original_count - 1 {
offset += 4;
let entry_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += entry_len;
}
std::fs::write(&ap, &data[..offset]).unwrap();
let entries_after = read_audit_log(&ap).unwrap();
assert_eq!(entries_after.len(), original_count - 1);
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&ap, &audit_key).unwrap();
assert!(result.chain_valid);
assert_eq!(result.entries_verified, (original_count - 1) as u64);
let header_data = std::fs::read(&ap).unwrap();
let header_count = u64::from_le_bytes(header_data[24..32].try_into().unwrap());
assert_ne!(header_count as usize, entries_after.len());
}
#[test]
fn scenario_insertion_attack_detected() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
drop(db);
let ap = audit_path(dir.path());
let data = std::fs::read(&ap).unwrap();
let audit_key = get_audit_key(dir.path(), pass);
let result_before = verify_audit_log(&ap, &audit_key).unwrap();
assert!(result_before.chain_valid);
let count_before = result_before.entries_verified;
let fake_entry_len: u32 = 56;
let mut fake = Vec::new();
fake.extend_from_slice(&0x454E_5452u32.to_le_bytes());
fake.extend_from_slice(&fake_entry_len.to_le_bytes());
fake.extend_from_slice(&999u64.to_le_bytes());
fake.extend_from_slice(&(count_before + 1).to_le_bytes());
fake.extend_from_slice(&2u16.to_le_bytes());
fake.extend_from_slice(&0u16.to_le_bytes());
fake.extend_from_slice(&[0xAA; 32]);
let mut tampered = data.clone();
tampered.extend_from_slice(&fake);
std::fs::write(&ap, &tampered).unwrap();
let result = verify_audit_log(&ap, &audit_key).unwrap();
assert!(!result.chain_valid);
assert_eq!(result.entries_verified, count_before);
}
#[test]
fn scenario_multiple_sessions_chain_continuity() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
let mut wtx = db.begin_write().unwrap();
wtx.insert(b"key1", b"val1").unwrap();
wtx.commit().unwrap();
drop(db);
let db = open_test_db(dir.path(), pass);
db.integrity_check().unwrap();
drop(db);
let db = open_test_db(dir.path(), pass);
db.change_passphrase(pass, b"pass2").unwrap();
drop(db);
let pass = b"pass2";
let db = open_test_db(dir.path(), pass);
let backup_path = dir.path().join("backup.citadel");
db.backup(&backup_path).unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
for (i, entry) in entries.iter().enumerate() {
assert_eq!(entry.sequence_no, (i + 1) as u64);
}
let created = entries
.iter()
.filter(|e| e.event_type == AuditEventType::DatabaseCreated)
.count();
let opened = entries
.iter()
.filter(|e| e.event_type == AuditEventType::DatabaseOpened)
.count();
let closed = entries
.iter()
.filter(|e| e.event_type == AuditEventType::DatabaseClosed)
.count();
assert_eq!(created, 1);
assert_eq!(opened, 3);
assert_eq!(closed, 4);
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&audit_path(dir.path()), &audit_key).unwrap();
assert!(result.chain_valid);
assert_eq!(result.entries_verified, entries.len() as u64);
}
#[test]
fn scenario_corrupted_entry_recovery_via_sentinel() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
db.integrity_check().unwrap();
db.integrity_check().unwrap();
db.integrity_check().unwrap();
db.integrity_check().unwrap();
db.integrity_check().unwrap();
drop(db);
let ap = audit_path(dir.path());
let entries_before = read_audit_log(&ap).unwrap();
let total = entries_before.len();
assert!(total >= 7);
let mut data = std::fs::read(&ap).unwrap();
let mut offset = 64usize;
for _ in 0..2 {
offset += 4;
let entry_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += entry_len;
}
data[offset + 4] = 0xFF;
data[offset + 5] = 0xFF;
data[offset + 6] = 0xFF;
data[offset + 7] = 0x7F;
std::fs::write(&ap, &data).unwrap();
let strict_entries = read_audit_log(&ap).unwrap();
assert_eq!(strict_entries.len(), 2);
let scan = scan_corrupted_audit_log(&ap).unwrap();
assert!(scan.entries.len() > strict_entries.len());
assert!(!scan.corruption_offsets.is_empty());
let recovered_seq_nos: Vec<u64> = scan.entries.iter().map(|e| e.sequence_no).collect();
assert!(recovered_seq_nos.contains(&1));
assert!(recovered_seq_nos.contains(&2));
let recovered_past_gap = recovered_seq_nos.iter().filter(|&&s| s >= 4).count();
assert!(recovered_past_gap > 0);
}
#[test]
fn scenario_torn_write_partial_last_entry() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
db.integrity_check().unwrap();
drop(db);
let ap = audit_path(dir.path());
let entries = read_audit_log(&ap).unwrap();
let original_count = entries.len();
let data = std::fs::read(&ap).unwrap();
let truncated_len = data.len() - 10;
std::fs::write(&ap, &data[..truncated_len]).unwrap();
let entries_after = read_audit_log(&ap).unwrap();
assert_eq!(entries_after.len(), original_count - 1);
let audit_key = get_audit_key(dir.path(), pass);
let result = verify_audit_log(&ap, &audit_key).unwrap();
assert!(result.chain_valid);
assert_eq!(result.entries_verified, (original_count - 1) as u64);
}
#[test]
fn scenario_zeroed_magic_recovery() {
let dir = tempfile::tempdir().unwrap();
let pass = b"pass";
let db = create_test_db(dir.path(), pass);
db.integrity_check().unwrap();
db.integrity_check().unwrap();
db.integrity_check().unwrap();
drop(db);
let ap = audit_path(dir.path());
let entries = read_audit_log(&ap).unwrap();
let total = entries.len();
assert!(total >= 5);
let mut data = std::fs::read(&ap).unwrap();
let mut offset = 64usize;
offset += 4;
let entry_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += entry_len;
data[offset] = 0;
data[offset + 1] = 0;
data[offset + 2] = 0;
data[offset + 3] = 0;
std::fs::write(&ap, &data).unwrap();
let strict = read_audit_log(&ap).unwrap();
assert_eq!(strict.len(), 1);
let scan = scan_corrupted_audit_log(&ap).unwrap();
assert!(scan.entries.len() > strict.len());
assert!(!scan.corruption_offsets.is_empty());
let recovered_seq: Vec<u64> = scan.entries.iter().map(|e| e.sequence_no).collect();
for seq in 3..=total as u64 {
assert!(
recovered_seq.contains(&seq),
"entry {} should be recovered",
seq
);
}
}
#[test]
fn scenario_entry_sentinel_on_disk() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
drop(db);
let ap = audit_path(dir.path());
let data = std::fs::read(&ap).unwrap();
let magic = u32::from_le_bytes(data[64..68].try_into().unwrap());
assert_eq!(magic, 0x454E_5452);
let entry_len = u32::from_le_bytes(data[68..72].try_into().unwrap()) as usize;
let second_offset = 64 + 4 + entry_len;
let magic2 = u32::from_le_bytes(data[second_offset..second_offset + 4].try_into().unwrap());
assert_eq!(magic2, 0x454E_5452);
}
#[test]
fn scenario_path_detail_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let db = create_test_db(dir.path(), b"pass");
let backup_path = dir.path().join("my-backup.citadel");
db.backup(&backup_path).unwrap();
drop(db);
let entries = read_audit_log(&audit_path(dir.path())).unwrap();
let backup_entry = entries
.iter()
.find(|e| e.event_type == AuditEventType::BackupCreated)
.unwrap();
assert!(backup_entry.detail.len() >= 2);
let path_len = u16::from_le_bytes(backup_entry.detail[0..2].try_into().unwrap()) as usize;
assert_eq!(path_len, backup_entry.detail.len() - 2);
let path_str = std::str::from_utf8(&backup_entry.detail[2..]).unwrap();
assert!(path_str.contains("my-backup.citadel"));
}