use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
fn mnemo(home: &Path) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mnemo"));
cmd.env("HOME", home)
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("XDG_DATA_HOME", home.join(".local/share"))
.stdin(Stdio::null());
cmd
}
fn run(home: &Path, args: &[&str]) -> Output {
mnemo(home).args(args).output().unwrap()
}
fn stdout(out: &Output) -> String {
String::from_utf8(out.stdout.clone()).unwrap()
}
fn db_path(home: &Path) -> PathBuf {
home.join(".local/share/mnemo/history.db")
}
fn init(home: &Path) {
assert!(run(home, &["init"]).status.success());
}
fn seed(home: &Path, command: &str, date: &str) {
let conn = rusqlite::Connection::open(db_path(home)).unwrap();
conn.execute(
"INSERT INTO commands (command, cwd, created_at, hash)
VALUES (?1, '/tmp', ?2, ?3)",
rusqlite::params![command, date, command],
)
.unwrap();
}
fn stored_command(home: &Path, date: &str) -> String {
let conn = rusqlite::Connection::open(db_path(home)).unwrap();
conn.query_row(
"SELECT command FROM commands WHERE created_at = ?1",
rusqlite::params![date],
|row| row.get::<_, String>(0),
)
.unwrap()
}
#[test]
fn scan_affiche_redacte_et_jamais_le_secret_en_clair() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
seed(
home,
"export DB_PASSWORD=s3cr3tvalue",
"2026-06-20 10:00:00",
);
seed(home, "ls -la", "2026-06-20 10:05:00");
let out = run(home, &["secrets", "scan"]);
assert!(out.status.success());
let text = stdout(&out);
assert!(text.contains("[REDACTED]"));
assert!(
!text.contains("s3cr3tvalue"),
"le secret ne doit jamais apparaître en clair : {text}"
);
assert!(!text.contains("ls -la"));
}
#[test]
fn scan_json_valide_sans_secret() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
seed(
home,
"curl -H 'Authorization: Bearer tok3nABC' h", "2026-06-20 10:00:00",
);
let out = run(home, &["secrets", "scan", "--json"]);
assert!(out.status.success());
let text = stdout(&out);
assert!(
!text.contains("tok3nABC"),
"le JSON ne doit pas contenir de secret : {text}"
);
let value: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(value["suspected"], 1);
let cats = value["results"][0]["categories"].as_array().unwrap();
assert!(cats.iter().any(|c| c == "bearer_token"));
}
#[test]
fn redact_dry_run_ne_modifie_pas_la_base() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
let date = "2026-06-20 10:00:00";
seed(home, "export TOKEN=abc123", date);
let out = run(home, &["secrets", "redact"]);
assert!(out.status.success());
assert!(stdout(&out).contains("[dry-run]"));
assert_eq!(stored_command(home, date), "export TOKEN=abc123");
}
#[test]
fn redact_apply_modifie_uniquement_la_colonne_command() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
let date = "2026-06-20 10:00:00";
seed(home, "export TOKEN=abc123secret", date);
let out = run(home, &["secrets", "redact", "--apply", "--yes"]);
assert!(out.status.success(), "{:?}", out);
let text = stdout(&out);
assert!(text.contains("Sauvegarde"));
assert!(text.contains("Commandes modifiées: 1"));
let conn = rusqlite::Connection::open(db_path(home)).unwrap();
let (command, cwd): (String, String) = conn
.query_row(
"SELECT command, cwd FROM commands WHERE created_at = ?1",
rusqlite::params![date],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(command, "export TOKEN=[REDACTED]");
assert_eq!(cwd, "/tmp");
assert!(!command.contains("abc123secret"));
}
#[test]
fn redact_apply_cree_une_sauvegarde() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
seed(home, "export PASSWORD=hunter2pass", "2026-06-20 10:00:00");
let backups_before = backup_count(home);
let out = run(home, &["secrets", "redact", "--apply", "--yes"]);
assert!(out.status.success());
assert!(backup_count(home) > backups_before);
}
#[test]
fn redact_apply_non_interactif_sans_yes_est_refuse() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
let date = "2026-06-20 10:00:00";
seed(home, "export TOKEN=abc123secret", date);
let out = run(home, &["secrets", "redact", "--apply"]);
assert!(out.status.success());
assert!(stdout(&out).contains("annulée"));
assert_eq!(stored_command(home, date), "export TOKEN=abc123secret");
}
#[test]
fn scan_sans_secret_indique_rien() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
init(home);
seed(home, "git status", "2026-06-20 10:00:00");
let out = run(home, &["secrets", "scan"]);
assert!(out.status.success());
assert!(stdout(&out).contains("Aucune commande"));
}
fn backup_count(home: &Path) -> usize {
let dir = home.join(".local/share/mnemo/backups");
match std::fs::read_dir(&dir) {
Ok(entries) => entries.count(),
Err(_) => 0,
}
}