Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257//! Shared scaffolding for tests. Compiled only under `cfg(test)`.//!//! Everything here exists to keep tests hermetic — see [`crate::docs::testing`]. Two//! things live here: a throwaway git repo with the process parked inside it,//! which several modules need because the code under test shells out to git//! in the working directory rather than against an explicit path, and a//! throwaway SSH keypair, which `ssh.rs` and `key.rs` need because the//! thing they get right or wrong is how real key material compares.
use std::path::{Path, PathBuf};use std::process::Command;use std::sync::{Mutex, MutexGuard};
use tempfile::TempDir;
/// The working directory is process-wide and cargo runs tests on threads that/// share it, so anything that moves the process has to take this first./// [`TempRepo`] is the only thing in the tree that does.fn cwd_lock() -> MutexGuard<'static, ()> { static LOCK: Mutex<()> = Mutex::new(()); // A panicking test poisons the lock; the data behind it is `()`, so there // is nothing to have corrupted and the next test may proceed. LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())}
/// A throwaway git repo, with the process inside it for as long as the value/// lives. Everything it does is confined to a temp directory: no command run/// through it writes outside `path`, so a test that sets git config can only/// reach this repo's `.git/config` and never the user's.pub struct TempRepo { dir: TempDir, previous: PathBuf, _guard: MutexGuard<'static, ()>,}
impl TempRepo { pub fn new(label: &str) -> Self { let guard = cwd_lock(); let previous = std::env::current_dir().expect("a working directory");
// `tempfile` picks the name, so two repos cannot collide however the // suite is run — the old scheme was unique per pid and per call, // which assumed one process at a time. let dir = tempfile::Builder::new() .prefix(&format!("atgc-test-{label}-")) .tempdir() .expect("temp dir");
// -b main so the branch does not depend on whatever // init.defaultBranch the machine running the tests has set. run_git(dir.path(), &["init", "-q", "-b", "main"]); std::env::set_current_dir(dir.path()).expect("enter the temp repo");
TempRepo { dir, previous, _guard: guard, } }
fn path(&self) -> &Path { self.dir.path() }
pub fn commit(&self, name: &str, body: &str, subject: &str) { std::fs::write(self.path().join(name), body).expect("write a file"); run_git(self.path(), &["add", name]); run_git(self.path(), &["commit", "-q", "-m", subject]); }
pub fn git(&self, args: &[&str]) -> String { run_git(self.path(), args) }
/// This repo's own config file, to prove a write landed here and not in /// the user's `~/.gitconfig`. pub fn local_config(&self) -> String { std::fs::read_to_string(self.path().join(".git/config")).expect("a local config") }}
impl Drop for TempRepo { fn drop(&mut self) { // Leave the directory before it is deleted, and restore whatever the // harness was in — a later test may be relative to it. `dir` drops // straight after this and takes the tree with it. let _ = std::env::set_current_dir(&self.previous); }}
/// A throwaway SSH keypair in a directory of its own, generated by/// `ssh-keygen` and deleted on drop.////// Real key material rather than a pasted-in constant, because what the key/// tests are about is the agreement between three things this project does/// not control: what `ssh-keygen` writes into a `.pub` file, what a/// `sh.tangled.publicKey` record holds, and what `ssh-keygen -lf` prints as/// the fingerprint. A hardcoded key would pin our own reading of all three/// and prove none of them.////// It needs no cwd lock: nothing here moves the process, and every path is/// passed in explicitly. `ssh-keygen` is a hard requirement of the suite, the/// way `git` already is — a machine that can push to Tangled has it.pub struct TempKeys { dir: TempDir,}
impl TempKeys { pub fn new(label: &str) -> Self { let dir = tempfile::Builder::new() .prefix(&format!("atgc-test-keys-{label}-")) // 0o700, because these stand in for ~/.config/atgc, which // production creates owner-only. `tempfile` defaults a // directory to 0o777 & ~umask, which would quietly make // every mode assertion below weaker than the real thing. .permissions( <std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(0o700), ) .tempdir() .expect("temp dir");
let keys = TempKeys { dir }; // -N "" is an empty passphrase and -q keeps the randomart off the // test output. The comment is what a real key carries and what // `normalize` has to strip. keys.keygen(&[ "-t", "ed25519", "-N", "", "-q", "-C", &format!("atgc-test-{label}"), "-f", &keys.private().to_string_lossy(), ]); keys }
/// The directory holding the pair, which stands in for `~/.ssh`. pub fn dir(&self) -> &Path { self.dir.path() }
pub fn private(&self) -> PathBuf { self.dir.path().join("id_ed25519") }
pub fn public(&self) -> PathBuf { self.dir.path().join("id_ed25519.pub") }
/// The `.pub` file's contents: `ssh-ed25519 <blob> atgc-test-<label>`. pub fn public_line(&self) -> String { std::fs::read_to_string(self.public()) .expect("a generated public key") .trim() .to_string() }
/// What `ssh-keygen -lf` calls this key — `SHA256:…`, the second field of /// its one line of output. pub fn fingerprint(&self) -> String { let listed = self.keygen(&["-lf", &self.public().to_string_lossy()]); listed .split_whitespace() .nth(1) .expect("ssh-keygen -lf prints `bits SHA256:… comment (TYPE)`") .to_string() }
fn keygen(&self, args: &[&str]) -> String { let out = Command::new("ssh-keygen") .args(args) .output() .expect("ssh-keygen should be on PATH"); assert!( out.status.success(), "ssh-keygen {args:?} failed: {}", String::from_utf8_lossy(&out.stderr) ); String::from_utf8(out.stdout) .expect("utf-8") .trim() .to_string() }}
/// Run git against `dir` explicitly, so setting a repo up never depends on/// where the process currently is. Identity and signing are pinned on the/// command line: the machine running the tests may have neither configured,/// or may have commit signing on, and either would otherwise decide whether/// a commit can be made at all.fn run_git(dir: &Path, args: &[&str]) -> String { let out = Command::new("git") .arg("-C") .arg(dir) .args(["-c", "user.name=atgc tests"]) .args(["-c", "user.email=tests@example.invalid"]) .args(["-c", "commit.gpgsign=false"]) .args(["-c", "tag.gpgsign=false"]) .args(args) .output() .expect("git should be on PATH"); assert!( out.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&out.stderr) ); String::from_utf8(out.stdout) .expect("utf-8") .trim() .to_string()}
/// One OAuth session record, for the tests that exercise the session store.////// Shared rather than built per module because/// [`jacquard::oauth::utils::generate_key`] mints a fresh P-256 key on every/// call: two independently built sessions differ in the DPoP key and compare/// unequal for a reason no caller has. A test wanting a second, different/// session should clone this one and change the field it cares about.pub fn oauth_session(access_token: &str) -> jacquard::oauth::session::ClientSessionData { use jacquard::common::deps::fluent_uri::Uri; use jacquard::oauth::scopes::Scopes; use jacquard::oauth::session::{ClientSessionData, DpopClientData}; use jacquard::oauth::types::{OAuthTokenType, TokenSet}; use jacquard::types::string::Did;
let did = Did::<jacquard::common::DefaultStr>::new_owned("did:plc:alice").expect("a valid DID"); ClientSessionData { account_did: did.clone(), session_id: "session".into(), host_url: Uri::parse("https://pds.example".to_string()).expect("a valid URI"), authserver_url: "https://issuer.example".into(), authserver_token_endpoint: "https://issuer.example/token".into(), authserver_revocation_endpoint: None, scopes: Scopes::empty(), dpop_data: DpopClientData { dpop_key: jacquard::oauth::utils::generate_key(&["ES256"]).expect("a P-256 key"), dpop_authserver_nonce: Default::default(), dpop_host_nonce: Default::default(), }, token_set: TokenSet { iss: "https://issuer.example".into(), sub: did, aud: "https://pds.example".into(), scope: None, refresh_token: None, access_token: access_token.into(), token_type: OAuthTokenType::DPoP, expires_at: None, }, resolved_scopes: None, }}