779 lines
22 KiB
Rust
779 lines
22 KiB
Rust
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
struct Sandbox {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl Sandbox {
|
|
fn new(name: &str) -> Self {
|
|
let path = std::env::temp_dir().join(format!("doot-test-{}", name));
|
|
if path.exists() {
|
|
std::fs::remove_dir_all(&path).unwrap();
|
|
}
|
|
std::fs::create_dir_all(&path).unwrap();
|
|
Self { path }
|
|
}
|
|
|
|
fn run_with_env<F>(&self, args: &[&str], configure: F) -> std::process::Output
|
|
where
|
|
F: FnOnce(&mut Command),
|
|
{
|
|
let doot = env!("CARGO_BIN_EXE_doot");
|
|
let mut command = Command::new(doot);
|
|
command
|
|
.args(args)
|
|
.env("DOOT_HOME", &self.path)
|
|
.env("DOOT_TEST_MODE", "1");
|
|
configure(&mut command);
|
|
command.output().expect("failed to run doot")
|
|
}
|
|
|
|
fn run(&self, args: &[&str]) -> std::process::Output {
|
|
self.run_with_env(args, |_| {})
|
|
}
|
|
|
|
fn config_dir(&self) -> PathBuf {
|
|
self.path.join(".config/doot")
|
|
}
|
|
|
|
fn state_dir(&self) -> PathBuf {
|
|
self.path.join(".local/state/doot")
|
|
}
|
|
|
|
fn config_file(&self) -> PathBuf {
|
|
self.config_dir().join("doot.doot")
|
|
}
|
|
|
|
fn write_config(&self, content: &str) {
|
|
std::fs::create_dir_all(self.config_dir()).unwrap();
|
|
std::fs::write(self.config_file(), content).unwrap();
|
|
}
|
|
|
|
fn write_source(&self, path: &str, content: &str) {
|
|
let full_path = self.config_dir().join(path);
|
|
if let Some(parent) = full_path.parent() {
|
|
std::fs::create_dir_all(parent).unwrap();
|
|
}
|
|
std::fs::write(full_path, content).unwrap();
|
|
}
|
|
|
|
fn is_symlink(&self, path: &PathBuf) -> bool {
|
|
path.is_symlink()
|
|
}
|
|
|
|
fn symlink_target(&self, path: &PathBuf) -> Option<PathBuf> {
|
|
std::fs::read_link(path).ok()
|
|
}
|
|
}
|
|
|
|
impl Drop for Sandbox {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.path);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_init_creates_structure() {
|
|
let sandbox = Sandbox::new("init");
|
|
let output = sandbox.run(&["init"]);
|
|
|
|
assert!(output.status.success(), "init failed: {:?}", output);
|
|
assert!(sandbox.config_file().exists(), "config file not created");
|
|
assert!(
|
|
sandbox.config_dir().join("config").exists(),
|
|
"config dir not created"
|
|
);
|
|
assert!(
|
|
sandbox.state_dir().join("backups").exists(),
|
|
"backups dir not created"
|
|
);
|
|
assert!(
|
|
sandbox.state_dir().join("snapshots").exists(),
|
|
"snapshots dir not created"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_valid_config() {
|
|
let sandbox = Sandbox::new("check-valid");
|
|
sandbox.write_config(
|
|
r#"
|
|
package: "ripgrep"
|
|
package: "fd"
|
|
"#,
|
|
);
|
|
|
|
let output = sandbox.run(&["check"]);
|
|
assert!(output.status.success(), "check failed: {:?}", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_dry_run() {
|
|
let sandbox = Sandbox::new("apply-dry");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "config/test.conf"
|
|
target = "~/.config/test/test.conf"
|
|
"#,
|
|
);
|
|
sandbox.write_source("config/test.conf", "test content");
|
|
|
|
let output = sandbox.run(&["apply", "-n"]);
|
|
assert!(output.status.success(), "apply -n failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/test/test.conf");
|
|
assert!(!target.exists(), "dry run should not create files");
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_creates_symlink() {
|
|
let sandbox = Sandbox::new("apply-symlink");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
deploy = "link"
|
|
"#,
|
|
);
|
|
sandbox.write_source("config/app.conf", "app config content");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(sandbox.is_symlink(&target), "target should be symlink");
|
|
|
|
let expected_source = sandbox.config_dir().join("config/app.conf");
|
|
assert_eq!(
|
|
sandbox.symlink_target(&target),
|
|
Some(expected_source),
|
|
"symlink should point to source"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_unchanged_on_rerun() {
|
|
let sandbox = Sandbox::new("apply-unchanged");
|
|
sandbox.write_config(
|
|
"dotfile:\n source = \"config/app.conf\"\n target = \"~/.config/app/app.conf\"\n deploy = \"link\"\n",
|
|
);
|
|
sandbox.write_source("config/app.conf", "content");
|
|
|
|
let first = sandbox.run(&["apply"]);
|
|
assert!(first.status.success(), "first apply failed");
|
|
|
|
let second = sandbox.run(&["apply"]);
|
|
assert!(second.status.success(), "second apply failed");
|
|
|
|
// Second apply should succeed (symlink already exists and points correctly)
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(
|
|
target.is_symlink(),
|
|
"target should still be symlink after second apply"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_creates_copy() {
|
|
let sandbox = Sandbox::new("apply-copy");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#,
|
|
);
|
|
sandbox.write_source("config/app.conf", "app config content");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(target.exists(), "target should exist");
|
|
assert!(
|
|
!target.is_symlink(),
|
|
"target should be a copy, not a symlink"
|
|
);
|
|
|
|
let content = std::fs::read_to_string(&target).unwrap();
|
|
assert_eq!(content, "app config content", "content should match source");
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_copy_unchanged_on_rerun() {
|
|
let sandbox = Sandbox::new("apply-copy-unchanged");
|
|
sandbox.write_config(
|
|
"dotfile:\n source = \"config/app.conf\"\n target = \"~/.config/app/app.conf\"\n",
|
|
);
|
|
sandbox.write_source("config/app.conf", "content");
|
|
|
|
let first = sandbox.run(&["apply"]);
|
|
assert!(first.status.success(), "first apply failed");
|
|
|
|
let second = sandbox.run(&["apply"]);
|
|
assert!(second.status.success(), "second apply failed");
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(target.exists(), "target should exist after second apply");
|
|
assert!(!target.is_symlink(), "target should still be a copy");
|
|
}
|
|
|
|
#[test]
|
|
fn test_template_redeploys_when_env_changes() {
|
|
let sandbox = Sandbox::new("template-env-change");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "templates/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
template = true
|
|
"#,
|
|
);
|
|
sandbox.write_source("templates/app.conf", "value = {{ env.TEMPLATE_VAL }}\n");
|
|
|
|
let first = sandbox.run_with_env(&["apply"], |cmd| {
|
|
cmd.env("TEMPLATE_VAL", "one");
|
|
});
|
|
assert!(first.status.success(), "first apply failed: {:?}", first);
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(target.exists(), "template target missing after first apply");
|
|
let first_content = std::fs::read_to_string(&target).unwrap();
|
|
assert!(
|
|
first_content.contains("one"),
|
|
"expected first render to contain env value"
|
|
);
|
|
|
|
let second = sandbox.run_with_env(&["apply"], |cmd| {
|
|
cmd.env("TEMPLATE_VAL", "two");
|
|
});
|
|
assert!(second.status.success(), "second apply failed: {:?}", second);
|
|
|
|
let stdout = String::from_utf8_lossy(&second.stdout);
|
|
assert!(
|
|
stdout.contains("[source changed]"),
|
|
"expected source change notice, got: {}",
|
|
stdout
|
|
);
|
|
|
|
let updated = std::fs::read_to_string(&target).unwrap();
|
|
assert!(
|
|
updated.contains("two"),
|
|
"template should reflect new env value"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_status_shows_state() {
|
|
let sandbox = Sandbox::new("status");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#,
|
|
);
|
|
sandbox.write_source("config/app.conf", "content");
|
|
sandbox.run(&["apply"]);
|
|
|
|
let output = sandbox.run(&["status"]);
|
|
assert!(output.status.success(), "status failed: {:?}", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_snapshot_and_rollback() {
|
|
let sandbox = Sandbox::new("snapshot");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#,
|
|
);
|
|
sandbox.write_source("config/app.conf", "v1");
|
|
sandbox.run(&["apply"]);
|
|
|
|
let snap_output = sandbox.run(&["snapshot", "v1"]);
|
|
assert!(
|
|
snap_output.status.success(),
|
|
"snapshot failed: {:?}",
|
|
snap_output
|
|
);
|
|
|
|
let snapshot_file = sandbox.state_dir().join("snapshots/v1.json");
|
|
assert!(snapshot_file.exists(), "snapshot file not created");
|
|
}
|
|
|
|
#[test]
|
|
fn test_dotfile_with_when_condition() {
|
|
let sandbox = Sandbox::new("conditional");
|
|
|
|
// Test that 'when' condition works - only deploy if condition is true
|
|
let config = r#"dotfile:
|
|
source = "config/test.conf"
|
|
target = "~/.config/test.conf"
|
|
when = true
|
|
"#;
|
|
sandbox.write_config(config);
|
|
sandbox.write_source("config/test.conf", "test content");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/test.conf");
|
|
assert!(
|
|
target.exists(),
|
|
"file should be deployed when condition is true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dotfile_when_false_skips() {
|
|
let sandbox = Sandbox::new("when-false");
|
|
|
|
let config = r#"dotfile:
|
|
source = "config/skip.conf"
|
|
target = "~/.config/skip.conf"
|
|
when = false
|
|
"#;
|
|
sandbox.write_config(config);
|
|
sandbox.write_source("config/skip.conf", "should not deploy");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/skip.conf");
|
|
assert!(
|
|
!target.exists(),
|
|
"file should NOT be deployed when condition is false"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_diff_shows_changes() {
|
|
let sandbox = Sandbox::new("diff");
|
|
sandbox.write_config(
|
|
r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#,
|
|
);
|
|
sandbox.write_source("config/app.conf", "new content");
|
|
|
|
let target_dir = sandbox.path.join(".config/app");
|
|
std::fs::create_dir_all(&target_dir).unwrap();
|
|
std::fs::write(target_dir.join("app.conf"), "old content").unwrap();
|
|
|
|
let output = sandbox.run(&["diff"]);
|
|
assert!(output.status.success(), "diff failed: {:?}", output);
|
|
}
|
|
|
|
// ── Keygen tests ──
|
|
|
|
#[test]
|
|
fn test_keygen_creates_identity_and_recipient() {
|
|
let sandbox = Sandbox::new("keygen");
|
|
let output = sandbox.run(&["keygen", "--force"]);
|
|
|
|
assert!(output.status.success(), "keygen failed: {:?}", output);
|
|
|
|
let identity_file = sandbox.config_dir().join("identity.txt");
|
|
let recipient_file = sandbox.config_dir().join("recipient.txt");
|
|
|
|
assert!(identity_file.exists(), "identity.txt not created");
|
|
assert!(recipient_file.exists(), "recipient.txt not created");
|
|
|
|
let identity_content = std::fs::read_to_string(&identity_file).unwrap();
|
|
assert!(
|
|
identity_content.contains("AGE-SECRET-KEY-"),
|
|
"identity should contain secret key"
|
|
);
|
|
assert!(
|
|
identity_content.contains("# public key: age1"),
|
|
"identity should contain public key comment"
|
|
);
|
|
|
|
let recipient_content = std::fs::read_to_string(&recipient_file).unwrap();
|
|
assert!(
|
|
recipient_content.starts_with("age1"),
|
|
"recipient.txt should contain public key"
|
|
);
|
|
|
|
// stdout should print the public key
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.trim().starts_with("age1"),
|
|
"stdout should print public key"
|
|
);
|
|
|
|
// Check identity file permissions on Unix
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let perms = std::fs::metadata(&identity_file).unwrap().permissions();
|
|
assert_eq!(
|
|
perms.mode() & 0o777,
|
|
0o600,
|
|
"identity file should have 0600 permissions"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_keygen_custom_output() {
|
|
let sandbox = Sandbox::new("keygen-custom");
|
|
let custom_path = sandbox.path.join("custom/my-identity.txt");
|
|
let output = sandbox.run(&[
|
|
"keygen",
|
|
"--output",
|
|
custom_path.to_str().unwrap(),
|
|
"--force",
|
|
]);
|
|
|
|
assert!(output.status.success(), "keygen failed: {:?}", output);
|
|
assert!(custom_path.exists(), "custom identity file not created");
|
|
|
|
let recipient_file = sandbox.path.join("custom/recipient.txt");
|
|
assert!(
|
|
recipient_file.exists(),
|
|
"recipient.txt should be created alongside identity"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_keygen_appends_to_existing_recipient() {
|
|
let sandbox = Sandbox::new("keygen-append");
|
|
|
|
// Run keygen twice
|
|
let first = sandbox.run(&["keygen", "--force"]);
|
|
assert!(first.status.success(), "first keygen failed");
|
|
|
|
let first_pubkey = String::from_utf8_lossy(&first.stdout).trim().to_string();
|
|
|
|
let second = sandbox.run(&["keygen", "--force"]);
|
|
assert!(second.status.success(), "second keygen failed");
|
|
|
|
let second_pubkey = String::from_utf8_lossy(&second.stdout).trim().to_string();
|
|
|
|
// recipient.txt should contain the latest public key
|
|
let recipient_content =
|
|
std::fs::read_to_string(sandbox.config_dir().join("recipient.txt")).unwrap();
|
|
|
|
// If keys differ, both should be present
|
|
if first_pubkey != second_pubkey {
|
|
assert!(
|
|
recipient_content.contains(&second_pubkey),
|
|
"recipient.txt should contain second public key"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Encrypt / Decrypt tests ──
|
|
|
|
#[test]
|
|
fn test_encrypt_and_decrypt_file() {
|
|
let sandbox = Sandbox::new("encrypt-decrypt");
|
|
|
|
// Generate keypair first
|
|
let keygen_output = sandbox.run(&["keygen", "--force"]);
|
|
assert!(keygen_output.status.success(), "keygen failed");
|
|
|
|
let pubkey = String::from_utf8_lossy(&keygen_output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
|
|
// Create a plaintext file
|
|
let plaintext_file = sandbox.path.join("secret.txt");
|
|
std::fs::write(&plaintext_file, "hello world").unwrap();
|
|
|
|
// Encrypt it
|
|
let encrypt_output = sandbox.run(&[
|
|
"encrypt",
|
|
plaintext_file.to_str().unwrap(),
|
|
"--recipient",
|
|
&pubkey,
|
|
]);
|
|
assert!(
|
|
encrypt_output.status.success(),
|
|
"encrypt failed: {:?}",
|
|
encrypt_output
|
|
);
|
|
|
|
let encrypted_file = sandbox.path.join("secret.txt.age");
|
|
assert!(encrypted_file.exists(), "encrypted file not created");
|
|
|
|
// Encrypted file should NOT contain plaintext
|
|
let encrypted_bytes = std::fs::read(&encrypted_file).unwrap();
|
|
assert_ne!(
|
|
encrypted_bytes, b"hello world",
|
|
"encrypted file should not be plaintext"
|
|
);
|
|
|
|
// Decrypt it
|
|
let identity_file = sandbox.config_dir().join("identity.txt");
|
|
let decrypt_output = sandbox.run(&[
|
|
"decrypt",
|
|
encrypted_file.to_str().unwrap(),
|
|
"--identity",
|
|
identity_file.to_str().unwrap(),
|
|
"--output",
|
|
sandbox.path.join("decrypted.txt").to_str().unwrap(),
|
|
]);
|
|
assert!(
|
|
decrypt_output.status.success(),
|
|
"decrypt failed: {:?}",
|
|
decrypt_output
|
|
);
|
|
|
|
let decrypted = std::fs::read_to_string(sandbox.path.join("decrypted.txt")).unwrap();
|
|
assert_eq!(decrypted, "hello world", "decrypted content should match");
|
|
}
|
|
|
|
#[test]
|
|
fn test_encrypt_decrypt_to_stdout() {
|
|
let sandbox = Sandbox::new("decrypt-stdout");
|
|
|
|
let keygen_output = sandbox.run(&["keygen", "--force"]);
|
|
assert!(keygen_output.status.success());
|
|
|
|
let pubkey = String::from_utf8_lossy(&keygen_output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
|
|
let plaintext_file = sandbox.path.join("data.txt");
|
|
std::fs::write(&plaintext_file, "secret data").unwrap();
|
|
|
|
let encrypt_output = sandbox.run(&[
|
|
"encrypt",
|
|
plaintext_file.to_str().unwrap(),
|
|
"--recipient",
|
|
&pubkey,
|
|
]);
|
|
assert!(encrypt_output.status.success());
|
|
|
|
let encrypted_file = sandbox.path.join("data.txt.age");
|
|
let identity_file = sandbox.config_dir().join("identity.txt");
|
|
|
|
// Decrypt to stdout (no --output)
|
|
let decrypt_output = sandbox.run(&[
|
|
"decrypt",
|
|
encrypted_file.to_str().unwrap(),
|
|
"--identity",
|
|
identity_file.to_str().unwrap(),
|
|
]);
|
|
assert!(
|
|
decrypt_output.status.success(),
|
|
"decrypt to stdout failed: {:?}",
|
|
decrypt_output
|
|
);
|
|
|
|
let stdout = String::from_utf8_lossy(&decrypt_output.stdout);
|
|
assert_eq!(stdout, "secret data", "stdout should contain plaintext");
|
|
}
|
|
|
|
#[test]
|
|
fn test_encrypt_no_recipient_fails() {
|
|
let sandbox = Sandbox::new("encrypt-no-recipient");
|
|
|
|
let plaintext_file = sandbox.path.join("file.txt");
|
|
std::fs::write(&plaintext_file, "data").unwrap();
|
|
|
|
// No recipient provided and no recipient.txt exists
|
|
let output = sandbox.run(&["encrypt", plaintext_file.to_str().unwrap()]);
|
|
assert!(
|
|
!output.status.success(),
|
|
"encrypt without recipient should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decrypt_no_identity_fails() {
|
|
let sandbox = Sandbox::new("decrypt-no-identity");
|
|
|
|
let dummy_file = sandbox.path.join("dummy.age");
|
|
std::fs::write(&dummy_file, "not really encrypted").unwrap();
|
|
|
|
let output = sandbox.run(&["decrypt", dummy_file.to_str().unwrap()]);
|
|
assert!(
|
|
!output.status.success(),
|
|
"decrypt without identity should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_encrypt_var_and_decrypt_var() {
|
|
let sandbox = Sandbox::new("encrypt-decrypt-var");
|
|
|
|
let keygen_output = sandbox.run(&["keygen", "--force"]);
|
|
assert!(keygen_output.status.success());
|
|
|
|
let pubkey = String::from_utf8_lossy(&keygen_output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
let identity_file = sandbox.config_dir().join("identity.txt");
|
|
|
|
// Encrypt a var (VALUE is a positional argument)
|
|
let encrypt_output = sandbox.run(&["encrypt-var", "my-secret-value", "--recipient", &pubkey]);
|
|
assert!(
|
|
encrypt_output.status.success(),
|
|
"encrypt-var failed: {:?}",
|
|
encrypt_output
|
|
);
|
|
|
|
let encrypted_b64 = String::from_utf8_lossy(&encrypt_output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
assert!(
|
|
!encrypted_b64.is_empty(),
|
|
"encrypted var should produce base64 output"
|
|
);
|
|
|
|
// Decrypt it back (VALUE is a positional argument)
|
|
let decrypt_output = sandbox.run(&[
|
|
"decrypt-var",
|
|
&encrypted_b64,
|
|
"--identity",
|
|
identity_file.to_str().unwrap(),
|
|
]);
|
|
assert!(
|
|
decrypt_output.status.success(),
|
|
"decrypt-var failed: {:?}",
|
|
decrypt_output
|
|
);
|
|
|
|
let decrypted = String::from_utf8_lossy(&decrypt_output.stdout);
|
|
assert_eq!(
|
|
decrypted, "my-secret-value",
|
|
"decrypted var should match original"
|
|
);
|
|
}
|
|
|
|
// ── Reencrypt tests ──
|
|
|
|
#[test]
|
|
fn test_reencrypt_age_files() {
|
|
let sandbox = Sandbox::new("reencrypt");
|
|
|
|
// Generate two keypairs
|
|
let keygen1 = sandbox.run(&["keygen", "--force"]);
|
|
assert!(keygen1.status.success());
|
|
let pubkey1 = String::from_utf8_lossy(&keygen1.stdout).trim().to_string();
|
|
|
|
// Save first identity for decryption
|
|
let identity_content =
|
|
std::fs::read_to_string(sandbox.config_dir().join("identity.txt")).unwrap();
|
|
|
|
let keygen2 = sandbox.run(&["keygen", "--force"]);
|
|
assert!(keygen2.status.success());
|
|
let pubkey2 = String::from_utf8_lossy(&keygen2.stdout).trim().to_string();
|
|
|
|
// Create a plaintext file and encrypt with first key only
|
|
let plaintext_file = sandbox.path.join("secret.txt");
|
|
std::fs::write(&plaintext_file, "reencrypt me").unwrap();
|
|
|
|
let encrypt_output = sandbox.run(&[
|
|
"encrypt",
|
|
plaintext_file.to_str().unwrap(),
|
|
"--recipient",
|
|
&pubkey1,
|
|
]);
|
|
assert!(encrypt_output.status.success());
|
|
|
|
// Set up config with encrypted file reference in secrets dir
|
|
let secrets_dir = sandbox.config_dir().join("secrets");
|
|
std::fs::create_dir_all(&secrets_dir).unwrap();
|
|
std::fs::copy(
|
|
sandbox.path.join("secret.txt.age"),
|
|
secrets_dir.join("secret.txt.age"),
|
|
)
|
|
.unwrap();
|
|
|
|
sandbox.write_config(
|
|
r#"
|
|
package: "git"
|
|
"#,
|
|
);
|
|
|
|
// Write the first identity back (reencrypt needs it to decrypt)
|
|
std::fs::write(sandbox.config_dir().join("identity.txt"), &identity_content).unwrap();
|
|
|
|
// Reencrypt with both recipients
|
|
let reencrypt_output = sandbox.run(&[
|
|
"reencrypt",
|
|
"--recipient",
|
|
&pubkey1,
|
|
"--recipient",
|
|
&pubkey2,
|
|
]);
|
|
assert!(
|
|
reencrypt_output.status.success(),
|
|
"reencrypt failed: {:?}\nstderr: {}",
|
|
reencrypt_output,
|
|
String::from_utf8_lossy(&reencrypt_output.stderr)
|
|
);
|
|
|
|
// The re-encrypted file should still be decryptable with the first identity
|
|
let identity_file = sandbox.config_dir().join("identity.txt");
|
|
let decrypt_output = sandbox.run(&[
|
|
"decrypt",
|
|
secrets_dir.join("secret.txt.age").to_str().unwrap(),
|
|
"--identity",
|
|
identity_file.to_str().unwrap(),
|
|
"--output",
|
|
sandbox.path.join("decrypted.txt").to_str().unwrap(),
|
|
]);
|
|
assert!(
|
|
decrypt_output.status.success(),
|
|
"decrypt after reencrypt failed: {:?}",
|
|
decrypt_output
|
|
);
|
|
|
|
let decrypted = std::fs::read_to_string(sandbox.path.join("decrypted.txt")).unwrap();
|
|
assert_eq!(
|
|
decrypted, "reencrypt me",
|
|
"content should survive reencryption"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reencrypt_no_identity_fails() {
|
|
let sandbox = Sandbox::new("reencrypt-no-id");
|
|
sandbox.write_config("package: \"git\"\n");
|
|
|
|
let output = sandbox.run(&[
|
|
"reencrypt",
|
|
"--recipient",
|
|
"age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
]);
|
|
assert!(
|
|
!output.status.success(),
|
|
"reencrypt without identity should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reencrypt_no_age_files_reports_zero() {
|
|
let sandbox = Sandbox::new("reencrypt-none");
|
|
|
|
let keygen_output = sandbox.run(&["keygen", "--force"]);
|
|
assert!(keygen_output.status.success());
|
|
let pubkey = String::from_utf8_lossy(&keygen_output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
|
|
sandbox.write_config("package: \"git\"\n");
|
|
|
|
let output = sandbox.run(&["reencrypt", "--recipient", &pubkey]);
|
|
assert!(output.status.success(), "reencrypt failed: {:?}", output);
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("no .age files found"),
|
|
"should report no files to reencrypt, got: {}",
|
|
stdout
|
|
);
|
|
}
|