1use blake3::Hasher;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs::File;
10use std::io::{BufReader, Read};
11use std::path::Path;
12use tracing::{debug, info, warn};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct FileHash {
17 pub hash: String,
19 pub size: u64,
21}
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct ArtifactManifest {
26 pub files: HashMap<String, FileHash>,
28 pub created_at: u64,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub worker_id: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct VerificationResult {
38 pub passed: Vec<String>,
40 pub failed: Vec<VerificationFailure>,
42 pub skipped: Vec<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct VerificationFailure {
49 pub path: String,
51 pub expected_hash: String,
53 pub actual_hash: String,
55 pub expected_size: u64,
57 pub actual_size: u64,
59}
60
61impl VerificationResult {
62 pub fn all_passed(&self) -> bool {
64 self.failed.is_empty()
65 }
66
67 pub fn summary(&self) -> String {
69 format!(
70 "{} passed, {} failed, {} skipped",
71 self.passed.len(),
72 self.failed.len(),
73 self.skipped.len()
74 )
75 }
76
77 pub fn format_failures(&self) -> String {
79 if self.failed.is_empty() {
80 return String::new();
81 }
82
83 let mut msg = String::new();
84 msg.push_str("Artifact integrity verification failed:\n\n");
85
86 for failure in &self.failed {
87 msg.push_str(&format!(" {} - HASH MISMATCH\n", failure.path));
88 msg.push_str(&format!(
89 " Expected: {} ({} bytes)\n",
90 short_hash(&failure.expected_hash),
91 failure.expected_size
92 ));
93 msg.push_str(&format!(
94 " Actual: {} ({} bytes)\n",
95 short_hash(&failure.actual_hash),
96 failure.actual_size
97 ));
98 }
99
100 msg.push_str("\nThis may indicate:\n");
101 msg.push_str(" - Transfer corruption (retry may help)\n");
102 msg.push_str(" - Incomplete transfer\n");
103 msg.push_str(" - Worker build cache inconsistency\n");
104 msg.push_str("\nSuggested actions:\n");
105 msg.push_str(" 1. Run `rch diagnose` for detailed analysis\n");
106 msg.push_str(" 2. Re-run the build to verify consistency\n");
107 msg.push_str(" 3. Check worker health: `rch workers probe`\n");
108
109 msg
110 }
111}
112
113impl VerificationFailure {
114 pub fn new(
116 path: impl Into<String>,
117 expected_hash: impl Into<String>,
118 actual_hash: impl Into<String>,
119 expected_size: u64,
120 actual_size: u64,
121 ) -> Self {
122 Self {
123 path: path.into(),
124 expected_hash: expected_hash.into(),
125 actual_hash: actual_hash.into(),
126 expected_size,
127 actual_size,
128 }
129 }
130}
131
132pub fn compute_file_hash(path: &Path) -> std::io::Result<FileHash> {
140 let file = File::open(path)?;
141 let metadata = file.metadata()?;
142 let size = metadata.len();
143
144 let mut reader = BufReader::new(file);
145 let mut hasher = Hasher::new();
146 let mut buffer = [0u8; 65536]; loop {
149 let bytes_read = reader.read(&mut buffer)?;
150 if bytes_read == 0 {
151 break;
152 }
153 hasher.update(&buffer[..bytes_read]);
154 }
155
156 let hash = hasher.finalize().to_hex().to_string();
157
158 Ok(FileHash { hash, size })
159}
160
161fn short_hash(hash: &str) -> String {
169 hash.chars().take(16).collect()
170}
171
172fn is_safe_path(path_str: &str) -> bool {
174 let path = Path::new(path_str);
175 if path.is_absolute() {
176 return false;
177 }
178 for component in path.components() {
179 match component {
180 std::path::Component::ParentDir => return false,
181 std::path::Component::RootDir | std::path::Component::Prefix(_) => return false,
182 _ => {}
183 }
184 }
185 true
186}
187
188pub fn verify_artifacts(
198 base_dir: &Path,
199 manifest: &ArtifactManifest,
200 max_size: u64,
201) -> VerificationResult {
202 let mut result = VerificationResult {
203 passed: Vec::new(),
204 failed: Vec::new(),
205 skipped: Vec::new(),
206 };
207
208 for (rel_path, expected) in &manifest.files {
209 if !is_safe_path(rel_path) {
211 warn!("Skipping unsafe path in manifest: {}", rel_path);
212 result.skipped.push(rel_path.clone());
213 continue;
214 }
215
216 let full_path = base_dir.join(rel_path);
217
218 if !full_path.exists() {
220 debug!("Skipping verification of missing file: {}", rel_path);
221 result.skipped.push(rel_path.clone());
222 continue;
223 }
224
225 let actual_size = match std::fs::metadata(&full_path) {
227 Ok(meta) => meta.len(),
228 Err(e) => {
229 warn!("Skipping verification of {}: {}", rel_path, e);
230 result.skipped.push(rel_path.clone());
231 continue;
232 }
233 };
234 if actual_size > max_size {
235 debug!(
236 "Skipping verification of large file: {} ({} bytes > {} max)",
237 rel_path, actual_size, max_size
238 );
239 result.skipped.push(rel_path.clone());
240 continue;
241 }
242
243 match compute_file_hash(&full_path) {
245 Ok(actual) => {
246 if actual.hash == expected.hash && actual.size == expected.size {
247 debug!("Verification passed: {}", rel_path);
248 result.passed.push(rel_path.clone());
249 } else {
250 warn!(
251 "Verification failed for {}: expected {} ({} bytes), got {} ({} bytes)",
252 rel_path,
253 short_hash(&expected.hash),
254 expected.size,
255 short_hash(&actual.hash),
256 actual.size
257 );
258 result.failed.push(VerificationFailure::new(
259 rel_path,
260 &expected.hash,
261 actual.hash,
262 expected.size,
263 actual.size,
264 ));
265 }
266 }
267 Err(e) => {
268 warn!("Failed to hash {}: {}", rel_path, e);
269 result.skipped.push(rel_path.clone());
270 }
271 }
272 }
273
274 info!("Artifact verification complete: {}", result.summary());
275
276 result
277}
278
279pub fn create_manifest(
289 base_dir: &Path,
290 rel_paths: &[String],
291 worker_id: Option<String>,
292) -> ArtifactManifest {
293 let mut manifest = ArtifactManifest {
294 files: HashMap::new(),
295 created_at: std::time::SystemTime::now()
296 .duration_since(std::time::UNIX_EPOCH)
297 .unwrap_or_default()
298 .as_secs(),
299 worker_id,
300 };
301
302 for rel_path in rel_paths {
303 let full_path = base_dir.join(rel_path);
304 match compute_file_hash(&full_path) {
305 Ok(hash) => {
306 manifest.files.insert(rel_path.clone(), hash);
307 }
308 Err(e) => {
309 debug!("Skipping {} in manifest: {}", rel_path, e);
310 }
311 }
312 }
313
314 manifest
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 use tempfile::TempDir;
322 use tracing::info;
323
324 fn init_test_logging() {
325 let _ = tracing_subscriber::fmt()
326 .with_test_writer()
327 .with_max_level(tracing::Level::DEBUG)
328 .try_init();
329 }
330
331 #[test]
332 fn test_short_hash_tolerates_short_and_non_ascii() {
333 assert_eq!(short_hash(""), "");
338 assert_eq!(short_hash("ab"), "ab");
339 assert_eq!(
340 short_hash("0123456789abcdef0123456789abcdef"),
341 "0123456789abcdef"
342 );
343 let multi = "é".repeat(20);
346 assert_eq!(short_hash(&multi).chars().count(), 16);
347 }
348
349 #[test]
350 fn test_format_failures_tolerates_short_hashes() {
351 let result = VerificationResult {
352 passed: vec![],
353 failed: vec![VerificationFailure::new("foo", "ab", "cd", 1, 1)],
354 skipped: vec![],
355 };
356 let msg = result.format_failures();
358 assert!(msg.contains("foo"));
359 assert!(msg.contains("ab"));
360 assert!(msg.contains("cd"));
361 }
362
363 #[test]
364 fn test_compute_file_hash_basic() {
365 init_test_logging();
366 info!("TEST START: test_compute_file_hash_basic");
367
368 let temp_dir = TempDir::new().unwrap();
369 let test_file = temp_dir.path().join("test.txt");
370
371 let content = b"Hello, World!";
373 std::fs::write(&test_file, content).unwrap();
374
375 let hash = compute_file_hash(&test_file).unwrap();
376
377 assert_eq!(hash.size, 13);
378 assert_eq!(hash.hash.len(), 64); info!("Hash: {}", hash.hash);
380
381 let hash2 = compute_file_hash(&test_file).unwrap();
383 assert_eq!(hash.hash, hash2.hash);
384
385 info!("TEST PASS: test_compute_file_hash_basic");
386 }
387
388 #[test]
389 fn test_compute_file_hash_empty() {
390 init_test_logging();
391 info!("TEST START: test_compute_file_hash_empty");
392
393 let temp_dir = TempDir::new().unwrap();
394 let test_file = temp_dir.path().join("empty.txt");
395 std::fs::write(&test_file, b"").unwrap();
396
397 let hash = compute_file_hash(&test_file).unwrap();
398
399 assert_eq!(hash.size, 0);
400 assert_eq!(hash.hash.len(), 64);
401 info!("Empty file hash: {}", hash.hash);
402
403 info!("TEST PASS: test_compute_file_hash_empty");
404 }
405
406 #[test]
407 fn test_compute_file_hash_nonexistent() {
408 init_test_logging();
409 info!("TEST START: test_compute_file_hash_nonexistent");
410
411 let result = compute_file_hash(Path::new("/nonexistent/file"));
412 assert!(result.is_err());
413
414 info!("TEST PASS: test_compute_file_hash_nonexistent");
415 }
416
417 #[test]
418 fn test_verify_artifacts_all_pass() {
419 init_test_logging();
420 info!("TEST START: test_verify_artifacts_all_pass");
421
422 let temp_dir = TempDir::new().unwrap();
423
424 std::fs::write(temp_dir.path().join("a.txt"), b"content a").unwrap();
426 std::fs::write(temp_dir.path().join("b.txt"), b"content b").unwrap();
427
428 let manifest = create_manifest(
430 temp_dir.path(),
431 &["a.txt".to_string(), "b.txt".to_string()],
432 Some("worker1".to_string()),
433 );
434
435 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
437
438 assert!(result.all_passed());
439 assert_eq!(result.passed.len(), 2);
440 assert!(result.failed.is_empty());
441
442 info!("TEST PASS: test_verify_artifacts_all_pass");
443 }
444
445 #[test]
446 fn test_verify_artifacts_with_mismatch() {
447 init_test_logging();
448 info!("TEST START: test_verify_artifacts_with_mismatch");
449
450 let temp_dir = TempDir::new().unwrap();
451
452 std::fs::write(temp_dir.path().join("test.txt"), b"original").unwrap();
454
455 let manifest = create_manifest(temp_dir.path(), &["test.txt".to_string()], None);
457
458 std::fs::write(temp_dir.path().join("test.txt"), b"modified").unwrap();
460
461 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
463
464 assert!(!result.all_passed());
465 assert_eq!(result.failed.len(), 1);
466 assert_eq!(result.failed[0].path, "test.txt");
467
468 info!("Failure details:\n{}", result.format_failures());
469
470 info!("TEST PASS: test_verify_artifacts_with_mismatch");
471 }
472
473 #[test]
474 fn test_verify_artifacts_skip_large() {
475 init_test_logging();
476 info!("TEST START: test_verify_artifacts_skip_large");
477
478 let temp_dir = TempDir::new().unwrap();
479
480 std::fs::write(temp_dir.path().join("large.txt"), b"some content here").unwrap();
482
483 let manifest = create_manifest(temp_dir.path(), &["large.txt".to_string()], None);
484
485 let result = verify_artifacts(temp_dir.path(), &manifest, 5);
487
488 assert!(result.all_passed()); assert_eq!(result.skipped.len(), 1);
490
491 info!("TEST PASS: test_verify_artifacts_skip_large");
492 }
493
494 #[test]
495 fn test_verify_artifacts_missing_file() {
496 init_test_logging();
497 info!("TEST START: test_verify_artifacts_missing_file");
498
499 let temp_dir = TempDir::new().unwrap();
500
501 let mut manifest = ArtifactManifest::default();
503 manifest.files.insert(
504 "missing.txt".to_string(),
505 FileHash {
506 hash: "abcd1234".repeat(8),
507 size: 100,
508 },
509 );
510
511 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
512
513 assert!(result.all_passed()); assert_eq!(result.skipped.len(), 1);
515
516 info!("TEST PASS: test_verify_artifacts_missing_file");
517 }
518
519 #[test]
520 fn test_verification_result_summary() {
521 init_test_logging();
522 info!("TEST START: test_verification_result_summary");
523
524 let result = VerificationResult {
525 passed: vec!["a.txt".to_string(), "b.txt".to_string()],
526 failed: vec![VerificationFailure::new("c.txt", "abc", "def", 100, 200)],
527 skipped: vec!["d.txt".to_string()],
528 };
529
530 let summary = result.summary();
531 assert!(summary.contains("2 passed"));
532 assert!(summary.contains("1 failed"));
533 assert!(summary.contains("1 skipped"));
534
535 info!("Summary: {}", summary);
536 info!("TEST PASS: test_verification_result_summary");
537 }
538
539 #[test]
540 fn test_create_manifest() {
541 init_test_logging();
542 info!("TEST START: test_create_manifest");
543
544 let temp_dir = TempDir::new().unwrap();
545
546 std::fs::write(temp_dir.path().join("file1.txt"), b"content1").unwrap();
547 std::fs::write(temp_dir.path().join("file2.txt"), b"content2").unwrap();
548
549 let manifest = create_manifest(
550 temp_dir.path(),
551 &[
552 "file1.txt".to_string(),
553 "file2.txt".to_string(),
554 "missing.txt".to_string(),
555 ],
556 Some("test-worker".to_string()),
557 );
558
559 assert_eq!(manifest.files.len(), 2);
561 assert!(manifest.files.contains_key("file1.txt"));
562 assert!(manifest.files.contains_key("file2.txt"));
563 assert!(!manifest.files.contains_key("missing.txt"));
564 assert_eq!(manifest.worker_id, Some("test-worker".to_string()));
565 assert!(manifest.created_at > 0);
566
567 info!("TEST PASS: test_create_manifest");
568 }
569
570 #[test]
571 fn test_file_hash_equality() {
572 init_test_logging();
573 info!("TEST START: test_file_hash_equality");
574
575 let hash1 = FileHash {
576 hash: "abc123".to_string(),
577 size: 100,
578 };
579 let hash2 = FileHash {
580 hash: "abc123".to_string(),
581 size: 100,
582 };
583 let hash3 = FileHash {
584 hash: "def456".to_string(),
585 size: 100,
586 };
587
588 assert_eq!(hash1, hash2);
589 assert_ne!(hash1, hash3);
590
591 info!("TEST PASS: test_file_hash_equality");
592 }
593
594 #[test]
595 fn test_file_hash_serialization() {
596 init_test_logging();
597 info!("TEST START: test_file_hash_serialization");
598
599 let hash = FileHash {
600 hash: "0".repeat(64),
601 size: 1024,
602 };
603
604 let json = serde_json::to_string(&hash).unwrap();
605 let deserialized: FileHash = serde_json::from_str(&json).unwrap();
606
607 assert_eq!(hash, deserialized);
608 assert!(json.contains("\"hash\""));
609 assert!(json.contains("\"size\""));
610
611 info!("TEST PASS: test_file_hash_serialization");
612 }
613
614 #[test]
615 fn test_file_hash_clone() {
616 init_test_logging();
617 info!("TEST START: test_file_hash_clone");
618
619 let original = FileHash {
620 hash: "test_hash".to_string(),
621 size: 500,
622 };
623 let cloned = original.clone();
624
625 assert_eq!(original.hash, cloned.hash);
626 assert_eq!(original.size, cloned.size);
627
628 info!("TEST PASS: test_file_hash_clone");
629 }
630
631 #[test]
632 fn test_artifact_manifest_default() {
633 init_test_logging();
634 info!("TEST START: test_artifact_manifest_default");
635
636 let manifest = ArtifactManifest::default();
637
638 assert!(manifest.files.is_empty());
639 assert_eq!(manifest.created_at, 0);
640 assert!(manifest.worker_id.is_none());
641
642 info!("TEST PASS: test_artifact_manifest_default");
643 }
644
645 #[test]
646 fn test_artifact_manifest_serialization() {
647 init_test_logging();
648 info!("TEST START: test_artifact_manifest_serialization");
649
650 let mut manifest = ArtifactManifest {
651 created_at: 1706380800,
652 worker_id: Some("worker-1".to_string()),
653 ..ArtifactManifest::default()
654 };
655 manifest.files.insert(
656 "test.bin".to_string(),
657 FileHash {
658 hash: "a".repeat(64),
659 size: 256,
660 },
661 );
662
663 let json = serde_json::to_string(&manifest).unwrap();
664 let deserialized: ArtifactManifest = serde_json::from_str(&json).unwrap();
665
666 assert_eq!(manifest.created_at, deserialized.created_at);
667 assert_eq!(manifest.worker_id, deserialized.worker_id);
668 assert_eq!(manifest.files.len(), deserialized.files.len());
669
670 info!("TEST PASS: test_artifact_manifest_serialization");
671 }
672
673 #[test]
674 fn test_artifact_manifest_without_worker_id() {
675 init_test_logging();
676 info!("TEST START: test_artifact_manifest_without_worker_id");
677
678 let manifest = ArtifactManifest {
679 files: HashMap::new(),
680 created_at: 0,
681 worker_id: None,
682 };
683
684 let json = serde_json::to_string(&manifest).unwrap();
685 assert!(!json.contains("worker_id"));
687
688 info!("TEST PASS: test_artifact_manifest_without_worker_id");
689 }
690
691 #[test]
692 fn test_verification_result_all_passed_empty() {
693 init_test_logging();
694 info!("TEST START: test_verification_result_all_passed_empty");
695
696 let result = VerificationResult {
697 passed: vec![],
698 failed: vec![],
699 skipped: vec![],
700 };
701
702 assert!(result.all_passed());
703 assert_eq!(result.summary(), "0 passed, 0 failed, 0 skipped");
704
705 info!("TEST PASS: test_verification_result_all_passed_empty");
706 }
707
708 #[test]
709 fn test_verification_result_format_failures_empty() {
710 init_test_logging();
711 info!("TEST START: test_verification_result_format_failures_empty");
712
713 let result = VerificationResult {
714 passed: vec!["a.txt".to_string()],
715 failed: vec![],
716 skipped: vec![],
717 };
718
719 let failures = result.format_failures();
720 assert!(failures.is_empty());
721
722 info!("TEST PASS: test_verification_result_format_failures_empty");
723 }
724
725 #[test]
726 fn test_verification_result_format_failures_content() {
727 init_test_logging();
728 info!("TEST START: test_verification_result_format_failures_content");
729
730 let result = VerificationResult {
731 passed: vec![],
732 failed: vec![VerificationFailure::new(
733 "binary.exe",
734 "a".repeat(64),
735 "b".repeat(64),
736 1000,
737 1001,
738 )],
739 skipped: vec![],
740 };
741
742 let failures = result.format_failures();
743 assert!(failures.contains("binary.exe"));
744 assert!(failures.contains("HASH MISMATCH"));
745 assert!(failures.contains("Expected:"));
746 assert!(failures.contains("Actual:"));
747 assert!(failures.contains("1000 bytes"));
748 assert!(failures.contains("1001 bytes"));
749 assert!(failures.contains("Suggested actions"));
750 assert!(failures.contains("rch diagnose"));
751
752 info!("TEST PASS: test_verification_result_format_failures_content");
753 }
754
755 #[test]
756 fn test_verification_failure_new() {
757 init_test_logging();
758 info!("TEST START: test_verification_failure_new");
759
760 let failure = VerificationFailure::new(
761 "path/to/file.bin",
762 "expected_hash_value",
763 "actual_hash_value",
764 500,
765 600,
766 );
767
768 assert_eq!(failure.path, "path/to/file.bin");
769 assert_eq!(failure.expected_hash, "expected_hash_value");
770 assert_eq!(failure.actual_hash, "actual_hash_value");
771 assert_eq!(failure.expected_size, 500);
772 assert_eq!(failure.actual_size, 600);
773
774 info!("TEST PASS: test_verification_failure_new");
775 }
776
777 #[test]
778 fn test_verification_failure_from_string() {
779 init_test_logging();
780 info!("TEST START: test_verification_failure_from_string");
781
782 let failure = VerificationFailure::new(
784 String::from("owned_path"),
785 String::from("owned_expected"),
786 String::from("owned_actual"),
787 100,
788 200,
789 );
790
791 assert_eq!(failure.path, "owned_path");
792 assert_eq!(failure.expected_hash, "owned_expected");
793 assert_eq!(failure.actual_hash, "owned_actual");
794
795 info!("TEST PASS: test_verification_failure_from_string");
796 }
797
798 #[test]
799 fn test_verification_failure_serialization() {
800 init_test_logging();
801 info!("TEST START: test_verification_failure_serialization");
802
803 let failure = VerificationFailure::new("test.bin", "hash1", "hash2", 100, 200);
804
805 let json = serde_json::to_string(&failure).unwrap();
806 let deserialized: VerificationFailure = serde_json::from_str(&json).unwrap();
807
808 assert_eq!(failure.path, deserialized.path);
809 assert_eq!(failure.expected_hash, deserialized.expected_hash);
810 assert_eq!(failure.actual_hash, deserialized.actual_hash);
811 assert_eq!(failure.expected_size, deserialized.expected_size);
812 assert_eq!(failure.actual_size, deserialized.actual_size);
813
814 info!("TEST PASS: test_verification_failure_serialization");
815 }
816
817 #[test]
818 fn test_verification_result_serialization() {
819 init_test_logging();
820 info!("TEST START: test_verification_result_serialization");
821
822 let result = VerificationResult {
823 passed: vec!["a.txt".to_string()],
824 failed: vec![VerificationFailure::new("b.txt", "h1", "h2", 10, 20)],
825 skipped: vec!["c.txt".to_string()],
826 };
827
828 let json = serde_json::to_string(&result).unwrap();
829 let deserialized: VerificationResult = serde_json::from_str(&json).unwrap();
830
831 assert_eq!(result.passed, deserialized.passed);
832 assert_eq!(result.failed.len(), deserialized.failed.len());
833 assert_eq!(result.skipped, deserialized.skipped);
834
835 info!("TEST PASS: test_verification_result_serialization");
836 }
837
838 #[test]
839 fn test_compute_file_hash_deterministic() {
840 init_test_logging();
841 info!("TEST START: test_compute_file_hash_deterministic");
842
843 let temp_dir = TempDir::new().unwrap();
844 let file1 = temp_dir.path().join("file1.txt");
845 let file2 = temp_dir.path().join("file2.txt");
846
847 let content = b"Identical content for hashing test";
849 std::fs::write(&file1, content).unwrap();
850 std::fs::write(&file2, content).unwrap();
851
852 let hash1 = compute_file_hash(&file1).unwrap();
853 let hash2 = compute_file_hash(&file2).unwrap();
854
855 assert_eq!(hash1.hash, hash2.hash);
856 assert_eq!(hash1.size, hash2.size);
857
858 info!("TEST PASS: test_compute_file_hash_deterministic");
859 }
860
861 #[test]
862 fn test_compute_file_hash_different_content() {
863 init_test_logging();
864 info!("TEST START: test_compute_file_hash_different_content");
865
866 let temp_dir = TempDir::new().unwrap();
867 let file1 = temp_dir.path().join("file1.txt");
868 let file2 = temp_dir.path().join("file2.txt");
869
870 std::fs::write(&file1, b"content one").unwrap();
871 std::fs::write(&file2, b"content two").unwrap();
872
873 let hash1 = compute_file_hash(&file1).unwrap();
874 let hash2 = compute_file_hash(&file2).unwrap();
875
876 assert_ne!(hash1.hash, hash2.hash);
877
878 info!("TEST PASS: test_compute_file_hash_different_content");
879 }
880
881 #[test]
882 fn test_verification_with_size_mismatch() {
883 init_test_logging();
884 info!("TEST START: test_verification_with_size_mismatch");
885
886 let temp_dir = TempDir::new().unwrap();
887 std::fs::write(temp_dir.path().join("test.txt"), b"content").unwrap();
888
889 let hash = compute_file_hash(&temp_dir.path().join("test.txt")).unwrap();
891 let mut manifest = ArtifactManifest::default();
892 manifest.files.insert(
893 "test.txt".to_string(),
894 FileHash {
895 hash: hash.hash, size: 9999, },
898 );
899
900 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
901
902 assert!(!result.all_passed());
904 assert_eq!(result.failed.len(), 1);
905
906 info!("TEST PASS: test_verification_with_size_mismatch");
907 }
908
909 #[test]
910 fn test_create_manifest_empty_list() {
911 init_test_logging();
912 info!("TEST START: test_create_manifest_empty_list");
913
914 let temp_dir = TempDir::new().unwrap();
915 let manifest = create_manifest(temp_dir.path(), &[], None);
916
917 assert!(manifest.files.is_empty());
918 assert!(manifest.worker_id.is_none());
919
920 info!("TEST PASS: test_create_manifest_empty_list");
921 }
922
923 #[test]
924 fn test_verification_clone_traits() {
925 init_test_logging();
926 info!("TEST START: test_verification_clone_traits");
927
928 let result = VerificationResult {
929 passed: vec!["a.txt".to_string()],
930 failed: vec![],
931 skipped: vec![],
932 };
933
934 let cloned = result.clone();
935 assert_eq!(result.passed, cloned.passed);
936 assert_eq!(result.failed.len(), cloned.failed.len());
937 assert_eq!(result.skipped, cloned.skipped);
938
939 info!("TEST PASS: test_verification_clone_traits");
940 }
941
942 #[test]
943 fn test_verification_failure_clone() {
944 init_test_logging();
945 info!("TEST START: test_verification_failure_clone");
946
947 let failure = VerificationFailure::new("path", "h1", "h2", 10, 20);
948 let cloned = failure.clone();
949
950 assert_eq!(failure.path, cloned.path);
951 assert_eq!(failure.expected_hash, cloned.expected_hash);
952 assert_eq!(failure.actual_hash, cloned.actual_hash);
953
954 info!("TEST PASS: test_verification_failure_clone");
955 }
956
957 #[test]
958 fn test_artifact_manifest_clone() {
959 init_test_logging();
960 info!("TEST START: test_artifact_manifest_clone");
961
962 let mut manifest = ArtifactManifest {
963 created_at: 12345,
964 worker_id: Some("worker".to_string()),
965 ..ArtifactManifest::default()
966 };
967 manifest.files.insert(
968 "f.txt".to_string(),
969 FileHash {
970 hash: "h".to_string(),
971 size: 1,
972 },
973 );
974
975 let cloned = manifest.clone();
976 assert_eq!(manifest.created_at, cloned.created_at);
977 assert_eq!(manifest.worker_id, cloned.worker_id);
978 assert_eq!(manifest.files.len(), cloned.files.len());
979
980 info!("TEST PASS: test_artifact_manifest_clone");
981 }
982
983 #[test]
984 fn test_file_hash_debug() {
985 init_test_logging();
986 info!("TEST START: test_file_hash_debug");
987
988 let hash = FileHash {
989 hash: "abc".to_string(),
990 size: 100,
991 };
992
993 let debug = format!("{:?}", hash);
994 assert!(debug.contains("FileHash"));
995 assert!(debug.contains("abc"));
996 assert!(debug.contains("100"));
997
998 info!("TEST PASS: test_file_hash_debug");
999 }
1000
1001 #[test]
1002 fn test_verification_result_debug() {
1003 init_test_logging();
1004 info!("TEST START: test_verification_result_debug");
1005
1006 let result = VerificationResult {
1007 passed: vec!["a.txt".to_string()],
1008 failed: vec![],
1009 skipped: vec![],
1010 };
1011
1012 let debug = format!("{:?}", result);
1013 assert!(debug.contains("VerificationResult"));
1014 assert!(debug.contains("a.txt"));
1015
1016 info!("TEST PASS: test_verification_result_debug");
1017 }
1018
1019 #[test]
1020 fn test_verify_artifacts_rejects_unsafe_paths() {
1021 init_test_logging();
1022 info!("TEST START: test_verify_artifacts_rejects_unsafe_paths");
1023
1024 let temp_dir = TempDir::new().unwrap();
1025 let mut manifest = create_manifest(temp_dir.path(), &[], None);
1027
1028 manifest.files.insert(
1030 "../outside.txt".to_string(),
1031 FileHash {
1032 hash: "abc".to_string(),
1033 size: 100,
1034 },
1035 );
1036 manifest.files.insert(
1037 "/etc/passwd".to_string(),
1038 FileHash {
1039 hash: "abc".to_string(),
1040 size: 100,
1041 },
1042 );
1043 manifest.files.insert(
1045 "safe.txt".to_string(),
1046 FileHash {
1047 hash: "abc".to_string(),
1048 size: 100,
1049 },
1050 );
1051 let result = verify_artifacts(temp_dir.path(), &manifest, 1024);
1056
1057 assert!(result.skipped.contains(&"../outside.txt".to_string()));
1059 assert!(result.skipped.contains(&"/etc/passwd".to_string()));
1060
1061 info!("TEST PASS: test_verify_artifacts_rejects_unsafe_paths");
1073 }
1074}