From 91ee354db6e48aa03db3d7bff283ec306c4bbd4f Mon Sep 17 00:00:00 2001 From: NikitolProject Date: Wed, 25 Feb 2026 00:39:33 +0300 Subject: [PATCH] test(03-02): add CLI round-trip integration tests - 6 integration tests via assert_cmd: single file, multiple files, empty, Cyrillic filename, 11MB large, APK no-compress - All tests verify byte-identical extraction (pack -> unpack -> assert_eq) - Uses tempdir() for isolation and parallel safety - Uses cargo_bin! macro (non-deprecated assert_cmd API) - Covers Phase 3 edge cases: 0-byte PKCS7, >10MB file, UTF-8 non-ASCII names Co-Authored-By: Claude Opus 4.6 --- tests/round_trip.rs | 192 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/round_trip.rs diff --git a/tests/round_trip.rs b/tests/round_trip.rs new file mode 100644 index 0000000..b634ff5 --- /dev/null +++ b/tests/round_trip.rs @@ -0,0 +1,192 @@ +//! 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); +}