This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-atproto / src / test_support.rs
6.3 kB 199 lines
1use std::borrow::Cow; 2use std::sync::{Arc, Mutex}; 3 4use base64::Engine; 5use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; 6use bytes::Bytes; 7use http::StatusCode; 8use k256::ecdsa::{Signature, SigningKey, signature::Signer}; 9use knot_runtime::{HttpResponse, K256Signer, ManualClock, SeededEntropy, UnixMicros}; 10use knot_types::crypto::{KeyCodec, PublicKey as CryptoKey}; 11use knot_types::{AccountDid, KnotId, Nsid, OwnerDid, RepoDid, RepoRkey}; 12use serde_json::Value; 13use url::Url; 14 15use crate::{MintNonce, ServiceJwt}; 16 17pub(crate) const SQUID: &str = "did:plc:squid"; 18pub(crate) const LIMPET: &str = "did:plc:limpet"; 19pub(crate) const KNOT: &str = "did:web:nel.pet"; 20pub(crate) const METHOD: &str = "sh.tangled.knot.addMember"; 21 22pub(crate) fn did(value: &str) -> AccountDid { 23 AccountDid::new(value).unwrap() 24} 25 26pub(crate) fn repo_did(value: &str) -> RepoDid { 27 RepoDid::new(value).unwrap() 28} 29 30pub(crate) fn owner_did(value: &str) -> OwnerDid { 31 OwnerDid::new(value).unwrap() 32} 33 34pub(crate) fn knot_did(value: &str) -> KnotId { 35 KnotId::new(value).unwrap() 36} 37 38pub(crate) fn handle(value: &str) -> knot_types::Handle { 39 knot_types::Handle::new_owned(value).unwrap() 40} 41 42pub(crate) fn member_method() -> Nsid { 43 Nsid::new_owned(METHOD).unwrap() 44} 45 46pub(crate) fn plc() -> crate::PlcDirectory { 47 crate::PlcDirectory::new(Url::parse("https://plc.directory/").unwrap()).unwrap() 48} 49 50pub(crate) fn clock() -> ManualClock { 51 ManualClock::new(UnixMicros::new(1_000_000_000)) 52} 53 54pub(crate) fn signer(seed: u8) -> SigningKey { 55 SigningKey::from_bytes(&[seed; 32].into()).unwrap() 56} 57 58pub(crate) fn sec1(signing: &SigningKey) -> Vec<u8> { 59 signing 60 .verifying_key() 61 .to_encoded_point(true) 62 .as_bytes() 63 .to_vec() 64} 65 66pub(crate) fn k256_public(signing: &SigningKey) -> CryptoKey<'static> { 67 CryptoKey { 68 codec: KeyCodec::Secp256k1, 69 bytes: Cow::Owned(sec1(signing)), 70 } 71} 72 73pub(crate) enum MethodKind { 74 Multikey, 75 LegacyK256, 76 LegacyP256, 77 None, 78} 79 80pub(crate) struct DocSpec<'a> { 81 pub id: &'a str, 82 pub signing: &'a SigningKey, 83 pub handle: &'a str, 84 pub pds: &'a str, 85 pub method: MethodKind, 86} 87 88pub(crate) fn did_doc(spec: DocSpec) -> Bytes { 89 let raw = sec1(spec.signing); 90 let legacy = || format!("z{}", bs58::encode(&raw).into_string()); 91 let method_entry: Option<(&'static str, String)> = match spec.method { 92 MethodKind::Multikey => Some(("Multikey", knot_types::crypto::multikey(0xe7, &raw))), 93 MethodKind::LegacyK256 => Some(("EcdsaSecp256k1VerificationKey2019", legacy())), 94 MethodKind::LegacyP256 => Some(("EcdsaSecp256r1VerificationKey2019", legacy())), 95 MethodKind::None => None, 96 }; 97 let verification_method: Value = match method_entry { 98 Some((kind, multibase)) => serde_json::json!([{ 99 "id": format!("{}#atproto", spec.id), 100 "type": kind, 101 "controller": spec.id, 102 "publicKeyMultibase": multibase, 103 }]), 104 None => serde_json::json!([]), 105 }; 106 let body = serde_json::json!({ 107 "id": spec.id, 108 "alsoKnownAs": [format!("at://{}", spec.handle)], 109 "verificationMethod": verification_method, 110 "service": [{ 111 "id": "#atproto_pds", 112 "type": "AtprotoPersonalDataServer", 113 "serviceEndpoint": spec.pds, 114 }] 115 }); 116 Bytes::from(serde_json::to_vec(&body).unwrap()) 117} 118 119pub(crate) fn ssh_line(algo: &str, material: &[u8], comment: &str) -> String { 120 let ssh_string = |bytes: &[u8]| [&(bytes.len() as u32).to_be_bytes()[..], bytes].concat(); 121 let blob = [ssh_string(algo.as_bytes()), ssh_string(material)].concat(); 122 format!("{algo} {} {comment}", STANDARD.encode(blob)) 123} 124 125pub(crate) fn list_body(lines: &[String], cursor: Option<&str>) -> Bytes { 126 let records: Vec<_> = lines 127 .iter() 128 .enumerate() 129 .map(|(index, line)| { 130 serde_json::json!({ 131 "uri": format!("at://{SQUID}/sh.tangled.publicKey/{index}"), 132 "value": { "$type": "sh.tangled.publicKey", "key": line, "name": "k", "createdAt": "2026-06-08T00:00:00Z" } 133 }) 134 }) 135 .collect(); 136 let cursor_field: Value = cursor.map_or(Value::Null, |value| serde_json::json!(value)); 137 let body = serde_json::json!({ "records": records, "cursor": cursor_field }); 138 Bytes::from(serde_json::to_vec(&body).unwrap()) 139} 140 141pub(crate) fn ok(body: Bytes) -> HttpResponse { 142 status(StatusCode::OK, body) 143} 144 145pub(crate) fn status(status: StatusCode, body: Bytes) -> HttpResponse { 146 HttpResponse { 147 status, 148 headers: http::HeaderMap::new(), 149 body, 150 } 151} 152 153pub(crate) fn mint(signing: &SigningKey, claims: &Value) -> ServiceJwt { 154 mint_with_header(signing, br#"{"alg":"ES256K","typ":"JWT"}"#, claims) 155} 156 157pub(crate) fn mint_with_header(signing: &SigningKey, header: &[u8], claims: &Value) -> ServiceJwt { 158 let header_b64 = URL_SAFE_NO_PAD.encode(header); 159 let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).unwrap()); 160 let signing_input = format!("{header_b64}.{payload}"); 161 let signature: Signature = signing.sign(signing_input.as_bytes()); 162 ServiceJwt::new(format!( 163 "{signing_input}.{}", 164 URL_SAFE_NO_PAD.encode(signature.to_bytes()) 165 )) 166 .expect("minted token is structurally a JWT") 167} 168 169pub(crate) fn runtime_signer(seed: u64) -> K256Signer { 170 K256Signer::generate(&SeededEntropy::new(seed)) 171} 172 173pub(crate) fn entropy(seed: u64) -> SeededEntropy { 174 SeededEntropy::new(seed) 175} 176 177pub(crate) fn repo_nonce(seed: u64) -> MintNonce { 178 MintNonce::mint( 179 &entropy(seed), 180 &owner_did("did:plc:nel"), 181 &RepoRkey::new("anemone").unwrap(), 182 ) 183} 184 185pub(crate) fn member_pointer() -> knot_lexicons::sh_tangled::knot::member::Member { 186 knot_lexicons::sh_tangled::knot::member::Member { 187 created_at: knot_types::Datetime::raw_str("2026-06-11T00:00:00Z"), 188 domain: "knot.nel.pet".into(), 189 subject: knot_types::Did::new_owned("did:plc:lyna").unwrap(), 190 extra_data: None, 191 } 192} 193 194pub(crate) type UrlLog = Arc<Mutex<Vec<Url>>>; 195 196pub(crate) fn recorder() -> (UrlLog, UrlLog) { 197 let urls: UrlLog = Arc::new(Mutex::new(Vec::new())); 198 (urls.clone(), urls) 199}