1use chamber_import_export::{ExportFormat, export_items};
2use chamber_vault::{BackupConfig, Item, Vault};
3use color_eyre::Result;
4use color_eyre::eyre::eyre;
5use std::fs;
6use std::path::{Path, PathBuf};
7use time::OffsetDateTime;
8
9pub trait VaultOperations {
10 fn list_items(&self) -> Result<Vec<Item>>;
21}
22
23impl VaultOperations for Vault {
24 fn list_items(&self) -> Result<Vec<Item>> {
25 self.list_items()
26 }
27}
28
29pub struct BackupManager<V: VaultOperations> {
30 pub config: BackupConfig,
31 vault: V,
32}
33
34impl<V: VaultOperations> BackupManager<V> {
35 pub const fn new(vault: V, config: BackupConfig) -> Self {
36 Self { config, vault }
37 }
38
39 pub fn backup_if_needed(&mut self) -> Result<Option<PathBuf>> {
55 if !self.config.enabled {
56 return Ok(None);
57 }
58
59 if !self.should_backup()? {
60 return Ok(None);
61 }
62
63 self.perform_backup()
64 }
65
66 #[allow(clippy::unwrap_in_result)]
89 #[allow(clippy::expect_used)]
90 pub fn force_backup(&mut self) -> Result<PathBuf> {
91 self.perform_backup().map(|opt| opt.expect("Backup failed to perform"))
92 }
93
94 fn should_backup(&self) -> Result<bool> {
95 if !self.config.backup_dir.exists() {
97 fs::create_dir_all(&self.config.backup_dir)?;
98 return Ok(true); }
100
101 let most_recent = self.find_most_recent_backup()?;
103
104 if let Some(recent_path) = most_recent {
105 if let Some(timestamp) = self.extract_timestamp_from_filename(&recent_path) {
107 let now = OffsetDateTime::now_utc();
108 let duration_since = now - timestamp;
109 #[allow(clippy::cast_possible_wrap)]
110 let interval = time::Duration::hours(self.config.interval_hours as i64);
111
112 return Ok(duration_since >= interval);
113 }
114 }
115
116 Ok(true) }
118
119 fn perform_backup(&mut self) -> Result<Option<PathBuf>> {
120 fs::create_dir_all(&self.config.backup_dir)?;
122
123 let timestamp = OffsetDateTime::now_utc();
125 let filename = self.generate_backup_filename(×tamp)?;
126 let backup_path = self.config.backup_dir.join(&filename);
127
128 let items = self.vault.list_items()?;
130
131 let export_format = match self.config.format.as_str() {
132 "json" => ExportFormat::Json,
133 "csv" => ExportFormat::Csv,
134 "backup" => ExportFormat::ChamberBackup,
135 _ => return Err(eyre!("Invalid backup format: {}", self.config.format)),
136 };
137
138 export_items(&items, &export_format, &backup_path)?;
140
141 let final_path = if self.config.compress {
143 Self::compress_backup(&backup_path)?
144 } else {
145 backup_path
146 };
147
148 if self.config.verify_after_backup {
150 self.verify_backup(&final_path)?;
151 }
152
153 self.cleanup_old_backups()?;
155
156 Ok(Some(final_path))
157 }
158
159 fn generate_backup_filename(&self, timestamp: &OffsetDateTime) -> Result<String> {
160 let date_str = timestamp.format(&time::format_description::well_known::Rfc3339)?;
161 let safe_date = date_str.replace(':', "-").replace('T', "_");
162
163 let extension = if self.config.compress {
164 format!("{}.gz", self.config.format)
165 } else {
166 self.config.format.clone()
167 };
168
169 Ok(format!(
170 "chamber_backup_{}_{}.{}",
171 safe_date,
172 timestamp.unix_timestamp(),
173 extension
174 ))
175 }
176
177 fn compress_backup(path: &Path) -> Result<PathBuf> {
178 use std::fs::File;
179 use std::io::BufReader;
180
181 let compressed_path =
182 path.with_extension(format!("{}.gz", path.extension().unwrap_or_default().to_string_lossy()));
183
184 let input = File::open(path)?;
185 let output = File::create(&compressed_path)?;
186
187 let mut encoder = flate2::write::GzEncoder::new(output, flate2::Compression::default());
188 let mut reader = BufReader::new(input);
189
190 std::io::copy(&mut reader, &mut encoder)?;
191 encoder.finish()?;
192
193 fs::remove_file(path)?;
195
196 Ok(compressed_path)
197 }
198
199 fn verify_backup(&self, path: &Path) -> Result<()> {
200 let metadata = fs::metadata(path)?;
202 if metadata.len() == 0 {
203 return Err(eyre!("Backup file is empty: {}", path.display()));
204 }
205
206 if path.extension().and_then(|s| s.to_str()) == Some("gz") {
208 Self::verify_compressed_backup(path)?;
209 } else {
210 self.verify_uncompressed_backup(path)?;
212 }
213
214 Ok(())
215 }
216
217 fn verify_compressed_backup(path: &Path) -> Result<()> {
218 use std::fs::File;
219 use std::io::Read;
220
221 let file = File::open(path)?;
222 let mut decoder = flate2::read::GzDecoder::new(file);
223 let mut buffer = [0; 1024];
224
225 let _ = decoder.read(&mut buffer)?;
227 Ok(())
228 }
229
230 fn verify_uncompressed_backup(&self, path: &Path) -> Result<()> {
231 let content = fs::read_to_string(path)?;
232
233 match self.config.format.as_str() {
234 "json" => {
235 serde_json::from_str::<serde_json::Value>(&content)?;
236 }
237 "backup" => {
238 serde_json::from_str::<chamber_import_export::ChamberBackup>(&content)?;
240 }
241 "csv" => {
242 if !content.starts_with("name,kind,value") {
244 return Err(eyre!("Invalid CSV backup format"));
245 }
246 }
247 _ => return Err(eyre!("Unknown backup format for verification")),
248 }
249
250 Ok(())
251 }
252
253 fn cleanup_old_backups(&self) -> Result<()> {
254 let mut backups = self.find_all_backups()?;
255
256 if backups.len() <= self.config.max_backups {
257 return Ok(());
258 }
259
260 backups.sort_by(|a, b| {
262 let time_a = self
263 .extract_timestamp_from_filename(a)
264 .unwrap_or(OffsetDateTime::UNIX_EPOCH);
265 let time_b = self
266 .extract_timestamp_from_filename(b)
267 .unwrap_or(OffsetDateTime::UNIX_EPOCH);
268 time_b.cmp(&time_a)
269 });
270
271 for old_backup in backups.iter().skip(self.config.max_backups) {
273 if let Err(e) = fs::remove_file(old_backup) {
274 eprintln!("Warning: Failed to remove old backup {}: {}", old_backup.display(), e);
275 }
276 }
277
278 Ok(())
279 }
280
281 pub fn find_all_backups(&self) -> Result<Vec<PathBuf>> {
305 let mut backups = Vec::new();
306
307 if !self.config.backup_dir.exists() {
308 return Ok(backups);
309 }
310
311 for entry in fs::read_dir(&self.config.backup_dir)? {
312 let entry = entry?;
313 let path = entry.path();
314
315 if path.is_file() && Self::is_backup_file(&path) {
316 backups.push(path);
317 }
318 }
319
320 Ok(backups)
321 }
322
323 pub fn find_most_recent_backup(&self) -> Result<Option<PathBuf>> {
347 let backups = self.find_all_backups()?;
348
349 let mut most_recent = None;
350 let mut most_recent_time = OffsetDateTime::UNIX_EPOCH;
351
352 for backup in backups {
353 if let Some(timestamp) = self.extract_timestamp_from_filename(&backup) {
354 if timestamp > most_recent_time {
355 most_recent_time = timestamp;
356 most_recent = Some(backup);
357 }
358 }
359 }
360
361 Ok(most_recent)
362 }
363
364 fn is_backup_file(path: &Path) -> bool {
365 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
366 filename.starts_with("chamber_backup_")
367 } else {
368 false
369 }
370 }
371
372 pub fn extract_timestamp_from_filename(&self, path: &Path) -> Option<OffsetDateTime> {
373 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
374 if let Some(timestamp_part) = filename.split('_').nth(4) {
377 if let Some(timestamp_str) = timestamp_part.split('.').next() {
378 if let Ok(timestamp) = timestamp_str.parse::<i64>() {
379 return OffsetDateTime::from_unix_timestamp(timestamp).ok();
380 }
381 }
382 }
383 }
384 None
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 #![allow(clippy::unwrap_used)]
391 #![allow(clippy::panic)]
392 use super::*;
393 use chamber_vault::{BackupConfig, Item, ItemKind};
394 use color_eyre::Result;
395 use std::fs;
396 use tempfile::TempDir;
397 use time::OffsetDateTime;
398
399 struct MockVault {
401 items: Vec<Item>,
402 should_fail: bool,
403 }
404
405 impl MockVault {
406 fn new(items: Vec<Item>) -> Self {
407 Self {
408 items,
409 should_fail: false,
410 }
411 }
412
413 fn new_failing() -> Self {
414 Self {
415 items: vec![],
416 should_fail: true,
417 }
418 }
419 }
420
421 impl VaultOperations for MockVault {
422 fn list_items(&self) -> Result<Vec<Item>> {
423 if self.should_fail {
424 return Err(eyre!("Mock vault error"));
425 }
426 Ok(self.items.clone())
427 }
428 }
429
430 fn create_test_item(id: u64, name: &str) -> Item {
431 Item {
432 id,
433 name: name.to_string(),
434 kind: ItemKind::Password,
435 value: "test_value".to_string(),
436 created_at: OffsetDateTime::now_utc(),
437 updated_at: OffsetDateTime::now_utc(),
438 }
439 }
440
441 fn create_test_config(temp_dir: &TempDir) -> BackupConfig {
442 BackupConfig {
443 enabled: true,
444 backup_dir: temp_dir.path().join("backups"),
445 format: "json".to_string(),
446 compress: false,
447 interval_hours: 24,
448 max_backups: 5,
449 verify_after_backup: false, }
451 }
452
453 fn create_test_config_with_options(
454 temp_dir: &TempDir,
455 format: &str,
456 compress: bool,
457 verify: bool,
458 max_backups: usize,
459 interval_hours: u64,
460 ) -> BackupConfig {
461 BackupConfig {
462 enabled: true,
463 backup_dir: temp_dir.path().join("backups"),
464 format: format.to_string(),
465 compress,
466 interval_hours,
467 max_backups,
468 verify_after_backup: verify,
469 }
470 }
471
472 #[test]
473 fn test_generic_backup_manager_creation() {
474 let temp_dir = TempDir::new().unwrap();
475 let config = create_test_config(&temp_dir);
476 let items = vec![create_test_item(1, "test_item")];
477 let vault = MockVault::new(items);
478
479 let manager = BackupManager::new(vault, config.clone());
480
481 assert_eq!(manager.config.enabled, config.enabled);
482 assert_eq!(manager.config.format, config.format);
483 assert_eq!(manager.config.max_backups, config.max_backups);
484 }
485
486 #[test]
487 fn test_vault_operations_trait() {
488 let items = vec![create_test_item(1, "test_item_1"), create_test_item(2, "test_item_2")];
489 let vault = MockVault::new(items);
490
491 let result = vault.list_items().unwrap();
492 assert_eq!(result.len(), 2);
493 assert_eq!(result[0].name, "test_item_1");
494 assert_eq!(result[1].name, "test_item_2");
495
496 assert_eq!(result[0].kind, ItemKind::Password);
498 assert_eq!(result[0].value, "test_value");
499 }
500
501 #[test]
502 fn test_vault_operations_failure() {
503 let vault = MockVault::new_failing();
504
505 let result = vault.list_items();
506 assert!(result.is_err());
507 assert!(result.unwrap_err().to_string().contains("Mock vault error"));
508 }
509
510 #[test]
511 fn test_backup_if_needed_disabled_with_generic() {
512 let temp_dir = TempDir::new().unwrap();
513 let mut config = create_test_config(&temp_dir);
514 config.enabled = false;
515 let items = vec![create_test_item(1, "test_item")];
516 let vault = MockVault::new(items);
517
518 let mut manager = BackupManager::new(vault, config);
519 let result = manager.backup_if_needed().unwrap();
520
521 assert!(result.is_none());
522 }
523
524 #[test]
525 fn test_should_backup_first_time() {
526 let temp_dir = TempDir::new().unwrap();
527 let config = create_test_config(&temp_dir);
528 let items = vec![create_test_item(1, "test_item")];
529 let vault = MockVault::new(items);
530
531 let manager = BackupManager::new(vault, config);
532
533 assert!(manager.should_backup().unwrap());
535 }
536
537 #[test]
538 fn test_find_all_backups_empty_directory() {
539 let temp_dir = TempDir::new().unwrap();
540 let config = create_test_config(&temp_dir);
541 let vault = MockVault::new(vec![create_test_item(1, "test_item")]);
542 let manager = BackupManager::new(vault, config);
543
544 let backups = manager.find_all_backups().unwrap();
545 assert!(backups.is_empty());
546 }
547
548 #[test]
549 fn test_find_all_backups_with_files() {
550 let temp_dir = TempDir::new().unwrap();
551 let backup_dir = temp_dir.path().join("backups");
552 fs::create_dir_all(&backup_dir).unwrap();
553
554 let backup1 = backup_dir.join("chamber_backup_2024-01-01_00-00-00Z_1640995200.json");
556 let backup2 = backup_dir.join("chamber_backup_2024-01-02_00-00-00Z_1641081600.json");
557
558 let invalid_file = backup_dir.join("not_a_backup.txt");
560
561 fs::write(&backup1, "backup1 content").unwrap();
562 fs::write(&backup2, "backup2 content").unwrap();
563 fs::write(&invalid_file, "invalid content").unwrap();
564
565 let config = create_test_config(&temp_dir);
566 let vault = MockVault::new(vec![create_test_item(1, "test_item")]);
567 let manager = BackupManager::new(vault, config);
568
569 let backups = manager.find_all_backups().unwrap();
570 assert_eq!(backups.len(), 2);
571 assert!(backups.contains(&backup1));
572 assert!(backups.contains(&backup2));
573 assert!(!backups.iter().any(|p| p == &invalid_file));
574 }
575
576 #[test]
577 fn test_concrete_item_usage() {
578 let items = vec![
580 Item {
581 id: 1,
582 name: "password_item".to_string(),
583 kind: ItemKind::Password,
584 value: "secret123".to_string(),
585 created_at: OffsetDateTime::now_utc(),
586 updated_at: OffsetDateTime::now_utc(),
587 },
588 Item {
589 id: 2,
590 name: "api_key_item".to_string(),
591 kind: ItemKind::ApiKey,
592 value: "api_key_abc".to_string(),
593 created_at: OffsetDateTime::now_utc(),
594 updated_at: OffsetDateTime::now_utc(),
595 },
596 ];
597
598 let vault = MockVault::new(items);
599 let result = vault.list_items().unwrap();
600
601 assert_eq!(result[0].name, "password_item");
603 assert_eq!(result[0].kind, ItemKind::Password);
604 assert_eq!(result[0].value, "secret123");
605 assert!(result[0].id > 0);
606
607 assert_eq!(result[1].name, "api_key_item");
608 assert_eq!(result[1].kind, ItemKind::ApiKey);
609 assert_eq!(result[1].value, "api_key_abc");
610 }
611
612 #[test]
613 fn test_trait_object_compatibility() {
614 let temp_dir = TempDir::new().unwrap();
615 let _ = create_test_config(&temp_dir);
616
617 let vault: Box<dyn VaultOperations> = Box::new(MockVault::new(vec![create_test_item(1, "test")]));
619 let items = vault.list_items().unwrap();
620 assert_eq!(items.len(), 1);
621 assert_eq!(items[0].name, "test");
622 }
623
624 struct AlternativeVault {
626 data: Vec<Item>,
627 }
628
629 impl AlternativeVault {
630 fn new(data: Vec<Item>) -> Self {
631 Self { data }
632 }
633 }
634
635 impl VaultOperations for AlternativeVault {
636 fn list_items(&self) -> Result<Vec<Item>> {
637 Ok(self.data.clone())
638 }
639 }
640
641 #[test]
642 fn test_multiple_vault_implementations() {
643 let temp_dir1 = TempDir::new().unwrap();
644 let temp_dir2 = TempDir::new().unwrap();
645
646 let items = vec![create_test_item(1, "shared_item")];
647
648 let mock_vault = MockVault::new(items.clone());
650 let alt_vault = AlternativeVault::new(items);
651
652 let manager1 = BackupManager::new(mock_vault, create_test_config(&temp_dir1));
653 let manager2 = BackupManager::new(alt_vault, create_test_config(&temp_dir2));
654
655 let items1 = manager1.vault.list_items().unwrap();
657 let items2 = manager2.vault.list_items().unwrap();
658
659 assert_eq!(items1[0].name, items2[0].name);
660 assert_eq!(items1[0].kind, items2[0].kind);
661 assert_eq!(items1[0].value, items2[0].value);
662 }
663
664 #[test]
665 fn test_generate_backup_filename() {
666 let temp_dir = TempDir::new().unwrap();
667 let config = create_test_config(&temp_dir);
668 let vault = MockVault::new(vec![]);
669 let manager = BackupManager::new(vault, config);
670
671 let timestamp = OffsetDateTime::from_unix_timestamp(1_640_995_200).unwrap(); let filename = manager.generate_backup_filename(×tamp).unwrap();
673
674 assert!(filename.starts_with("chamber_backup_"));
675 assert!(filename.contains("_1640995200"));
676 assert!(
677 Path::new(&filename)
678 .extension()
679 .is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
680 );
681 }
682
683 #[test]
684 fn test_generate_backup_filename_with_compression() {
685 let temp_dir = TempDir::new().unwrap();
686 let config = create_test_config_with_options(&temp_dir, "json", true, false, 5, 24);
687 let vault = MockVault::new(vec![]);
688 let manager = BackupManager::new(vault, config);
689
690 let timestamp = OffsetDateTime::from_unix_timestamp(1_640_995_200).unwrap();
691 let filename = manager.generate_backup_filename(×tamp).unwrap();
692
693 assert!(filename.starts_with("chamber_backup_"));
694 assert!(filename.ends_with(".json.gz"));
695 }
696
697 #[test]
698 fn test_generate_backup_filename_different_formats() {
699 let temp_dir = TempDir::new().unwrap();
700
701 for format in ["json", "csv", "backup"] {
702 let config = create_test_config_with_options(&temp_dir, format, false, false, 5, 24);
703 let vault = MockVault::new(vec![]);
704 let manager = BackupManager::new(vault, config);
705
706 let timestamp = OffsetDateTime::from_unix_timestamp(1_640_995_200).unwrap();
707 let filename = manager.generate_backup_filename(×tamp).unwrap();
708
709 assert!(filename.ends_with(&format!(".{format}")));
710 }
711 }
712
713 #[test]
714 fn test_extract_timestamp_from_filename() {
715 let temp_dir = TempDir::new().unwrap();
716 let config = create_test_config(&temp_dir);
717 let vault = MockVault::new(vec![]);
718 let manager = BackupManager::new(vault, config);
719
720 let timestamp = OffsetDateTime::from_unix_timestamp(1_640_995_200).unwrap();
722 let generated_filename = manager.generate_backup_filename(×tamp).unwrap();
723 let test_path = temp_dir.path().join(&generated_filename);
724
725 let extracted_timestamp = manager.extract_timestamp_from_filename(&test_path);
726
727 assert!(extracted_timestamp.is_some());
728 assert_eq!(extracted_timestamp.unwrap().unix_timestamp(), 1_640_995_200);
729 }
730
731 #[test]
732 fn test_extract_timestamp_from_invalid_filename() {
733 let temp_dir = TempDir::new().unwrap();
734 let config = create_test_config(&temp_dir);
735 let vault = MockVault::new(vec![]);
736 let manager = BackupManager::new(vault, config);
737
738 let test_path = temp_dir.path().join("invalid_filename.json");
739 let timestamp = manager.extract_timestamp_from_filename(&test_path);
740
741 assert!(timestamp.is_none());
742 }
743
744 #[test]
745 fn test_is_backup_file() {
746 assert!(BackupManager::<MockVault>::is_backup_file(Path::new(
747 "chamber_backup_2024-01-01_00-00-00Z_1640995200.json"
748 )));
749
750 assert!(!BackupManager::<MockVault>::is_backup_file(Path::new(
751 "not_a_backup.json"
752 )));
753
754 assert!(!BackupManager::<MockVault>::is_backup_file(Path::new(
755 "chamber_2024-01-01.json"
756 )));
757 }
758
759 #[test]
760 fn test_find_most_recent_backup() {
761 let temp_dir = TempDir::new().unwrap();
762 let backup_dir = temp_dir.path().join("backups");
763 fs::create_dir_all(&backup_dir).unwrap();
764
765 let backup1 = backup_dir.join("chamber_backup_2024-01-01_00-00-00Z_1640995200.json");
767 let backup2 = backup_dir.join("chamber_backup_2024-01-02_00-00-00Z_1641081600.json");
768 let backup3 = backup_dir.join("chamber_backup_2024-01-03_00-00-00Z_1641168000.json");
769
770 fs::write(&backup1, "backup1").unwrap();
771 fs::write(&backup2, "backup2").unwrap();
772 fs::write(&backup3, "backup3").unwrap();
773
774 let config = create_test_config(&temp_dir);
775 let vault = MockVault::new(vec![]);
776 let manager = BackupManager::new(vault, config);
777
778 let most_recent = manager.find_most_recent_backup().unwrap();
779 assert!(most_recent.is_some());
780 assert_eq!(most_recent.unwrap(), backup3);
781 }
782
783 #[test]
784 fn test_find_most_recent_backup_empty() {
785 let temp_dir = TempDir::new().unwrap();
786 let config = create_test_config(&temp_dir);
787 let vault = MockVault::new(vec![]);
788 let manager = BackupManager::new(vault, config);
789
790 let most_recent = manager.find_most_recent_backup().unwrap();
791 assert!(most_recent.is_none());
792 }
793
794 #[test]
795 fn test_should_backup_with_recent_backup() {
796 let temp_dir = TempDir::new().unwrap();
797 let backup_dir = temp_dir.path().join("backups");
798 fs::create_dir_all(&backup_dir).unwrap();
799
800 let now = OffsetDateTime::now_utc();
802 let timestamp = now.unix_timestamp();
803 let recent_backup = backup_dir.join(format!("chamber_backup_2024-01-01_00-00-00Z_{timestamp}.json"));
804 fs::write(&recent_backup, "recent backup").unwrap();
805
806 let config = create_test_config(&temp_dir);
807 let vault = MockVault::new(vec![]);
808 let manager = BackupManager::new(vault, config);
809
810 assert!(!manager.should_backup().unwrap());
812 }
813
814 #[test]
815 fn test_should_backup_with_old_backup() {
816 let temp_dir = TempDir::new().unwrap();
817 let backup_dir = temp_dir.path().join("backups");
818 fs::create_dir_all(&backup_dir).unwrap();
819
820 let old_time = OffsetDateTime::now_utc() - time::Duration::hours(25);
822 let timestamp = old_time.unix_timestamp();
823 let old_backup = backup_dir.join(format!("chamber_backup_2024-01-01_00-00-00Z_{timestamp}.json"));
824 fs::write(&old_backup, "old backup").unwrap();
825
826 let config = create_test_config(&temp_dir);
827 let vault = MockVault::new(vec![]);
828 let manager = BackupManager::new(vault, config);
829
830 assert!(manager.should_backup().unwrap());
832 }
833
834 #[test]
835 fn test_compress_backup() {
836 let temp_dir = TempDir::new().unwrap();
837 let test_file = temp_dir.path().join("test.json");
838 let test_content = r#"{"test": "data", "items": [1, 2, 3]}"#;
839
840 fs::write(&test_file, test_content).unwrap();
841
842 let compressed_path = BackupManager::<MockVault>::compress_backup(&test_file).unwrap();
843
844 assert!(compressed_path.extension().unwrap() == "gz");
845 assert!(compressed_path.exists());
846 assert!(!test_file.exists()); let metadata = fs::metadata(&compressed_path).unwrap();
850 assert!(metadata.len() > 0);
851 }
852
853 #[test]
854 fn test_verify_compressed_backup() {
855 let temp_dir = TempDir::new().unwrap();
856 let test_file = temp_dir.path().join("test.json");
857 let test_content = r#"{"test": "data"}"#;
858
859 fs::write(&test_file, test_content).unwrap();
860 let compressed_path = BackupManager::<MockVault>::compress_backup(&test_file).unwrap();
861
862 let result = BackupManager::<MockVault>::verify_compressed_backup(&compressed_path);
864 assert!(result.is_ok());
865 }
866
867 #[test]
868 fn test_verify_compressed_backup_invalid_file() {
869 let temp_dir = TempDir::new().unwrap();
870 let invalid_gz = temp_dir.path().join("invalid.gz");
871 fs::write(&invalid_gz, "not gzip data").unwrap();
872
873 let result = BackupManager::<MockVault>::verify_compressed_backup(&invalid_gz);
874 assert!(result.is_err());
875 }
876
877 #[test]
878 fn test_verify_uncompressed_backup_json() {
879 let temp_dir = TempDir::new().unwrap();
880 let config = create_test_config_with_options(&temp_dir, "json", false, false, 5, 24);
881 let vault = MockVault::new(vec![]);
882 let manager = BackupManager::new(vault, config);
883
884 let test_file = temp_dir.path().join("test.json");
885 let valid_json = r#"{"items": [{"name": "test", "value": "data"}]}"#;
886 fs::write(&test_file, valid_json).unwrap();
887
888 let result = manager.verify_uncompressed_backup(&test_file);
889 assert!(result.is_ok());
890 }
891
892 #[test]
893 fn test_verify_uncompressed_backup_invalid_json() {
894 let temp_dir = TempDir::new().unwrap();
895 let config = create_test_config_with_options(&temp_dir, "json", false, false, 5, 24);
896 let vault = MockVault::new(vec![]);
897 let manager = BackupManager::new(vault, config);
898
899 let test_file = temp_dir.path().join("test.json");
900 fs::write(&test_file, "invalid json content").unwrap();
901
902 let result = manager.verify_uncompressed_backup(&test_file);
903 assert!(result.is_err());
904 }
905
906 #[test]
907 fn test_verify_uncompressed_backup_csv() {
908 let temp_dir = TempDir::new().unwrap();
909 let config = create_test_config_with_options(&temp_dir, "csv", false, false, 5, 24);
910 let vault = MockVault::new(vec![]);
911 let manager = BackupManager::new(vault, config);
912
913 let test_file = temp_dir.path().join("test.csv");
914 let valid_csv = "name,kind,value\ntest,password,secret";
915 fs::write(&test_file, valid_csv).unwrap();
916
917 let result = manager.verify_uncompressed_backup(&test_file);
918 assert!(result.is_ok());
919 }
920
921 #[test]
922 fn test_verify_uncompressed_backup_invalid_csv() {
923 let temp_dir = TempDir::new().unwrap();
924 let config = create_test_config_with_options(&temp_dir, "csv", false, false, 5, 24);
925 let vault = MockVault::new(vec![]);
926 let manager = BackupManager::new(vault, config);
927
928 let test_file = temp_dir.path().join("test.csv");
929 fs::write(&test_file, "invalid csv header").unwrap();
930
931 let result = manager.verify_uncompressed_backup(&test_file);
932 assert!(result.is_err());
933 }
934
935 #[test]
936 fn test_verify_backup_empty_file() {
937 let temp_dir = TempDir::new().unwrap();
938 let config = create_test_config(&temp_dir);
939 let vault = MockVault::new(vec![]);
940 let manager = BackupManager::new(vault, config);
941
942 let empty_file = temp_dir.path().join("empty.json");
943 fs::write(&empty_file, "").unwrap();
944
945 let result = manager.verify_backup(&empty_file);
946 assert!(result.is_err());
947 assert!(result.unwrap_err().to_string().contains("empty"));
948 }
949
950 #[test]
951 fn test_cleanup_old_backups() {
952 let temp_dir = TempDir::new().unwrap();
953 let backup_dir = temp_dir.path().join("backups");
954 fs::create_dir_all(&backup_dir).unwrap();
955
956 let backups = [
958 ("chamber_backup_2024-01-01_00-00-00Z_1640995200.json", 1_640_995_200),
959 ("chamber_backup_2024-01-02_00-00-00Z_1641081600.json", 1_641_081_600),
960 ("chamber_backup_2024-01-03_00-00-00Z_1641168000.json", 1_641_168_000),
961 ("chamber_backup_2024-01-04_00-00-00Z_1641254400.json", 1_641_254_400),
962 ("chamber_backup_2024-01-05_00-00-00Z_1641340800.json", 1_641_340_800),
963 ("chamber_backup_2024-01-06_00-00-00Z_1641427200.json", 1_641_427_200),
964 ("chamber_backup_2024-01-07_00-00-00Z_1641513600.json", 1_641_513_600),
965 ];
966
967 for (filename, _) in &backups {
968 let path = backup_dir.join(filename);
969 fs::write(&path, "backup content").unwrap();
970 }
971
972 let config = create_test_config_with_options(&temp_dir, "json", false, false, 3, 24);
973 let vault = MockVault::new(vec![]);
974 let manager = BackupManager::new(vault, config);
975
976 let result = manager.cleanup_old_backups();
977 assert!(result.is_ok());
978
979 let remaining_backups = manager.find_all_backups().unwrap();
981 assert_eq!(remaining_backups.len(), 3);
982
983 let filenames: Vec<String> = remaining_backups
985 .iter()
986 .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
987 .collect();
988
989 assert!(filenames.contains(&"chamber_backup_2024-01-07_00-00-00Z_1641513600.json".to_string()));
990 assert!(filenames.contains(&"chamber_backup_2024-01-06_00-00-00Z_1641427200.json".to_string()));
991 assert!(filenames.contains(&"chamber_backup_2024-01-05_00-00-00Z_1641340800.json".to_string()));
992 }
993
994 #[test]
995 fn test_cleanup_old_backups_under_limit() {
996 let temp_dir = TempDir::new().unwrap();
997 let backup_dir = temp_dir.path().join("backups");
998 fs::create_dir_all(&backup_dir).unwrap();
999
1000 let backup1 = backup_dir.join("chamber_backup_2024-01-01_00-00-00Z_1640995200.json");
1002 let backup2 = backup_dir.join("chamber_backup_2024-01-02_00-00-00Z_1641081600.json");
1003
1004 fs::write(&backup1, "backup1").unwrap();
1005 fs::write(&backup2, "backup2").unwrap();
1006
1007 let config = create_test_config_with_options(&temp_dir, "json", false, false, 5, 24);
1008 let vault = MockVault::new(vec![]);
1009 let manager = BackupManager::new(vault, config);
1010
1011 let result = manager.cleanup_old_backups();
1012 assert!(result.is_ok());
1013
1014 let remaining_backups = manager.find_all_backups().unwrap();
1016 assert_eq!(remaining_backups.len(), 2);
1017 }
1018
1019 #[test]
1020 fn test_backup_if_needed_vault_error() {
1021 let temp_dir = TempDir::new().unwrap();
1022 let config = create_test_config(&temp_dir);
1023 let vault = MockVault::new_failing();
1024
1025 let mut manager = BackupManager::new(vault, config);
1026 let result = manager.backup_if_needed();
1027
1028 assert!(result.is_err());
1030 assert!(result.unwrap_err().to_string().contains("Mock vault error"));
1031 }
1032
1033 #[test]
1034 fn test_force_backup_with_items() {
1035 let temp_dir = TempDir::new().unwrap();
1036 let config = create_test_config(&temp_dir);
1037 let items = vec![create_test_item(1, "test_item")];
1038 let vault = MockVault::new(items);
1039
1040 let mut manager = BackupManager::new(vault, config);
1041
1042 let result = manager.force_backup();
1044
1045 match result {
1048 Ok(path) => {
1049 assert!(path.exists() || path.parent().is_some_and(|_| false));
1051 println!("Force backup succeeded with path: {}", path.display());
1052 }
1053 Err(e) => {
1054 let error_msg = e.to_string();
1056 assert!(
1057 !error_msg.contains("Mock vault error"),
1058 "Error should not be from vault operations, got: {error_msg}"
1059 );
1060 println!("Force backup failed as expected with: {error_msg}");
1061 }
1062 }
1063 }
1064
1065 #[test]
1066 fn test_force_backup_vault_error() {
1067 let temp_dir = TempDir::new().unwrap();
1068 let config = create_test_config(&temp_dir);
1069 let vault = MockVault::new_failing(); let mut manager = BackupManager::new(vault, config);
1072
1073 let result = manager.force_backup();
1075 assert!(result.is_err());
1076 assert!(result.unwrap_err().to_string().contains("Mock vault error"));
1077 }
1078
1079 #[test]
1080 fn test_perform_backup_flow() {
1081 let temp_dir = TempDir::new().unwrap();
1083 let config = create_test_config(&temp_dir);
1084 let items = vec![create_test_item(1, "test_item")];
1085 let vault = MockVault::new(items);
1086
1087 let mut manager = BackupManager::new(vault, config);
1088
1089 let result = manager.perform_backup();
1091
1092 assert!(manager.config.backup_dir.exists());
1094
1095 match result {
1097 Ok(Some(path)) => {
1098 assert!(path.to_string_lossy().contains("chamber_backup_"));
1100 println!("Backup created at: {}", path.display());
1101 }
1102 Ok(None) => {
1103 panic!("perform_backup returned None, which shouldn't happen in this test");
1104 }
1105 Err(e) => {
1106 println!("Backup failed with: {e}");
1108 assert!(!e.to_string().contains("Mock vault error"));
1110 }
1111 }
1112 }
1113
1114 #[test]
1115 fn test_different_backup_formats() {
1116 let temp_dir = TempDir::new().unwrap();
1117
1118 for format in ["json", "csv", "backup"] {
1119 let config = create_test_config_with_options(&temp_dir, format, false, false, 5, 24);
1120 let vault = MockVault::new(vec![create_test_item(1, "test")]);
1121 let manager = BackupManager::new(vault, config);
1122
1123 let timestamp = OffsetDateTime::now_utc();
1124 let filename = manager.generate_backup_filename(×tamp).unwrap();
1125 assert!(filename.ends_with(&format!(".{format}")));
1126 }
1127 }
1128
1129 #[test]
1130 fn test_different_intervals() {
1131 let temp_dir = TempDir::new().unwrap();
1132
1133 for interval in [1, 12, 24, 48, 168] {
1134 let config = create_test_config_with_options(&temp_dir, "json", false, false, 5, interval);
1136 let vault = MockVault::new(vec![]);
1137 let manager = BackupManager::new(vault, config);
1138
1139 assert_eq!(manager.config.interval_hours, interval);
1140 }
1141 }
1142
1143 #[test]
1144 fn test_verify_unknown_format() {
1145 let temp_dir = TempDir::new().unwrap();
1146 let config = create_test_config_with_options(&temp_dir, "unknown", false, false, 5, 24);
1147 let vault = MockVault::new(vec![]);
1148 let manager = BackupManager::new(vault, config);
1149
1150 let test_file = temp_dir.path().join("test.unknown");
1151 fs::write(&test_file, "some content").unwrap();
1152
1153 let result = manager.verify_uncompressed_backup(&test_file);
1154 assert!(result.is_err());
1155 assert!(result.unwrap_err().to_string().contains("Unknown backup format"));
1156 }
1157
1158 #[test]
1159 fn test_extract_timestamp_edge_cases() {
1160 let temp_dir = TempDir::new().unwrap();
1161 let config = create_test_config(&temp_dir);
1162 let vault = MockVault::new(vec![]);
1163 let manager = BackupManager::new(vault, config);
1164
1165 let malformed_path = temp_dir.path().join("chamber_backup_2024-01-01_00-00-00Z_invalid.json");
1167 assert!(manager.extract_timestamp_from_filename(&malformed_path).is_none());
1168
1169 let missing_path = temp_dir.path().join("chamber_backup_2024-01-01_00-00-00Z.json");
1171 assert!(manager.extract_timestamp_from_filename(&missing_path).is_none());
1172
1173 let edge_cases = [
1175 ("chamber_backup_2024-01-01_00-00-00Z_0.json", 0),
1176 ("chamber_backup_2024-01-01_00-00-00Z_2147483647.json", 2_147_483_647), ];
1178
1179 for (filename, expected) in edge_cases {
1180 let path = temp_dir.path().join(filename);
1181 let timestamp = manager.extract_timestamp_from_filename(&path);
1182 assert!(timestamp.is_some());
1183 assert_eq!(timestamp.unwrap().unix_timestamp(), expected);
1184 }
1185 }
1186}