use std::fs::{self, File}; use std::io::Write; use std::path::Path; use sanctum::crypto::{ derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, CHUNK_SIZE, FORMAT_VERSION, }; use sanctum::storage::Database; use sanctum::sync::{run_sync, SyncDirection, SyncOptions}; fn create_test_container(path: &Path) -> (Database, [u8; 32]) { if path.exists() { let _ = fs::remove_file(path); } let db = Database::open(path).expect("Open database"); let salt = generate_salt(); let kdf_params = KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1, }; let kek = derive_kek("sync_password", &salt, &kdf_params).unwrap(); let dek = generate_dek(); let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap(); db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).unwrap(); (db, *dek) } #[test] fn test_sync_push_and_pull_basic() { let temp_root = std::env::temp_dir().join(format!("sanctum_sync_test_{}", rand::random::())); let container_path = temp_root.join("test.sanctum"); let source_dir = temp_root.join("source"); let target_dir = temp_root.join("restored"); fs::create_dir_all(&source_dir).unwrap(); fs::create_dir_all(source_dir.join("sub")).unwrap(); // Testdateien anlegen fs::write(source_dir.join("file1.txt"), b"Hello Sanctum Sync!").unwrap(); fs::write(source_dir.join("sub").join("file2.bin"), vec![0x42u8; 100_000]).unwrap(); let (db, dek) = create_test_container(&container_path); // 1. Push let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Backup", &opts, ).expect("Sync push"); assert_eq!(stats.files_scanned, 2); assert_eq!(stats.files_transferred, 2); assert_eq!(stats.files_skipped, 0); // 2. Erneuter Push (Fast check / Delta): Muss 0 übertragen, 2 überspringen let stats_delta = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Backup", &opts, ).expect("Sync push delta"); assert_eq!(stats_delta.files_transferred, 0); assert_eq!(stats_delta.files_skipped, 2); // 3. Dry-Run mit neuer Datei fs::write(source_dir.join("new_file.txt"), b"Brand new file").unwrap(); let mut dry_opts = opts.clone(); dry_opts.dry_run = true; let stats_dry = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Backup", &dry_opts, ).expect("Sync push dry-run"); assert_eq!(stats_dry.files_transferred, 1); assert_eq!(stats_dry.files_skipped, 2); // Verifizieren, dass new_file.txt im Tresor tatsächlich NICHT existiert let node = db.resolve_path_in_vault("/Backup/new_file.txt", 0, &dek).unwrap(); assert!(node.is_none()); // 4. Pull let mut pull_opts = SyncOptions::default(); pull_opts.direction = SyncDirection::Pull; pull_opts.quiet = true; let stats_pull = run_sync( &db, 0, &dek, FORMAT_VERSION, "/Backup", target_dir.to_str().unwrap(), &pull_opts, ).expect("Sync pull"); assert_eq!(stats_pull.files_transferred, 2); // Inhalt vergleichen let c1 = fs::read(target_dir.join("file1.txt")).unwrap(); assert_eq!(c1, b"Hello Sanctum Sync!"); let c2 = fs::read(target_dir.join("sub").join("file2.bin")).unwrap(); assert_eq!(c2, vec![0x42u8; 100_000]); // Aufräumen let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_sync_multi_megabyte_large_file() { let temp_root = std::env::temp_dir().join(format!("sanctum_sync_large_{}", rand::random::())); let container_path = temp_root.join("test_large.sanctum"); let source_dir = temp_root.join("source"); let target_dir = temp_root.join("restored"); fs::create_dir_all(&source_dir).unwrap(); // 3.5 MB große Datei erzeugen (geht über 4 Chunks: 0, 1, 2, 3) let large_file_path = source_dir.join("large_payload.bin"); let mut large_file = File::create(&large_file_path).unwrap(); let chunk_sample = vec![0xA5u8; CHUNK_SIZE]; for _ in 0..3 { large_file.write_all(&chunk_sample).unwrap(); } large_file.write_all(&vec![0x5Au8; 512 * 1024]).unwrap(); // 3.5 MB large_file.flush().unwrap(); drop(large_file); let (db, dek) = create_test_container(&container_path); // Push let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.quiet = true; let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/LargeTest", &opts, ).expect("Sync push large"); assert_eq!(stats.files_transferred, 1); assert_eq!(stats.bytes_transferred, 3 * (CHUNK_SIZE as u64) + 512 * 1024); // Pull let mut pull_opts = SyncOptions::default(); pull_opts.direction = SyncDirection::Pull; pull_opts.quiet = true; run_sync( &db, 0, &dek, FORMAT_VERSION, "/LargeTest", target_dir.to_str().unwrap(), &pull_opts, ).expect("Sync pull large"); let restored = fs::read(target_dir.join("large_payload.bin")).unwrap(); let original = fs::read(&large_file_path).unwrap(); assert_eq!(restored.len(), original.len()); assert_eq!(restored, original); let _ = fs::remove_dir_all(&temp_root); } #[test] fn test_sync_delete_and_exclude_flags() { let temp_root = std::env::temp_dir().join(format!("sanctum_sync_flags_{}", rand::random::())); let container_path = temp_root.join("test_flags.sanctum"); let source_dir = temp_root.join("source"); fs::create_dir_all(&source_dir).unwrap(); fs::write(source_dir.join("keep.txt"), b"Keep this").unwrap(); fs::write(source_dir.join("remove_me.txt"), b"Will be deleted later").unwrap(); fs::write(source_dir.join("ignore.tmp"), b"Temporary file").unwrap(); let (db, dek) = create_test_container(&container_path); let mut opts = SyncOptions::default(); opts.direction = SyncDirection::Push; opts.exclude_patterns = vec!["*.tmp".to_string()]; opts.quiet = true; // 1. Initialer Push mit Exclude let stats = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Files", &opts, ).unwrap(); assert_eq!(stats.files_transferred, 2); // keep.txt und remove_me.txt assert!(db.resolve_path_in_vault("/Files/ignore.tmp", 0, &dek).unwrap().is_none()); // 2. Lokale Datei löschen und Sync mit --delete ausführen fs::remove_file(source_dir.join("remove_me.txt")).unwrap(); opts.delete = true; let stats_del = run_sync( &db, 0, &dek, FORMAT_VERSION, source_dir.to_str().unwrap(), "/Files", &opts, ).unwrap(); assert_eq!(stats_del.files_deleted, 1); assert!(db.resolve_path_in_vault("/Files/remove_me.txt", 0, &dek).unwrap().is_none()); assert!(db.resolve_path_in_vault("/Files/keep.txt", 0, &dek).unwrap().is_some()); let _ = fs::remove_dir_all(&temp_root); }