//! 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 predicates::prelude::*; use std::fs; use std::os::unix::fs::PermissionsExt; 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); } /// Directory round-trip: pack a directory tree, unpack, verify files, empty dirs, and permissions. #[test] fn test_roundtrip_directory() { let dir = tempdir().unwrap(); let testdir = dir.path().join("testdir"); let subdir = testdir.join("subdir"); let emptydir = testdir.join("empty"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); // Create directory structure fs::create_dir_all(&subdir).unwrap(); fs::create_dir_all(&emptydir).unwrap(); fs::write(testdir.join("hello.txt"), b"Hello from dir").unwrap(); fs::write(subdir.join("nested.txt"), b"Nested file").unwrap(); // Set specific permissions fs::set_permissions(&testdir, fs::Permissions::from_mode(0o755)).unwrap(); fs::set_permissions(testdir.join("hello.txt"), fs::Permissions::from_mode(0o644)).unwrap(); fs::set_permissions(&subdir, fs::Permissions::from_mode(0o755)).unwrap(); fs::set_permissions(subdir.join("nested.txt"), fs::Permissions::from_mode(0o755)).unwrap(); fs::set_permissions(&emptydir, fs::Permissions::from_mode(0o700)).unwrap(); // Pack directory cmd() .args(["pack", testdir.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); // Unpack cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); // Verify file contents let hello = fs::read(output_dir.join("testdir/hello.txt")).unwrap(); assert_eq!(hello, b"Hello from dir"); let nested = fs::read(output_dir.join("testdir/subdir/nested.txt")).unwrap(); assert_eq!(nested, b"Nested file"); // Verify empty directory exists assert!( output_dir.join("testdir/empty").is_dir(), "Empty directory should be recreated" ); // Verify permissions let nested_mode = fs::metadata(output_dir.join("testdir/subdir/nested.txt")) .unwrap() .permissions() .mode() & 0o7777; assert_eq!(nested_mode, 0o755, "nested.txt should have mode 0755"); let empty_mode = fs::metadata(output_dir.join("testdir/empty")) .unwrap() .permissions() .mode() & 0o7777; assert_eq!(empty_mode, 0o700, "empty dir should have mode 0700"); } /// Mixed files and directories: pack both a standalone file and a directory, verify round-trip. #[test] fn test_roundtrip_mixed_files_and_dirs() { let dir = tempdir().unwrap(); let standalone = dir.path().join("standalone.txt"); let mydir = dir.path().join("mydir"); let archive = dir.path().join("archive.bin"); let output_dir = dir.path().join("output"); fs::write(&standalone, b"Standalone").unwrap(); fs::create_dir_all(&mydir).unwrap(); fs::write(mydir.join("inner.txt"), b"Inner").unwrap(); // Pack both file and directory cmd() .args([ "pack", standalone.to_str().unwrap(), mydir.to_str().unwrap(), "-o", archive.to_str().unwrap(), ]) .assert() .success(); // Unpack cmd() .args(["unpack", archive.to_str().unwrap(), "-o", output_dir.to_str().unwrap()]) .assert() .success(); // Verify both entries assert_eq!( fs::read(output_dir.join("standalone.txt")).unwrap(), b"Standalone" ); assert_eq!( fs::read(output_dir.join("mydir/inner.txt")).unwrap(), b"Inner" ); } /// Inspect shows directory info: entry type and permissions for directory entries. #[test] fn test_inspect_shows_directory_info() { let dir = tempdir().unwrap(); let testdir = dir.path().join("testdir"); let archive = dir.path().join("archive.bin"); fs::create_dir_all(&testdir).unwrap(); fs::write(testdir.join("file.txt"), b"content").unwrap(); cmd() .args(["pack", testdir.to_str().unwrap(), "-o", archive.to_str().unwrap()]) .assert() .success(); // Inspect and check output contains directory info cmd() .args(["inspect", archive.to_str().unwrap()]) .assert() .success() .stdout(predicate::str::contains("dir")) .stdout(predicate::str::contains("file")) .stdout(predicate::str::contains("0755").or(predicate::str::contains("0775"))) .stdout(predicate::str::contains("Permissions:")); }