From f664d456dcbf8dd5da881ae871fbc799e627c306 Mon Sep 17 00:00:00 2001 From: dawn Date: Mon, 19 Oct 2026 13:49:20 +0300 Subject: [PATCH] knot-ssh: share test helpers between test files Signed-off-by: dawn --- knot2/crates/knot-ssh/tests/common/mod.rs | 332 +++++++++++++++++++ knot2/crates/knot-ssh/tests/ssh_push.rs | 369 ++-------------------- 2 files changed, 357 insertions(+), 344 deletions(-) create mode 100644 knot2/crates/knot-ssh/tests/common/mod.rs diff --git a/knot2/crates/knot-ssh/tests/common/mod.rs b/knot2/crates/knot-ssh/tests/common/mod.rs new file mode 100644 index 000000000..2b9beb3cd --- /dev/null +++ b/knot2/crates/knot-ssh/tests/common/mod.rs @@ -0,0 +1,332 @@ +#![allow(dead_code)] + +use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use knot_atproto::Atproto; +use knot_fixtures::UNSHARED_SSH; +use knot_git::{ArchiveLimit, Layout}; +use knot_index::Index; +use knot_pack::MaxWireBytes; +use knot_runtime::{ + FakeDns, FakeHttp, HttpResponse, K256Signer, ManualClock, SeededEntropy, Signer, UnixMicros, +}; +use knot_types::{AccountDid, Did, KnotId, KnotServiceUrl, UnixSeconds}; +use tokio::net::TcpListener; +use url::Url; + +pub const OWNER_DID: &str = "did:plc:nel"; +pub const OWNER_HANDLE: &str = "nel.pet"; +pub const PRIVATE_DID: &str = "did:plc:clam"; + +pub fn owner() -> AccountDid { + AccountDid::new(OWNER_DID).unwrap() +} + +pub fn actor_for_seed(seed: u64) -> knot_types::ActorId { + knot_types::ActorId::from_secp256k1( + K256Signer::generate(&SeededEntropy::new(seed)) + .public_key() + .as_bytes(), + ) +} + +pub fn git(cwd: &Path, env: &[(&str, &str)], args: &[&str]) -> (bool, String) { + let mut command = knot_fixtures::command(cwd); + command.args(args); + env.iter().for_each(|(key, value)| { + command.env(key, value); + }); + let out = command.output().expect("git runs"); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + (out.status.success(), combined) +} + +pub fn keygen(dir: &Path, name: &str) -> (String, String) { + let path = dir.join(name); + let out = Command::new("ssh-keygen") + .args([ + "-t", + "ed25519", + "-N", + "", + "-C", + "nel@oyster.cafe", + "-f", + path.to_str().unwrap(), + ]) + .output() + .expect("ssh-keygen runs"); + assert!( + out.status.success(), + "ssh-keygen failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let public_line = std::fs::read_to_string(dir.join(format!("{name}.pub"))) + .unwrap() + .trim() + .to_string(); + (path.to_str().unwrap().to_string(), public_line) +} + +pub fn did_document(signer: &K256Signer, did: &Did, pds: &KnotServiceUrl) -> Vec { + let multikey = knot_types::crypto::multikey(0xe7, signer.public_key().as_bytes()); + serde_json::to_vec(&serde_json::json!({ + "id": did.as_str(), + "alsoKnownAs": [format!("at://{OWNER_HANDLE}")], + "verificationMethod": [{ + "id": format!("{}#atproto", did.as_str()), + "type": "Multikey", + "controller": did.as_str(), + "publicKeyMultibase": multikey + }], + "service": [{ + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": pds.as_str() + }] + })) + .unwrap() +} + +pub fn list_records_body(lines: &[impl AsRef]) -> Vec { + let records: Vec<_> = lines + .iter() + .map(|line| { + serde_json::json!({ + "value": { + "$type": "org.tangled.key.sshKey", + "publicKey": line.as_ref(), + "name": "laptop", + "createdAt": "2026-06-08T00:00:00Z" + } + }) + }) + .collect(); + serde_json::to_vec(&serde_json::json!({ "records": records })).unwrap() +} + +pub fn ok_body(body: Vec) -> HttpResponse { + HttpResponse { + status: http::StatusCode::OK, + headers: http::HeaderMap::new(), + body: bytes::Bytes::from(body), + } +} + +pub fn not_found() -> HttpResponse { + HttpResponse { + status: http::StatusCode::NOT_FOUND, + headers: http::HeaderMap::new(), + body: bytes::Bytes::new(), + } +} + +pub fn server_error() -> HttpResponse { + HttpResponse { + status: http::StatusCode::INTERNAL_SERVER_ERROR, + headers: http::HeaderMap::new(), + body: bytes::Bytes::new(), + } +} + +pub fn fake_dns() -> impl knot_runtime::DnsTxtResolver { + FakeDns::new(|name: &str| { + Ok(match name { + name if name == format!("_atproto.{OWNER_HANDLE}") => { + vec![format!("did={}", owner())] + } + _ => Vec::new(), + }) + }) +} + +pub fn forever() -> knot_index::KeyLease { + knot_index::KeyTtl::from_secs(u32::MAX.into()).lease_from(UnixSeconds::new(0)) +} + +#[derive(Default, Clone)] +pub struct Accounts { + identities: Arc>>>, + unreachable: Arc>>, + listings: Arc, +} + +impl Accounts { + pub fn publishing(identities: HashMap>) -> Self { + Self { + identities: Arc::new(Mutex::new(identities)), + ..Self::default() + } + } + + pub fn unreachable(self, dids: HashSet) -> Self { + *self.unreachable.lock().unwrap() = dids; + self + } + + pub fn restore(&self, did: &AccountDid) { + self.unreachable.lock().unwrap().remove(did); + } + + pub fn publish(&self, did: &AccountDid, line: String) { + self.identities + .lock() + .unwrap() + .entry(did.clone()) + .or_default() + .push(line); + } + + pub fn published_by(&self, did: &AccountDid) -> Vec { + self.identities + .lock() + .unwrap() + .get(did) + .cloned() + .unwrap_or_default() + } + + pub fn listings(&self) -> usize { + self.listings.load(Ordering::SeqCst) + } +} + +pub fn multi_http(accounts: Accounts) -> impl knot_runtime::HttpTransport { + let signer = K256Signer::generate(&SeededEntropy::new(77)); + let pds_test = KnotServiceUrl::new("https://pds.test").unwrap(); + FakeHttp::new(move |request| { + let host = request.url.host_str().unwrap_or_default().to_string(); + if host == "plc.directory" { + let did = Did::new_owned(request.url.path().trim_start_matches('/')) + .map(|did| did_document(&signer, &did, &pds_test)) + .unwrap_or_default(); + return Ok(ok_body(did)); + } + if host == "pds.test" { + let repo = request + .url + .query_pairs() + .find(|(key, _)| key == "repo") + .and_then(|(_, value)| AccountDid::new(value).ok()); + accounts.listings.fetch_add(1, Ordering::SeqCst); + if repo + .as_ref() + .is_some_and(|did| accounts.unreachable.lock().unwrap().contains(did)) + { + return Ok(server_error()); + } + let lines = repo + .as_ref() + .map(|did| accounts.published_by(did)) + .unwrap_or_default(); + let refs: Vec<&str> = lines.iter().map(String::as_str).collect(); + return Ok(ok_body(list_records_body(&refs))); + } + Ok(not_found()) + }) +} + +pub async fn launch( + host_key_dir: &Path, + layout: Layout, + index: Arc, + accounts: Accounts, +) -> u16 { + let atproto = Arc::new( + Atproto::new( + multi_http(accounts), + ManualClock::new(UnixMicros::new(1_000_000_000)), + KnotId::new("did:web:nel.pet").unwrap(), + knot_atproto::PlcDirectory::new(Url::parse("https://plc.directory/").unwrap()).unwrap(), + ) + .with_dns(Arc::new(fake_dns())), + ); + std::fs::create_dir_all(host_key_dir).unwrap(); + let host_key = knot_ssh::load_or_create_host_key(&host_key_dir.join("host")).unwrap(); + let state = Arc::new(knot_ssh::SshState::new(knot_ssh::SshConfig { + repos: knot_acl::Repos::new(layout), + index, + atproto, + knot_actor: actor_for_seed(77), + hostname: knot_types::KnotHostname::new("knot.test").unwrap(), + appview: knot_types::AppviewEndpoint::new("https://tangled.test").unwrap(), + admins: std::collections::BTreeSet::new(), + admission: knot_types::AdmissionPolicy::Closed, + max_pack_bytes: MaxWireBytes::new(1 << 30), + archive_limit: ArchiveLimit::default(), + ci_logs: None, + })); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + tokio::spawn(async move { + let _ = knot_ssh::serve_on_socket(listener, host_key, state).await; + }); + port +} + +pub fn ssh_command(key_path: &str) -> String { + format!("ssh -i {key_path} {UNSHARED_SSH}") +} + +pub async fn git_ssh(cwd: &Path, key: &str, args: &[&str]) -> (bool, String) { + let ssh = ssh_command(key); + let cwd = cwd.to_path_buf(); + let owned: Vec = args.iter().map(|arg| arg.to_string()).collect(); + tokio::task::spawn_blocking(move || { + let argv: Vec<&str> = owned.iter().map(String::as_str).collect(); + git(&cwd, &[("GIT_SSH_COMMAND", &ssh)], &argv) + }) + .await + .unwrap() +} + +pub async fn push(work: &Path, url: &str, key: &str, refspecs: &[&str]) -> (bool, String) { + let args: Vec<&str> = std::iter::once("push") + .chain(std::iter::once(url)) + .chain(refspecs.iter().copied()) + .collect(); + git_ssh(work, key, &args).await +} + +pub fn seed_work(work: &Path) -> String { + std::fs::create_dir_all(work).unwrap(); + git(work, &[], &["init", "-q", "-b", "main"]); + std::fs::write(work.join("README.md"), "hello over ssh\n").unwrap(); + git(work, &[], &["add", "-A"]); + git(work, &[], &["commit", "-q", "-m", "initial"]); + let (ok, head) = git(work, &[], &["rev-parse", "HEAD"]); + assert!(ok); + head.trim().to_string() +} + +pub async fn ssh_exec(key: &str, port: u16, command: String) -> (Option, String, String) { + let key = key.to_string(); + tokio::task::spawn_blocking(move || { + let out = Command::new("ssh") + .args(["-i", &key]) + .args(UNSHARED_SSH.split_whitespace()) + .args(["-p", &port.to_string(), "git@127.0.0.1", &command]) + .stdin(std::process::Stdio::null()) + .output() + .expect("ssh runs"); + ( + out.status.code(), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) + }) + .await + .unwrap() +} + +pub fn url(port: u16, path: &str) -> String { + format!("ssh://git@127.0.0.1:{port}/{path}") +} diff --git a/knot2/crates/knot-ssh/tests/ssh_push.rs b/knot2/crates/knot-ssh/tests/ssh_push.rs index 8bbc4fddf..42f107ba8 100644 --- a/knot2/crates/knot-ssh/tests/ssh_push.rs +++ b/knot2/crates/knot-ssh/tests/ssh_push.rs @@ -1,8 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::process::Command; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use futures::stream::StreamExt; use knot_atproto::Atproto; @@ -12,9 +11,7 @@ use knot_fixtures::UNSHARED_SSH; use knot_git::{ArchiveLimit, Layout, Repo}; use knot_index::{Index, Resolved}; use knot_pack::MaxWireBytes; -use knot_runtime::{ - FakeDns, FakeHttp, HttpResponse, K256Signer, ManualClock, SeededEntropy, Signer, UnixMicros, -}; +use knot_runtime::{FakeHttp, K256Signer, ManualClock, SeededEntropy, UnixMicros}; use knot_types::{ AccountDid, Did, KnotId, KnotServiceUrl, Oid, OwnerDid, RefName, RepoDid, RepoName, RepoRkey, UnixSeconds, @@ -23,9 +20,16 @@ use tempfile::TempDir; use tokio::net::TcpListener; use url::Url; +mod common; + +use common::{ + Accounts, OWNER_DID, PRIVATE_DID, actor_for_seed, did_document, fake_dns, forever, git, + git_ssh, keygen, launch, list_records_body, not_found, ok_body, owner, push, seed_work, + ssh_command, ssh_exec, url, +}; + const REPO_DID: &str = "did:plc:squid"; const REPO_NAME: &str = "anemone"; -const OWNER_DID: &str = "did:plc:nel"; const TID_REPO_DID: &str = "did:plc:limpet"; const TID_RKEY: &str = "3mizfnpxii522"; const TID_REPO_NAME: &str = "periwinkle.cloud"; @@ -39,8 +43,8 @@ fn repo_name() -> RepoName { RepoName::new(REPO_NAME).unwrap() } -fn owner() -> AccountDid { - AccountDid::new(OWNER_DID).unwrap() +fn named_url(port: u16) -> String { + url(port, &format!("{}/{}", owner(), repo_name())) } fn owner_did() -> OwnerDid { @@ -63,122 +67,6 @@ fn pds_url() -> KnotServiceUrl { KnotServiceUrl::new(format!("https://{PDS_HOST}")).unwrap() } -fn git(cwd: &Path, env: &[(&str, &str)], args: &[&str]) -> (bool, String) { - let mut command = knot_fixtures::command(cwd); - command.args(args); - env.iter().for_each(|(key, value)| { - command.env(key, value); - }); - let out = command.output().expect("git runs"); - let combined = format!( - "{}{}", - String::from_utf8_lossy(&out.stdout), - String::from_utf8_lossy(&out.stderr) - ); - (out.status.success(), combined) -} - -fn keygen(dir: &Path, name: &str) -> (String, String) { - let path = dir.join(name); - let out = Command::new("ssh-keygen") - .args([ - "-t", - "ed25519", - "-N", - "", - "-C", - "nel@oyster.cafe", - "-f", - path.to_str().unwrap(), - ]) - .output() - .expect("ssh-keygen runs"); - assert!( - out.status.success(), - "ssh-keygen failed: {}", - String::from_utf8_lossy(&out.stderr) - ); - let public_line = std::fs::read_to_string(dir.join(format!("{name}.pub"))) - .unwrap() - .trim() - .to_string(); - (path.to_str().unwrap().to_string(), public_line) -} - -fn did_document(signer: &K256Signer, did: &Did, pds: &KnotServiceUrl) -> Vec { - let multikey = knot_types::crypto::multikey(0xe7, signer.public_key().as_bytes()); - serde_json::to_vec(&serde_json::json!({ - "id": did.as_str(), - "alsoKnownAs": ["at://nel.pet"], - "verificationMethod": [{ - "id": format!("{}#atproto", did.as_str()), - "type": "Multikey", - "controller": did.as_str(), - "publicKeyMultibase": multikey - }], - "service": [{ - "id": "#atproto_pds", - "type": "AtprotoPersonalDataServer", - "serviceEndpoint": pds.as_str() - }] - })) - .unwrap() -} - -fn list_records_body(lines: &[&str]) -> Vec { - let records: Vec<_> = lines - .iter() - .map(|line| { - serde_json::json!({ - "value": { - "$type": "org.tangled.key.sshKey", - "publicKey": line, - "name": "laptop", - "createdAt": "2026-06-08T00:00:00Z" - } - }) - }) - .collect(); - serde_json::to_vec(&serde_json::json!({ "records": records })).unwrap() -} - -fn ok_body(body: Vec) -> HttpResponse { - HttpResponse { - status: http::StatusCode::OK, - headers: http::HeaderMap::new(), - body: bytes::Bytes::from(body), - } -} - -fn fake_dns() -> impl knot_runtime::DnsTxtResolver { - FakeDns::new(|name: &str| { - Ok(match name { - "_atproto.nel.pet" => vec![format!("did={}", owner())], - _ => Vec::new(), - }) - }) -} - -fn not_found() -> HttpResponse { - HttpResponse { - status: http::StatusCode::NOT_FOUND, - headers: http::HeaderMap::new(), - body: bytes::Bytes::new(), - } -} - -fn forever() -> knot_index::KeyLease { - knot_index::KeyTtl::from_secs(u32::MAX.into()).lease_from(UnixSeconds::new(0)) -} - -fn server_error() -> HttpResponse { - HttpResponse { - status: http::StatusCode::INTERNAL_SERVER_ERROR, - headers: http::HeaderMap::new(), - body: bytes::Bytes::new(), - } -} - fn fake_http(published_line: String) -> impl knot_runtime::HttpTransport { let signer = K256Signer::generate(&SeededEntropy::new(1)); let pds = pds_url(); @@ -203,96 +91,6 @@ fn fake_http(published_line: String) -> impl knot_runtime::HttpTransport { }) } -#[derive(Default, Clone)] -struct Accounts { - identities: Arc>>>, - unreachable: Arc>>, - listings: Arc, -} - -impl Accounts { - fn publishing(identities: HashMap>) -> Self { - Self { - identities: Arc::new(Mutex::new(identities)), - ..Self::default() - } - } - - fn unreachable(self, dids: HashSet) -> Self { - *self.unreachable.lock().unwrap() = dids; - self - } - - fn restore(&self, did: &AccountDid) { - self.unreachable.lock().unwrap().remove(did); - } - - fn publish(&self, did: &AccountDid, line: String) { - self.identities - .lock() - .unwrap() - .entry(did.clone()) - .or_default() - .push(line); - } - - fn published_by(&self, did: &AccountDid) -> Vec { - self.identities - .lock() - .unwrap() - .get(did) - .cloned() - .unwrap_or_default() - } - - fn listings(&self) -> usize { - self.listings.load(Ordering::SeqCst) - } -} - -fn multi_http(accounts: Accounts) -> impl knot_runtime::HttpTransport { - let signer = K256Signer::generate(&SeededEntropy::new(77)); - let pds_test = KnotServiceUrl::new("https://pds.test").unwrap(); - FakeHttp::new(move |request| { - let host = request.url.host_str().unwrap_or_default().to_string(); - if host == "plc.directory" { - let did = Did::new_owned(request.url.path().trim_start_matches('/')) - .map(|did| did_document(&signer, &did, &pds_test)) - .unwrap_or_default(); - return Ok(ok_body(did)); - } - if host == "pds.test" { - let repo = request - .url - .query_pairs() - .find(|(key, _)| key == "repo") - .and_then(|(_, value)| AccountDid::new(value).ok()); - accounts.listings.fetch_add(1, Ordering::SeqCst); - if repo - .as_ref() - .is_some_and(|did| accounts.unreachable.lock().unwrap().contains(did)) - { - return Ok(server_error()); - } - let lines = repo - .as_ref() - .map(|did| accounts.published_by(did)) - .unwrap_or_default(); - let refs: Vec<&str> = lines.iter().map(String::as_str).collect(); - return Ok(ok_body(list_records_body(&refs))); - } - Ok(not_found()) - }) -} - -fn actor_for_seed(seed: u64) -> knot_types::ActorId { - knot_types::ActorId::from_secp256k1( - K256Signer::generate(&SeededEntropy::new(seed)) - .public_key() - .as_bytes(), - ) -} - struct Server { _scan: TempDir, layout: Layout, @@ -540,30 +338,6 @@ async fn spawn_server_core( ) } -fn ssh_command(key_path: &str) -> String { - format!("ssh -i {key_path} {UNSHARED_SSH}") -} - -async fn git_ssh(cwd: &Path, key: &str, args: &[&str]) -> (bool, String) { - let ssh = ssh_command(key); - let cwd = cwd.to_path_buf(); - let owned: Vec = args.iter().map(|arg| arg.to_string()).collect(); - tokio::task::spawn_blocking(move || { - let argv: Vec<&str> = owned.iter().map(String::as_str).collect(); - git(&cwd, &[("GIT_SSH_COMMAND", &ssh)], &argv) - }) - .await - .unwrap() -} - -async fn push(work: &Path, url: &str, key: &str, refspecs: &[&str]) -> (bool, String) { - let args: Vec<&str> = std::iter::once("push") - .chain(std::iter::once(url)) - .chain(refspecs.iter().copied()) - .collect(); - git_ssh(work, key, &args).await -} - async fn clone(url: &str, key: &str, dest: &Path) -> (bool, String) { git_ssh( Path::new("/tmp"), @@ -573,17 +347,6 @@ async fn clone(url: &str, key: &str, dest: &Path) -> (bool, String) { .await } -fn seed_work(work: &Path) -> String { - std::fs::create_dir_all(work).unwrap(); - git(work, &[], &["init", "-q", "-b", "main"]); - std::fs::write(work.join("README.md"), "hello over ssh\n").unwrap(); - git(work, &[], &["add", "-A"]); - git(work, &[], &["commit", "-q", "-m", "initial"]); - let (ok, head) = git(work, &[], &["rev-parse", "HEAD"]); - assert!(ok); - head.trim().to_string() -} - fn seed_commits(work: &Path, count: usize) { std::fs::create_dir_all(work).unwrap(); git(work, &[], &["init", "-q", "-b", "main"]); @@ -667,12 +430,7 @@ async fn fixture_with_archive_limit(archive_limit: ArchiveLimit) -> Fixture { false, ) .await; - let url = format!( - "ssh://git@127.0.0.1:{}/{}/{}", - server.port, - owner(), - repo_name() - ); + let url = named_url(server.port); let work = scratch.path().join("work"); Fixture { scratch, @@ -704,12 +462,7 @@ async fn incremental_fetch_exit( let scratch = tempfile::tempdir().unwrap(); let (key_path, public_line) = keygen(scratch.path(), "client"); let (server, _index) = spawn_server(public_line, MaxWireBytes::new(1 << 30)).await; - let url = format!( - "ssh://git@127.0.0.1:{}/{}/{}", - server.port, - owner(), - repo_name() - ); + let url = named_url(server.port); let work = scratch.path().join("work"); seed_commits(&work, seed_count); @@ -840,12 +593,7 @@ async fn a_push_while_the_index_is_warming_is_refused() { let scratch = tempfile::tempdir().unwrap(); let (key_path, public_line) = keygen(scratch.path(), "client"); let (server, _index) = spawn_server_with(public_line, MaxWireBytes::new(1 << 30), false).await; - let url = format!( - "ssh://git@127.0.0.1:{}/{}/{}", - server.port, - owner(), - repo_name() - ); + let url = named_url(server.port); let work = scratch.path().join("work"); seed_work(&work); @@ -878,7 +626,7 @@ async fn unresolvable_targets_refused() { .layout .create(&RepoDid::new("did:plc:clam").unwrap()) .unwrap(); - let ghost_url = format!("ssh://git@127.0.0.1:{port}/did:plc:clam"); + let ghost_url = url(port, PRIVATE_DID); let dest = fx.scratch.path().join("ghost"); let (ok, out) = clone(&ghost_url, &fx.key_path, &dest).await; assert!( @@ -956,12 +704,7 @@ async fn an_oversized_push_is_refused_at_the_ssh_boundary() { let scratch = tempfile::tempdir().unwrap(); let (key_path, public_line) = keygen(scratch.path(), "client"); let (server, _index) = spawn_server(public_line, MaxWireBytes::new(64)).await; - let url = format!( - "ssh://git@127.0.0.1:{}/{}/{}", - server.port, - owner(), - repo_name() - ); + let url = named_url(server.port); let work = scratch.path().join("work"); seed_work(&work); @@ -1001,12 +744,7 @@ async fn a_denied_push_over_ssh_leaves_no_objects_in_the_live_odb() { let (_registered_path, registered_line) = keygen(scratch.path(), "registered"); let (attacker_path, _attacker_line) = keygen(scratch.path(), "attacker"); let (server, _index) = spawn_server(registered_line, MaxWireBytes::new(1 << 30)).await; - let url = format!( - "ssh://git@127.0.0.1:{}/{}/{}", - server.port, - owner(), - repo_name() - ); + let url = named_url(server.port); let work = scratch.path().join("work"); let head = seed_work(&work); @@ -1352,36 +1090,6 @@ fn a_group_or_other_readable_host_key_is_refused_on_load() { ); } -async fn launch(host_key_dir: &Path, layout: Layout, index: Arc, accounts: Accounts) -> u16 { - let atproto = Arc::new(Atproto::new( - multi_http(accounts), - ManualClock::new(UnixMicros::new(1_000_000_000)), - KnotId::new("did:web:nel.pet").unwrap(), - knot_atproto::PlcDirectory::new(Url::parse("https://plc.directory/").unwrap()).unwrap(), - )); - std::fs::create_dir_all(host_key_dir).unwrap(); - let host_key = knot_ssh::load_or_create_host_key(&host_key_dir.join("host")).unwrap(); - let state = Arc::new(knot_ssh::SshState::new(knot_ssh::SshConfig { - repos: knot_acl::Repos::new(layout), - index, - atproto, - knot_actor: actor_for_seed(77), - hostname: knot_types::KnotHostname::new("knot.test").unwrap(), - appview: knot_types::AppviewEndpoint::new("https://tangled.test").unwrap(), - admins: std::collections::BTreeSet::new(), - admission: knot_types::AdmissionPolicy::Closed, - max_pack_bytes: MaxWireBytes::new(1 << 30), - archive_limit: ArchiveLimit::default(), - ci_logs: None, - })); - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = listener.local_addr().unwrap().port(); - tokio::spawn(async move { - let _ = knot_ssh::serve_on_socket(listener, host_key, state).await; - }); - port -} - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn a_handle_in_the_url_identifies_a_visitor_and_lets_a_multi_key_client_find_its_key() { let fx = fixture().await; @@ -1498,7 +1206,7 @@ async fn an_unreachable_pds_reads_as_transient_and_its_keys_get_in_once_it_recov accounts.clone(), ) .await; - let url = format!("ssh://git@127.0.0.1:{port}/{}", repo_did); + let url = url(port, repo_did.as_ref()); let work = scratch.path().join("work"); let head = seed_work(&work); @@ -1570,7 +1278,7 @@ async fn an_account_the_budget_couldnt_fit_still_clears_the_handshake_and_pushes Accounts::publishing(HashMap::from([(owner(), vec![owner_line])])), ) .await; - let url = format!("ssh://git@127.0.0.1:{port}/{}", repo_did); + let url = url(port, repo_did.as_ref()); let work = scratch.path().join("work"); let head = seed_work(&work); @@ -1683,7 +1391,7 @@ async fn a_collaborator_pushes_its_repo_but_is_denied_on_a_repo_it_doesnt_collab let work_a = scratch.path().join("work_a"); let head_a = seed_work(&work_a); - let url_a = format!("ssh://git@127.0.0.1:{port}/{}", repo_a); + let url_a = url(port, repo_a.as_ref()); let (ok, out) = push(&work_a, &url_a, &collab_key, &["main"]).await; assert!( ok, @@ -1697,7 +1405,7 @@ async fn a_collaborator_pushes_its_repo_but_is_denied_on_a_repo_it_doesnt_collab let work_b = scratch.path().join("work_b"); seed_work(&work_b); - let url_b = format!("ssh://git@127.0.0.1:{port}/{}", repo_b); + let url_b = url(port, repo_b.as_ref()); let (denied, out) = push(&work_b, &url_b, &collab_key, &["main"]).await; assert!( !denied, @@ -1800,7 +1508,7 @@ async fn a_new_collaborator(pds_down: bool) -> Invitation { }; let port = launch(dir, layout.clone(), Arc::clone(&index), accounts.clone()).await; Invitation { - url: format!("ssh://git@127.0.0.1:{port}/{REPO_DID}"), + url: url(port, REPO_DID), scratch, layout, head, @@ -2246,12 +1954,7 @@ async fn an_authorized_ssh_push_projects_the_ref_records_the_http_push_projects( .await; let firehose = firehose.expect("the projection fixture serves a firehose"); - let url = format!( - "ssh://git@127.0.0.1:{}/{}/{}", - server.port, - owner(), - repo_name() - ); + let url = named_url(server.port); let work = scratch.path().join("work"); let head = seed_work(&work); @@ -2283,8 +1986,6 @@ async fn an_authorized_ssh_push_projects_the_ref_records_the_http_push_projects( ); } -const PRIVATE_DID: &str = "did:plc:clam"; - struct Reefs { scratch: TempDir, layout: Layout, @@ -2421,7 +2122,7 @@ async fn reefs() -> Reefs { impl Reefs { fn url(&self, repo: &str) -> String { - format!("ssh://git@127.0.0.1:{}/{repo}", self.port) + url(self.port, repo) } async fn seed(&self, repo: &str) -> String { @@ -2436,26 +2137,6 @@ impl Reefs { } } -async fn ssh_exec(key: &str, port: u16, command: String) -> (Option, String, String) { - let key = key.to_string(); - tokio::task::spawn_blocking(move || { - let out = Command::new("ssh") - .args(["-i", &key]) - .args(UNSHARED_SSH.split_whitespace()) - .args(["-p", &port.to_string(), "git@127.0.0.1", &command]) - .stdin(std::process::Stdio::null()) - .output() - .expect("ssh runs"); - ( - out.status.code(), - String::from_utf8_lossy(&out.stdout).into_owned(), - String::from_utf8_lossy(&out.stderr).into_owned(), - ) - }) - .await - .unwrap() -} - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn members_clone_a_private_repo_over_ssh_and_nobody_else_can_tell_it_exists() { let reefs = reefs().await; -- 2.54.0