//! CLI round-trip integration tests. //! //! Each test runs the actual `encrypted_archive` binary via `assert_cmd`, //! packs files into an archive, unpacks them, and verifies byte-identical output. //! All tests use `tempdir()` for isolation (auto-cleanup, parallel-safe). use assert_cmd::Command; use std::fs; use tempfile::tempdir; /// Helper: get a Command for the encrypted_archive binary. fn cmd() -> Command { Command::new(assert_cmd::cargo::cargo_bin!("encrypted_archive")) } /// Single text file: pack "Hello", unpack, verify byte-identical. #[test] fn test_roundtrip_single_text_file() { let dir = tempdir().unwrap(); let input_file = dir.path().join("hello.txt"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); fs::write(&input_file, b"Hello").unwrap(); cmd() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); let extracted = fs::read(output_dir.join("hello.txt")).unwrap(); assert_eq!(extracted, b"Hello"); } /// Multiple files: pack two files, unpack, verify both byte-identical. #[test] fn test_roundtrip_multiple_files() { let dir = tempdir().unwrap(); let text_file = dir.path().join("text.txt"); let binary_file = dir.path().join("binary.dat"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); fs::write(&text_file, b"Some text content").unwrap(); fs::write(&binary_file, &[0x42u8; 256]).unwrap(); cmd() .args([ "pack", text_file.to_str().unwrap(), binary_file.to_str().unwrap(), "-o", archive.to_str().unwrap(), ]) .assert() .success(); cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); assert_eq!( fs::read(output_dir.join("text.txt")).unwrap(), b"Some text content" ); assert_eq!( fs::read(output_dir.join("binary.dat")).unwrap(), vec![0x42u8; 256] ); } /// Empty file: pack 0-byte file, unpack, verify extracted file is exactly 0 bytes. /// This tests PKCS7 full-block padding on 0-byte input (encrypted_size = 16). #[test] fn test_roundtrip_empty_file() { let dir = tempdir().unwrap(); let input_file = dir.path().join("empty.txt"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); fs::write(&input_file, b"").unwrap(); cmd() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); let extracted = fs::read(output_dir.join("empty.txt")).unwrap(); assert!(extracted.is_empty(), "Empty file should extract as 0 bytes"); } /// Cyrillic filename: pack file with UTF-8 non-ASCII name, unpack, verify content. /// This tests non-ASCII (UTF-8) filename handling in TOC entries. #[test] fn test_roundtrip_cyrillic_filename() { let dir = tempdir().unwrap(); let input_file = dir.path().join("файл.txt"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); let content = "Содержимое".as_bytes(); fs::write(&input_file, content).unwrap(); cmd() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); let extracted = fs::read(output_dir.join("файл.txt")).unwrap(); assert_eq!(extracted, content); } /// Large file (11MB): pack, unpack, verify byte-identical. /// Tests large file handling (>10MB) per Phase 3 success criteria. #[test] fn test_roundtrip_large_file() { let dir = tempdir().unwrap(); let input_file = dir.path().join("large.bin"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); // Generate 11MB of deterministic pseudo-random data let data: Vec = (0..11_000_000u32) .map(|i| i.wrapping_mul(2654435761) as u8) .collect(); fs::write(&input_file, &data).unwrap(); // Pack with --no-compress bin (skip compression for binary extension) cmd() .args([ "pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap(), "--no-compress", "bin", ]) .assert() .success(); cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); let extracted = fs::read(output_dir.join("large.bin")).unwrap(); assert_eq!(extracted.len(), data.len(), "Extracted file size must match original"); assert_eq!(extracted, data, "Extracted file content must be byte-identical"); } /// No-compress flag: pack APK file (auto-detected as no-compress), unpack, verify. /// APK extension is in the known compressed extensions list, so compression_flag=0. #[test] fn test_roundtrip_no_compress_flag() { let dir = tempdir().unwrap(); let input_file = dir.path().join("data.apk"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); // 100 bytes of pattern data let data: Vec = (0..100u8).collect(); fs::write(&input_file, &data).unwrap(); cmd() .args(["pack", input_file.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); let extracted = fs::read(output_dir.join("data.apk")).unwrap(); assert_eq!(extracted, data); }