Files
android-encrypted-archiver/tests/round_trip.rs
NikitolProject 8760981717 test(08-01): add directory round-trip integration tests
- test_roundtrip_directory: full directory tree with permissions verification
- test_roundtrip_mixed_files_and_dirs: mixed file + directory pack/unpack
- test_inspect_shows_directory_info: inspect output contains dir/file types and permissions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:48:20 +03:00

328 lines
10 KiB
Rust

//! 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<u8> = (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<u8> = (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:"));
}