diff --git a/knot2/crates/knot-sim/tests/common/mod.rs b/knot2/crates/knot-sim/tests/common/mod.rs index b91efbd58..b12c29da2 100644 --- a/knot2/crates/knot-sim/tests/common/mod.rs +++ b/knot2/crates/knot-sim/tests/common/mod.rs @@ -1,8 +1,11 @@ #![allow(dead_code)] +use std::collections::BTreeSet; +use std::io::Write; use std::net::SocketAddr; use std::num::{NonZeroU32, NonZeroU64}; use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; use std::sync::Arc; use std::time::Duration; @@ -13,6 +16,10 @@ use knot_edge::{ IdleTimeout, ListenLimits, MaxInflightRequests, RequestTimeout, RequestsPerSecond, RequiresFullHandshake, StaticCertPaths, TlsSetup, WriteRequestTimeout, ZeroRttRoutes, }; +use knot_git::Layout; +use knot_runtime::{K256Signer, OsEntropy, Signer}; +use knot_secrets::{MasterKey, SealedStore}; +use knot_types::{ActorId, Did, KnotId, ObjectFormat}; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::crypto::aws_lc_rs; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; @@ -322,3 +329,149 @@ pub fn spawn_worker(test: &str, envs: &[(&str, &str)]) -> std::process::Child { } command.spawn().expect("spawn chaos worker") } + +pub const PINNED_DATE: &str = "2026-06-20T12:00:00+00:00"; + +pub fn git(cwd: &Path, args: &[&str]) -> String { + let out = knot_fixtures::command_at(cwd, PINNED_DATE) + .args(args) + .output() + .expect("git runs"); + assert!( + out.status.success(), + "git {args:?} failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + String::from_utf8(out.stdout).unwrap().trim().to_string() +} + +pub fn object_set(dir: &Path) -> BTreeSet { + git( + dir, + &[ + "cat-file", + "--batch-all-objects", + "--batch-check=%(objectname)", + ], + ) + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect() +} + +pub fn index_pack(repo: &Path, pack: &[u8]) { + let mut child = knot_fixtures::command(repo) + .args(["index-pack", "--stdin", "--fix-thin"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn index-pack"); + child.stdin.take().unwrap().write_all(pack).unwrap(); + let output = child.wait_with_output().unwrap(); + assert!( + output.status.success(), + "index-pack failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +pub fn extract_pack(response: &[u8]) -> Vec { + let mut channel = Vec::new(); + let mut pos = 0usize; + while pos + 4 <= response.len() { + let len = std::str::from_utf8(&response[pos..pos + 4]) + .ok() + .and_then(|hex| usize::from_str_radix(hex, 16).ok()) + .unwrap_or(0); + pos += 4; + if len < 4 { + continue; + } + let end = (pos + len - 4).min(response.len()); + let payload = &response[pos..end]; + pos = end; + if payload.first() == Some(&1) { + channel.extend_from_slice(&payload[1..]); + } + } + match channel.windows(4).position(|window| window == b"PACK") { + Some(start) => channel.split_off(start), + None => channel, + } +} + +pub fn fetch_body(tip: &str) -> Bytes { + let mut body = Vec::new(); + body.extend_from_slice(&pkt(b"command=fetch\n")); + body.extend_from_slice(&pkt(b"agent=knot/0\n")); + body.extend_from_slice(b"0001"); + body.extend_from_slice(&pkt(b"no-progress\n")); + body.extend_from_slice(&pkt(b"ofs-delta\n")); + body.extend_from_slice(&pkt(format!("want {tip}\n").as_bytes())); + body.extend_from_slice(&pkt(b"done\n")); + body.extend_from_slice(b"0000"); + Bytes::from(body) +} + +pub fn did_document(signer: &K256Signer, did: &Did, pds: &str) -> Vec { + let multikey = ActorId::new(knot_types::crypto::multikey( + 0xe7, + signer.public_key().as_bytes(), + )) + .unwrap(); + 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.as_str() + }], + "service": [{ + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": pds + }] + })) + .unwrap() +} + +pub fn keygen(dir: &Path) -> (String, String) { + let path = dir.join("client"); + 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()); + let public_line = std::fs::read_to_string(dir.join("client.pub")) + .unwrap() + .trim() + .to_string(); + (path.to_str().unwrap().to_string(), public_line) +} + +pub fn bootstrap_target(root: &Path, knot: &KnotId, master: &MasterKey) -> (Layout, K256Signer) { + let repos = root.join("repos"); + std::fs::create_dir_all(&repos).unwrap(); + let layout = Layout::new(&repos) + .with_object_format(ObjectFormat::SHA1) + .reserving_meta(knot) + .unwrap(); + layout.bootstrap_meta(knot).unwrap(); + let secrets = SealedStore::open(root.join("sealed-keys"), master, Box::new(OsEntropy)).unwrap(); + secrets.ensure(knot).unwrap(); + let signer = secrets.signer(knot).unwrap(); + (layout, signer) +} diff --git a/knot2/crates/knot-sim/tests/h3.rs b/knot2/crates/knot-sim/tests/h3.rs index e76133e85..34f9727d0 100644 --- a/knot2/crates/knot-sim/tests/h3.rs +++ b/knot2/crates/knot-sim/tests/h3.rs @@ -1,54 +1,16 @@ mod common; use std::collections::BTreeSet; -use std::io::Write; use std::path::Path; -use std::process::Stdio; use std::sync::Arc; -use bytes::Bytes; -use common::Edge; +use common::{Edge, extract_pack, fetch_body, git, index_pack, object_set}; use http::Method; use knot_edge::RequiresFullHandshake; use knot_git::Layout; use knot_pack::{RepoLookup, RepoResolver, RepoTarget}; use knot_types::{ObjectFormat, RepoDid}; -const PINNED_DATE: &str = "2026-06-20T12:00:00+00:00"; - -fn git(cwd: &Path, args: &[&str]) -> String { - let out = knot_fixtures::command(cwd) - .args(args) - .env("GIT_AUTHOR_DATE", PINNED_DATE) - .env("GIT_COMMITTER_DATE", PINNED_DATE) - .output() - .expect("git runs"); - assert!( - out.status.success(), - "git {args:?} failed: {}", - String::from_utf8_lossy(&out.stderr) - ); - String::from_utf8(out.stdout) - .expect("git stdout is utf-8") - .trim() - .to_string() -} - -fn object_set(dir: &Path) -> BTreeSet { - git( - dir, - &[ - "cat-file", - "--batch-all-objects", - "--batch-check=%(objectname)", - ], - ) - .lines() - .map(|line| line.trim().to_string()) - .filter(|line| !line.is_empty()) - .collect() -} - fn seed_bare(work: &Path, bare: &Path, format: ObjectFormat) -> String { let fmt = format!("--object-format={}", format.capability()); std::fs::create_dir_all(work).unwrap(); @@ -70,61 +32,6 @@ fn init(dir: &Path, format: ObjectFormat) { git(dir, &["init", &fmt, "-q", dir.to_str().unwrap()]); } -fn index_pack(repo: &Path, pack: &[u8]) { - let mut child = knot_fixtures::command(repo) - .args(["index-pack", "--stdin", "--fix-thin"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn index-pack"); - child.stdin.take().unwrap().write_all(pack).unwrap(); - let output = child.wait_with_output().unwrap(); - assert!( - output.status.success(), - "index-pack failed: {}", - String::from_utf8_lossy(&output.stderr) - ); -} - -fn fetch_body(tip: &str) -> Bytes { - let mut body = Vec::new(); - body.extend_from_slice(&common::pkt(b"command=fetch\n")); - body.extend_from_slice(&common::pkt(b"agent=knot/0\n")); - body.extend_from_slice(b"0001"); - body.extend_from_slice(&common::pkt(b"no-progress\n")); - body.extend_from_slice(&common::pkt(b"ofs-delta\n")); - body.extend_from_slice(&common::pkt(format!("want {tip}\n").as_bytes())); - body.extend_from_slice(&common::pkt(b"done\n")); - body.extend_from_slice(b"0000"); - Bytes::from(body) -} - -fn extract_pack(response: &[u8]) -> Vec { - let mut channel = Vec::new(); - let mut pos = 0usize; - while pos + 4 <= response.len() { - let len = std::str::from_utf8(&response[pos..pos + 4]) - .ok() - .and_then(|hex| usize::from_str_radix(hex, 16).ok()) - .unwrap_or(0); - pos += 4; - if len < 4 { - continue; - } - let end = (pos + len - 4).min(response.len()); - let payload = &response[pos..end]; - pos = end; - if payload.first() == Some(&1) { - channel.extend_from_slice(&payload[1..]); - } - } - match channel.windows(4).position(|window| window == b"PACK") { - Some(start) => channel.split_off(start), - None => channel, - } -} - fn serve_dids() -> Arc { Arc::new( |target: &RepoTarget, _viewer: &knot_acl::Viewer| match target { diff --git a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs index 9fdabe41e..e33a4d007 100644 --- a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs +++ b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use bytes::Bytes; +use common::keygen; use http::Method; use knot_atproto::Atproto; use knot_cob::{CobHome, CobStore}; @@ -21,8 +22,8 @@ use knot_runtime::{ }; use knot_secrets::{MasterKey, SealedStore}; use knot_types::{ - AccountDid, ActorId, AdmissionPolicy, AuthorName, Did, Email, KnotHostname, KnotId, - KnotServiceUrl, Nsid, OwnerDid, RepoDid, RepoName, RepoRkey, ServiceDid, UnixSeconds, + AccountDid, AdmissionPolicy, AuthorName, Did, Email, KnotHostname, KnotId, KnotServiceUrl, + Nsid, OwnerDid, RepoDid, RepoName, RepoRkey, ServiceDid, UnixSeconds, }; use tempfile::TempDir; use tokio::net::TcpListener; @@ -121,57 +122,10 @@ fn git(cwd: &Path, env: &[(String, String)], args: &[&str]) -> (bool, String) { ) } -fn keygen(dir: &Path) -> (String, String) { - let path = dir.join("client"); - 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()); - let public_line = std::fs::read_to_string(dir.join("client.pub")) - .unwrap() - .trim() - .to_string(); - (path.to_str().unwrap().to_string(), public_line) -} - fn actor_signer() -> K256Signer { K256Signer::from_slice(&[9u8; 32]).unwrap() } -fn did_document(did: &Did, pds: &KnotServiceUrl) -> Vec { - let multikey = ActorId::new(knot_types::crypto::multikey( - 0xe7, - actor_signer().public_key().as_bytes(), - )) - .unwrap(); - 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.as_str() - }], - "service": [{ - "id": "#atproto_pds", - "type": "AtprotoPersonalDataServer", - "serviceEndpoint": pds.as_str() - }] - })) - .unwrap() -} - fn list_records_body(public_line: &str) -> Vec { serde_json::to_vec(&serde_json::json!({ "records": [{ @@ -209,7 +163,7 @@ fn fake_http( true => &knot_pds, false => &foreign_pds, }; - did_document(&did, pds) + common::did_document(&actor_signer(), &did, pds.as_str()) } else { return Ok(HttpResponse { status: http::StatusCode::NOT_FOUND, diff --git a/knot2/crates/knot-sim/tests/social_chaos.rs b/knot2/crates/knot-sim/tests/social_chaos.rs index 9605e8cf7..241e37b98 100644 --- a/knot2/crates/knot-sim/tests/social_chaos.rs +++ b/knot2/crates/knot-sim/tests/social_chaos.rs @@ -11,12 +11,9 @@ use knot_cobs::{Registration, RegistryChange}; use knot_git::{Layout, Repo}; use knot_index::{Coverage, Index, Resolved, SocialIndex}; use knot_record::chain; -use knot_runtime::OsEntropy; -use knot_secrets::{MasterKey, SealedStore}; +use knot_secrets::MasterKey; use knot_sim::write_pull; -use knot_types::{ - KnotId, ObjectFormat, OwnerDid, RepoDid, RepoName, RepoRkey, TypeName, UnixSeconds, -}; +use knot_types::{KnotId, OwnerDid, RepoDid, RepoName, RepoRkey, TypeName, UnixSeconds}; const KNOT_HOST: &str = "knot.nel.pet"; const ADMIN_DID: &str = "did:web:admin.nel.pet"; @@ -40,20 +37,9 @@ fn master() -> MasterKey { } fn build_target(root: &Path) { - let repos = root.join("repos"); - std::fs::create_dir_all(&repos).unwrap(); - let layout = Layout::new(&repos) - .with_object_format(ObjectFormat::SHA1) - .reserving_meta(&knot()) - .unwrap(); - layout.bootstrap_meta(&knot()).unwrap(); + let (layout, signer) = common::bootstrap_target(root, &knot(), &master()); let meta_path = layout.meta_path(&knot()).unwrap(); - let secrets = - SealedStore::open(root.join("sealed-keys"), &master(), Box::new(OsEntropy)).unwrap(); - secrets.ensure(&knot()).unwrap(); - let signer = secrets.signer(&knot()).unwrap(); - let repo = layout.create(&did()).unwrap(); write_pull(&repo, &did()); let mut ticker = jacquard_common::types::tid::Ticker::new(); diff --git a/knot2/crates/knot-sim/tests/ssh.rs b/knot2/crates/knot-sim/tests/ssh.rs index 62fc08f05..4415418bb 100644 --- a/knot2/crates/knot-sim/tests/ssh.rs +++ b/knot2/crates/knot-sim/tests/ssh.rs @@ -1,7 +1,9 @@ +mod common; + use std::path::Path; -use std::process::Command; use std::sync::Arc; +use common::{PINNED_DATE, did_document, keygen}; use knot_atproto::Atproto; use knot_cob::{CobHome, CobStore}; use knot_cobs::{Registration, RegistryChange}; @@ -19,7 +21,6 @@ const REPO_NAME: &str = "anemone"; const OWNER_DID: &str = "did:plc:nel"; const PDS_HOST: &str = "pds.oyster.cafe"; const KNOT_DID: &str = "did:web:nel.pet"; -const PINNED_DATE: &str = "2026-06-20T12:00:00+00:00"; fn repo_did() -> RepoDid { RepoDid::new(REPO_DID).unwrap() @@ -50,49 +51,6 @@ fn git(cwd: &Path, env: &[(&str, &str)], args: &[&str]) -> (bool, String) { ) } -fn keygen(dir: &Path) -> (String, String) { - let path = dir.join("client"); - 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()); - let public_line = std::fs::read_to_string(dir.join("client.pub")) - .unwrap() - .trim() - .to_string(); - (path.to_str().unwrap().to_string(), public_line) -} - -fn did_document(signer: &K256Signer, did: &Did) -> 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": format!("https://{PDS_HOST}") - }] - })) - .unwrap() -} - fn list_records_body(public_line: &str) -> Vec { serde_json::to_vec(&serde_json::json!({ "records": [{ @@ -112,15 +70,16 @@ fn fake_http(published_line: String) -> impl knot_runtime::HttpTransport { let signer = K256Signer::generate(&SeededEntropy::new(1)); let owner = Did::new_owned(OWNER_DID).unwrap(); let repo = Did::new_owned(REPO_DID).unwrap(); + let pds_url = format!("https://{PDS_HOST}"); FakeHttp::new(move |request| { let host = request.url.host_str().unwrap_or_default().to_string(); let path = request.url.path().to_string(); let body = if host == PDS_HOST { list_records_body(&published_line) } else if path.ends_with(owner.as_str()) { - did_document(&signer, &owner) + did_document(&signer, &owner, &pds_url) } else if path.ends_with(repo.as_str()) { - did_document(&signer, &repo) + did_document(&signer, &repo, &pds_url) } else { return Ok(HttpResponse { status: http::StatusCode::NOT_FOUND,