From a73f71e3de70db3296581d83f12369198426ef3e Mon Sep 17 00:00:00 2001 From: Jujuforce Date: Sun, 28 Jun 2026 12:28:00 +0200 Subject: [PATCH] test: add e2e fixture decryption test Decrypts real .app/.opt containers dropped into a git-ignored fixtures/ dir (copied to a temp dir first so extraction never touches the fixtures), asserting each produces a non-empty extraction. No-op when fixtures/ is empty, so the suite stays green without the proprietary sample data. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 ++ README.md | 9 ++++++ tests/e2e.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 tests/e2e.rs diff --git a/.gitignore b/.gitignore index a3c3401..2b39066 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ flamegraph.svg CLAUDE.md memory/ + +# Local e2e test fixtures (real, proprietary containers — never committed) +/fixtures/ diff --git a/README.md b/README.md index b8a30bc..71421d8 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,15 @@ scripts/test.sh --no-docker # skip the Docker/Linux step image to build and test the static Linux binary — the same way release artifacts are produced — so both targets can be validated from any host. +### End-to-end fixtures + +The `tests/e2e.rs` test decrypts real containers placed in a `fixtures/` +directory at the repo root and checks that each extracts successfully. That +folder is git-ignored — the proprietary sample containers are never committed — +and the test is a no-op when it is empty, so just drop a few `.app`/`.opt` files +in `fixtures/` and run `cargo test` (or `scripts/test.sh`) to exercise the full +decrypt-and-extract path. + ## License [BSD Zero Clause License](LICENSE) (0BSD) diff --git a/tests/e2e.rs b/tests/e2e.rs new file mode 100644 index 0000000..b95cad4 --- /dev/null +++ b/tests/e2e.rs @@ -0,0 +1,79 @@ +//! End-to-end decryption test driven by local fixtures. +//! +//! Drop real `.app` / `.opt` containers into the git-ignored `fixtures/` +//! directory at the crate root. Each one is copied to a temp dir, run through +//! the built binary, and checked for a non-empty extraction output. When +//! `fixtures/` is absent or empty the test is a no-op, so the suite stays green +//! without the (proprietary) sample data. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn fixture_files() -> Vec { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures"); + let Ok(entries) = fs::read_dir(dir) else { + return Vec::new(); + }; + let mut files: Vec = entries + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| { + p.is_file() + && matches!( + p.extension().and_then(|e| e.to_str()), + Some("app") | Some("opt") + ) + }) + .collect(); + files.sort(); + files +} + +#[test] +fn decrypts_fixture_containers() { + let files = fixture_files(); + if files.is_empty() { + eprintln!( + "skipping e2e: no .app/.opt fixtures in {}/fixtures — add containers to run this test", + env!("CARGO_MANIFEST_DIR") + ); + return; + } + + // Work in a temp dir so extraction never touches the fixtures folder. The + // tool extracts next to its input, so we copy each fixture in first. + let workdir = std::env::temp_dir().join(format!("fsdecrypt-e2e-{}", std::process::id())); + let _ = fs::remove_dir_all(&workdir); + fs::create_dir_all(&workdir).expect("create work dir"); + + for src in &files { + let name = src.file_name().unwrap().to_os_string(); + let input = workdir.join(&name); + fs::copy(src, &input).expect("copy fixture into work dir"); + + let output = Command::new(env!("CARGO_BIN_EXE_fsdecrypt")) + .arg(&input) + .output() + .expect("failed to run fsdecrypt"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "decrypting {name:?} failed (exit {:?})\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}", + output.status.code() + ); + + // A default run extracts into a folder named after the input (no extension). + let out_dir = input.with_extension(""); + let produced = fs::read_dir(&out_dir).map(|rd| rd.count()).unwrap_or(0); + assert!( + produced > 0, + "{name:?}: expected extracted entries in {}", + out_dir.display() + ); + println!("ok: {name:?} -> {produced} top-level entr(y/ies) extracted"); + } + + let _ = fs::remove_dir_all(&workdir); +}