crates/cli/git-ents/tests/common/mod.rs
| 1 | //! Shared test fixtures for `git-ents`'s integration suite: a real, |
| 2 | //! on-disk repository plus a deterministic signing key — the counterpart |
| 3 | //! to `ents-testutil`'s in-memory fixtures, needed here because this |
| 4 | //! crate's composition roots ([`git_ents::root::LocalRoot`], |
| 5 | //! [`git_ents::root::HostedRoot`]) wire a real `LooseRefStore` and a real |
| 6 | //! odb, not the in-memory `MemRefStore`/`ObjectStore` pair every library |
| 7 | //! crate's own tests use. |
| 8 | |
| 9 | #![allow(dead_code, reason = "not every test file uses every helper")] |
| 10 | #![allow(clippy::expect_used, reason = "integration test")] |
| 11 | |
| 12 | use std::path::{Path, PathBuf}; |
| 13 | use std::process::Command; |
| 14 | |
| 15 | use ssh_key::private::{Ed25519Keypair, KeypairData}; |
| 16 | use ssh_key::{LineEnding, PrivateKey}; |
| 17 | use tempfile::TempDir; |
| 18 | |
| 19 | /// A real, empty git repository plus a deterministic signing key, ready |
| 20 | /// for [`git_ents::root::LocalRoot::open`] or [`git_ents::root::HostedRoot::open`]. |
| 21 | pub struct Fixture { |
| 22 | pub dir: TempDir, |
| 23 | pub key_path: PathBuf, |
| 24 | } |
| 25 | |
| 26 | impl Fixture { |
| 27 | /// Initialize a fresh, non-bare repository with a deterministic |
| 28 | /// ed25519 signing key at `<repo>/../id_ed25519`, seeded by `seed`. |
| 29 | pub fn new(seed: u8) -> Self { |
| 30 | let dir = tempfile::tempdir().expect("tempdir"); |
| 31 | gix::init(dir.path()).expect("init"); |
| 32 | let key_path = dir.path().join(".id_ed25519"); |
| 33 | write_key(&key_path, seed); |
| 34 | Self { dir, key_path } |
| 35 | } |
| 36 | |
| 37 | /// Initialize a fresh *bare* repository (the single-node hosted root's |
| 38 | /// shape) with a deterministic signing key. |
| 39 | pub fn new_bare(seed: u8) -> Self { |
| 40 | let dir = tempfile::tempdir().expect("tempdir"); |
| 41 | gix::init_bare(dir.path()).expect("init bare"); |
| 42 | let key_path = dir.path().join(".id_ed25519"); |
| 43 | write_key(&key_path, seed); |
| 44 | Self { dir, key_path } |
| 45 | } |
| 46 | |
| 47 | pub fn path(&self) -> &Path { |
| 48 | self.dir.path() |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | /// Write an executable script at `path` that overwrites its one argument |
| 53 | /// (the scratch file a composing command opens) with `contents` — a |
| 54 | /// stand-in for a real `$EDITOR`, exercising the same spawn-and-read-back |
| 55 | /// path a real editor would. |
| 56 | pub fn write_fake_editor(path: &Path, contents: &str) { |
| 57 | let script = format!("#!/bin/sh\ncat > \"$1\" <<'EOF'\n{contents}\nEOF\n"); |
| 58 | std::fs::write(path, script).expect("write fake editor"); |
| 59 | #[cfg(unix)] |
| 60 | { |
| 61 | use std::os::unix::fs::PermissionsExt as _; |
| 62 | let mut perms = std::fs::metadata(path).expect("metadata").permissions(); |
| 63 | perms.set_mode(0o755); |
| 64 | std::fs::set_permissions(path, perms).expect("chmod"); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | /// Write a deterministic key inside `dir` (as `.id_ed25519`) and return its |
| 69 | /// path — for tests that need a key living alongside a specific working |
| 70 | /// directory (a clone) rather than a [`Fixture`]'s own repo directory. |
| 71 | pub fn write_key_in(dir: &Path, seed: u8) -> PathBuf { |
| 72 | let path = dir.join(".id_ed25519"); |
| 73 | write_key(&path, seed); |
| 74 | path |
| 75 | } |
| 76 | |
| 77 | /// Write a deterministic, unencrypted ed25519 key at `path` — the fixture |
| 78 | /// counterpart to `ents_testutil::Keypair::from_seed`, but a real file a |
| 79 | /// [`git_ents::sign::Signer`] can load. |
| 80 | pub fn write_key(path: &Path, seed: u8) { |
| 81 | let pair = Ed25519Keypair::from_seed(&[seed; 32]); |
| 82 | let key = PrivateKey::new(KeypairData::from(pair), "git-ents-test").expect("well-formed"); |
| 83 | key.write_openssh_file(path, LineEnding::LF) |
| 84 | .expect("write key"); |
| 85 | } |
| 86 | |
| 87 | /// Configure `dir`'s own local git config to sign with `key` |
| 88 | /// (`user.signingkey` + `gpg.format=ssh`) — what a real operator's |
| 89 | /// `git ents setup` does for a clone, needed here so `git push |
| 90 | /// --signed=if-asked` against the hosted root (which now always |
| 91 | /// advertises `push-cert`) actually produces a certificate instead of |
| 92 | /// silently pushing unsigned. |
| 93 | pub fn configure_signing(dir: &Path, key: &Path) { |
| 94 | for (name, value) in [ |
| 95 | ("user.signingkey", key.to_str().expect("utf8 path")), |
| 96 | ("gpg.format", "ssh"), |
| 97 | ] { |
| 98 | let output = Command::new("git") |
| 99 | .arg("-C") |
| 100 | .arg(dir) |
| 101 | .args(["config", "--local", name, value]) |
| 102 | .output() |
| 103 | .expect("git runs"); |
| 104 | assert!(output.status.success(), "{output:?}"); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /// The path to the built `git-ents` binary under test — `cargo test` |
| 109 | /// exposes this via `CARGO_BIN_EXE_<name>`. |
| 110 | pub fn bin_path() -> PathBuf { |
| 111 | PathBuf::from(env!("CARGO_BIN_EXE_git-ents")) |
| 112 | } |