use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
const BINARY: &str = env!("CARGO_BIN_EXE_secrets-le");
static COUNTER: AtomicUsize = AtomicUsize::new(0);
const LINE: &str = "DATABASE_PASSWORD=hunter2hunter2";
struct Tree {
root: PathBuf,
}
impl Tree {
fn new(name: &str) -> Self {
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"secrets-le-platform-{name}-{}-{unique}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a temporary directory");
Self {
root: std::fs::canonicalize(&root).expect("a canonical directory"),
}
}
fn path(&self) -> &Path {
&self.root
}
fn write(&self, relative: &str, contents: &str) -> PathBuf {
let target = self.root.join(relative);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).expect("a parent directory");
}
std::fs::write(&target, contents).expect("a file");
target
}
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
struct Run {
code: i32,
stdout: String,
stderr: String,
}
fn run_with(args: &[&str], timezone: Option<&str>) -> Run {
let mut command = Command::new(BINARY);
command.args(args);
match timezone {
Some(zone) => command.env("TZ", zone),
None => command.env_remove("TZ"),
};
let output = command.output().expect("the binary runs");
Run {
code: output.status.code().expect("an exit code, not a signal"),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
fn run(args: &[&str]) -> Run {
run_with(args, Some("UTC"))
}
fn reports(run: &Run) -> Vec<serde_json::Value> {
run.stdout
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).expect("stdout carries only JSON"))
.collect()
}
fn files(reports: &[serde_json::Value]) -> Vec<String> {
reports
.iter()
.map(|report| report["file"].as_str().expect("a file").to_string())
.collect()
}
#[test]
fn every_path_in_the_report_uses_forward_slashes() {
let tree = Tree::new("separators");
tree.write("config/nested/deeper/app.env", &format!("{LINE}\n"));
tree.write("top.env", &format!("{LINE}\n"));
let argument = tree.path().to_string_lossy().into_owned();
assert!(
argument.contains(std::path::MAIN_SEPARATOR),
"the input path has no separator at all, so this proves nothing: {argument}"
);
let run = run(&[&argument]);
assert_eq!(run.code, 1, "{}", run.stderr);
let reported = files(&reports(&run));
assert!(!reported.is_empty(), "nothing was reported");
for file in &reported {
assert!(
!file.contains('\\'),
"a report path used a backslash: {file}\n\
stdout is protocol and must read the same on every platform"
);
assert!(
file.contains('/'),
"a report path has no separator at all, so nothing was converted: {file}"
);
}
assert!(
reported
.iter()
.any(|file| file.ends_with("config/nested/deeper/app.env")),
"the nested path was not reported with forward slashes: {reported:?}"
);
assert!(
!run.stderr.contains(".env\\") && !run.stderr.contains("config\\"),
"{}",
run.stderr
);
}
#[cfg(unix)]
#[test]
fn a_backslash_inside_a_unix_filename_is_not_a_separator() {
let tree = Tree::new("backslash");
tree.write("od\\d.env", &format!("{LINE}\n"));
let run = run(&[&tree.path().to_string_lossy()]);
assert_eq!(run.code, 1, "{}", run.stderr);
assert!(
files(&reports(&run))
.iter()
.any(|file| file.ends_with("od\\d.env")),
"the backslash in the name was rewritten as a separator"
);
}
#[cfg(not(unix))]
#[test]
fn a_backslash_inside_a_unix_filename_is_not_a_separator() {
eprintln!("SKIPPED a backslash in a filename: Windows reserves it as the separator");
}
#[test]
fn the_answer_does_not_depend_on_the_timezone() {
let tree = Tree::new("timezone");
tree.write("app.env", &format!("{LINE}\n"));
tree.write("clean.js", "const total = 1 + 2;\n");
let arguments = [tree.path().to_string_lossy().to_string()];
let borrowed: Vec<&str> = arguments.iter().map(String::as_str).collect();
let utc = run_with(&borrowed, Some("UTC"));
let unset = run_with(&borrowed, None);
let elsewhere = run_with(&borrowed, Some("Pacific/Kiritimati"));
assert_eq!(utc.stdout, unset.stdout, "TZ=UTC and no TZ disagree");
assert_eq!(
utc.stdout, elsewhere.stdout,
"the timezone changed a report"
);
assert_eq!(utc.stderr, unset.stderr);
assert_eq!(utc.code, unset.code);
}
#[test]
fn a_case_insensitive_filesystem_does_not_report_one_file_twice() {
let tree = Tree::new("case-folding");
tree.write("README.md", &format!("{LINE}\n"));
tree.write("readme.md", &format!("{LINE}\n"));
let on_disk = std::fs::read_dir(tree.path())
.expect("the tree is readable")
.filter_map(Result::ok)
.count();
let insensitive = on_disk == 1;
eprintln!(
"case folding: this filesystem holds {on_disk} file(s) for README.md and readme.md \
({})",
if insensitive {
"case-insensitive"
} else {
"case-sensitive"
}
);
let run = run(&[&tree.path().to_string_lossy()]);
assert_eq!(run.code, 1, "{}", run.stderr);
let mut reported = files(&reports(&run));
assert_eq!(
reported.len(),
on_disk,
"the walk reported {} file(s) for {on_disk} on disk: {reported:?}",
reported.len()
);
let before = reported.len();
reported.sort();
reported.dedup();
assert_eq!(reported.len(), before, "a file was reported twice");
}
#[test]
fn a_reserved_windows_filename_does_not_stop_the_walk() {
let tree = Tree::new("reserved");
tree.write("ordinary.env", &format!("{LINE}\n"));
let mut created = Vec::new();
for name in ["CON", "PRN", "AUX", "NUL", "COM1"] {
match std::fs::write(tree.path().join(name), format!("{LINE}\n")) {
Ok(()) => created.push(name),
Err(error) => eprintln!("SKIPPED reserved name {name}: {error}"),
}
}
let run = run(&[&tree.path().to_string_lossy()]);
assert_eq!(
run.code, 1,
"the walk did not survive the reserved names\n{}",
run.stderr
);
let reported = files(&reports(&run));
assert!(
reported.iter().any(|file| file.ends_with("ordinary.env")),
"the file beside them was lost: {reported:?}"
);
for name in created {
assert!(
reported.iter().any(|file| file.ends_with(name)),
"{name} was created but never reported: {reported:?}"
);
}
}
#[test]
fn a_child_that_refuses_stdin_still_answers_with_its_exit_code() {
let tree = Tree::new("stdin-race");
let file = tree.write("app.env", &format!("{LINE}\n"));
let mut child = Command::new(BINARY)
.args(["--stdin", &file.to_string_lossy()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the binary runs");
let mut pipe = child.stdin.take().expect("stdin");
let _ = pipe.write_all(&vec![b'x'; 1_000_000]);
let _ = pipe.flush();
drop(pipe);
let output = child.wait_with_output().expect("finishes");
assert_eq!(
output.status.code(),
Some(2),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"a refusal writes no report: {}",
String::from_utf8_lossy(&output.stdout)
);
}