feat(cli/org): org init — structure + wrap + configure_git_signing + signed bootstrap commit
This commit is contained in:
137
crates/relicario-cli/tests/org_init_signing.rs
Normal file
137
crates/relicario-cli/tests/org_init_signing.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn relicario(config_home: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_relicario"))
|
||||
.env("XDG_CONFIG_HOME", config_home)
|
||||
.env("HOME", config_home) // belt-and-suspenders for dirs on all platforms
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run relicario")
|
||||
}
|
||||
|
||||
/// Like relicario() but also injects the git committer identity so that
|
||||
/// `git commit` inside `org init` doesn't fail with "Please tell me who you are."
|
||||
fn relicario_with_git_identity(config_home: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_relicario"))
|
||||
.env("XDG_CONFIG_HOME", config_home)
|
||||
.env("HOME", config_home)
|
||||
.env("GIT_AUTHOR_NAME", "Test Device")
|
||||
.env("GIT_AUTHOR_EMAIL", "test@relicario.test")
|
||||
.env("GIT_COMMITTER_NAME", "Test Device")
|
||||
.env("GIT_COMMITTER_EMAIL", "test@relicario.test")
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run relicario")
|
||||
}
|
||||
|
||||
fn git(repo: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new("git")
|
||||
.current_dir(repo)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("run git")
|
||||
}
|
||||
|
||||
/// Lay out device keys directly under `<config_home>/relicario/devices/<name>/`
|
||||
/// and set `devices/current` — mirrors the B2 seed_helper_tests approach.
|
||||
/// Returns the OpenSSH public key string so the caller can build an allowed_signers
|
||||
/// file for `git verify-commit`.
|
||||
fn seed_device(config_home: &Path, name: &str) -> String {
|
||||
let (priv_openssh, pub_openssh) =
|
||||
relicario_core::device::generate_keypair().expect("generate_keypair");
|
||||
|
||||
let dev_dir = config_home
|
||||
.join("relicario")
|
||||
.join("devices")
|
||||
.join(name);
|
||||
fs::create_dir_all(&dev_dir).expect("create device dir");
|
||||
let signing_key_path = dev_dir.join("signing.key");
|
||||
fs::write(&signing_key_path, priv_openssh.as_str())
|
||||
.expect("write signing.key");
|
||||
// ssh requires 0600 on private key files or it refuses to use them.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&signing_key_path, fs::Permissions::from_mode(0o600))
|
||||
.expect("chmod signing.key");
|
||||
}
|
||||
fs::write(dev_dir.join("signing.pub"), &pub_openssh)
|
||||
.expect("write signing.pub");
|
||||
// Also write stub deploy key files so configure_git_signing doesn't trip on
|
||||
// a missing deploy.key path (the git config value just points to the file;
|
||||
// the file itself is never read during org init).
|
||||
fs::write(dev_dir.join("deploy.key"), "").expect("write stub deploy.key");
|
||||
fs::write(dev_dir.join("deploy.pub"), "").expect("write stub deploy.pub");
|
||||
|
||||
// Set this device as current.
|
||||
let devices_dir = config_home.join("relicario").join("devices");
|
||||
fs::write(devices_dir.join("current"), format!("{name}\n"))
|
||||
.expect("write current");
|
||||
|
||||
pub_openssh
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn org_init_produces_a_signed_initial_commit() {
|
||||
let cfg = TempDir::new().unwrap();
|
||||
let org = TempDir::new().unwrap();
|
||||
|
||||
// Lay out the device key directly (no `device add` needed — it requires Gitea).
|
||||
let pub_openssh = seed_device(cfg.path(), "test-dev");
|
||||
|
||||
// Initialize the org vault. `--dir` comes AFTER `org init` (B14 global).
|
||||
// Inject git identity so the commit doesn't fail "Please tell me who you are."
|
||||
let init = relicario_with_git_identity(
|
||||
cfg.path(),
|
||||
&["org", "init", "--dir", org.path().to_str().unwrap(), "--name", "Acme"],
|
||||
);
|
||||
assert!(
|
||||
init.status.success(),
|
||||
"org init failed:\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&init.stdout),
|
||||
String::from_utf8_lossy(&init.stderr)
|
||||
);
|
||||
|
||||
// The org repo must be configured to sign.
|
||||
let cfg_out = git(org.path(), &["config", "commit.gpgsign"]);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&cfg_out.stdout).trim(),
|
||||
"true",
|
||||
"org repo must have commit.gpgsign=true"
|
||||
);
|
||||
|
||||
// The HEAD commit object must carry a signature header.
|
||||
let head = git(org.path(), &["cat-file", "commit", "HEAD"]);
|
||||
let body = String::from_utf8_lossy(&head.stdout);
|
||||
assert!(
|
||||
body.contains("gpgsig "),
|
||||
"HEAD commit must be signed (no gpgsig header found):\n{body}"
|
||||
);
|
||||
|
||||
// Configure an allowed_signers file so `git verify-commit` can validate the
|
||||
// SSH signature. The principal must match the committer email injected above.
|
||||
let allowed_signers_path = cfg.path().join("allowed_signers");
|
||||
let allowed_line = format!("test@relicario.test {}", pub_openssh.trim());
|
||||
fs::write(&allowed_signers_path, format!("{allowed_line}\n"))
|
||||
.expect("write allowed_signers");
|
||||
git(
|
||||
org.path(),
|
||||
&[
|
||||
"config",
|
||||
"gpg.ssh.allowedSignersFile",
|
||||
allowed_signers_path.to_str().unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
// Now verify-commit should succeed.
|
||||
let verify = git(org.path(), &["verify-commit", "HEAD"]);
|
||||
assert!(
|
||||
verify.status.success(),
|
||||
"git verify-commit HEAD failed:\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&verify.stdout),
|
||||
String::from_utf8_lossy(&verify.stderr)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user