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 <noreply@anthropic.com>
This commit is contained in:
Jujuforce
2026-06-28 12:28:00 +02:00
co-authored by Claude Opus 4.8
parent 0a7b633310
commit a73f71e3de
3 changed files with 91 additions and 0 deletions
+3
View File
@@ -9,3 +9,6 @@
flamegraph.svg
CLAUDE.md
memory/
# Local e2e test fixtures (real, proprietary containers — never committed)
/fixtures/
+9
View File
@@ -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)
+79
View File
@@ -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<PathBuf> {
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<PathBuf> = 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);
}