test.rs
1 use std::collections::BTreeSet; 2 use std::fs; 3 use std::path::Path; 4 use std::str::FromStr; 5 use std::sync::Arc; 6 7 use axum::body::{Body, Bytes}; 8 use axum::http::{Method, Request}; 9 use axum::Router; 10 use serde_json::Value; 11 use tower::ServiceExt; 12 13 use radicle::cob::migrate; 14 use radicle::cob::patch::MergeTarget; 15 use radicle::cob::Title; 16 use radicle::crypto::signature::Signer; 17 use radicle::crypto::ssh::Keystore; 18 use radicle::crypto::{KeyPair, Seed, Signature}; 19 use radicle::git::fmt::RefString; 20 use radicle::identity::{project, Visibility}; 21 use radicle::node::device::Device; 22 use radicle::node::{Features, Timestamp, UserAgent}; 23 use radicle::profile::{env, Home}; 24 use radicle::storage::ReadStorage; 25 use radicle::{node, profile}; 26 use radicle::{Node, Storage}; 27 28 use crate::api::Context; 29 30 pub const RID: &str = "rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp"; 31 pub const RID_PRIVATE: &str = "rad:zLuTzcmoWMcdK37xqArS8eckp9vK"; 32 pub const HEAD: &str = "e8c676b9e3b42308dc9d218b70faa5408f8e58ca"; 33 pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83"; 34 pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7"; 35 pub const DID: &str = "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"; 36 pub const ISSUE_ID: &str = "ca67d195c0b308b51810dedd93157a20764d5db5"; 37 pub const PATCH_ID: &str = "b4084412ea3644d7dd7ae075c1cbbd4d702db0ec"; 38 pub const TIMESTAMP: u64 = 1671125284; 39 pub const CONTRIBUTOR_ALIAS: &str = "seed"; 40 41 /// Create a new profile. 42 pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { 43 let home = Home::new(home).unwrap(); 44 let keystore = Keystore::new(&home.keys()); 45 let keypair = KeyPair::from_seed(Seed::from(seed)); 46 let alias = node::Alias::new("seed"); 47 let storage = Storage::open( 48 home.storage(), 49 radicle::git::UserInfo { 50 alias: alias.clone(), 51 key: keypair.pk.into(), 52 }, 53 ) 54 .unwrap(); 55 56 let mut db = home.policies_mut().unwrap(); 57 db.follow(&keypair.pk.into(), Some(&alias)).unwrap(); 58 59 let node_db = home.database_mut().unwrap(); 60 node_db 61 .init( 62 &keypair.pk.into(), 63 Features::SEED, 64 &alias, 65 &UserAgent::default(), 66 Timestamp::try_from(TIMESTAMP).unwrap(), 67 [], 68 ) 69 .unwrap(); 70 71 // Migrate COBs cache. 72 let mut cobs = home.cobs_db_mut().unwrap(); 73 cobs.migrate(migrate::ignore).unwrap(); 74 75 radicle::storage::git::transport::local::register(storage.clone()); 76 keystore.store(keypair.clone(), "radicle", None).unwrap(); 77 78 radicle::Profile { 79 home, 80 storage, 81 keystore, 82 public_key: keypair.pk.into(), 83 config: profile::Config::new(alias), 84 } 85 } 86 87 pub fn seed(dir: &Path) -> Context { 88 let home = dir.join("radicle"); 89 let profile = profile(home.as_path(), [0xff; 32]); 90 let signer = Device::mock_from_seed([0xff; 32]); 91 92 crate::logger::init().ok(); 93 94 seed_with_signer(dir, profile, &signer) 95 } 96 97 fn seed_with_signer<G: Signer<Signature>>( 98 dir: &Path, 99 profile: radicle::Profile, 100 signer: &Device<G>, 101 ) -> Context { 102 const DEFAULT_BRANCH: &str = "master"; 103 104 crate::logger::init().ok(); 105 106 profile.policies_mut().unwrap(); 107 profile.database_mut().unwrap(); // Create the database. 108 109 let mut policies = profile.policies_mut().unwrap(); 110 let workdir = dir.join("hello-world-private"); 111 fs::create_dir_all(&workdir).unwrap(); 112 113 // add commits to workdir (repo) 114 let mut opts = radicle::git::raw::RepositoryInitOptions::new(); 115 opts.initial_head(DEFAULT_BRANCH); 116 let repo = radicle::git::raw::Repository::init_opts(&workdir, &opts).unwrap(); 117 let tree = radicle::git::write_tree( 118 Path::new("README"), 119 "Hello Private World!\n".as_bytes(), 120 &repo, 121 ) 122 .unwrap(); 123 124 let sig_time = radicle::git::raw::Time::new(1673001014, 0); 125 let sig = 126 radicle::git::raw::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap(); 127 128 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[]) 129 .unwrap(); 130 131 // rad init 132 let repo = radicle::git::raw::Repository::open(&workdir).unwrap(); 133 let name = project::ProjectName::from_str("hello-world-private").unwrap(); 134 let description = "Private Rad repository for tests".to_string(); 135 let branch = RefString::try_from(DEFAULT_BRANCH).unwrap(); 136 let visibility = Visibility::Private { 137 allow: BTreeSet::default(), 138 }; 139 let (rid, _, _) = radicle::rad::init( 140 &repo, 141 name, 142 &description, 143 branch, 144 visibility, 145 signer, 146 &profile.storage, 147 ) 148 .unwrap(); 149 150 policies 151 .set_seed_policy(&rid, node::policy::Policy::Block) 152 .unwrap(); 153 154 let workdir = dir.join("hello-world"); 155 156 env::set_var(env::GIT_COMMITTER_DATE, TIMESTAMP.to_string()); 157 158 fs::create_dir_all(&workdir).unwrap(); 159 160 // add commits to workdir (repo) 161 let mut opts = radicle::git::raw::RepositoryInitOptions::new(); 162 opts.initial_head(DEFAULT_BRANCH); 163 let repo = radicle::git::raw::Repository::init_opts(&workdir, &opts).unwrap(); 164 let tree = 165 radicle::git::write_tree(Path::new("README"), "Hello World!\n".as_bytes(), &repo).unwrap(); 166 167 let sig_time = radicle::git::raw::Time::new(1673001014, 0); 168 let sig = 169 radicle::git::raw::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap(); 170 171 let oid = repo 172 .commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[]) 173 .unwrap(); 174 let commit = repo.find_commit(oid).unwrap(); 175 176 repo.checkout_tree(tree.as_object(), None).unwrap(); 177 178 let tree = radicle::git::write_tree( 179 Path::new("CONTRIBUTING"), 180 "Thank you very much!\n".as_bytes(), 181 &repo, 182 ) 183 .unwrap(); 184 let sig_time = radicle::git::raw::Time::new(1673002014, 0); 185 let sig = 186 radicle::git::raw::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap(); 187 188 let oid2 = repo 189 .commit( 190 Some("HEAD"), 191 &sig, 192 &sig, 193 "Add contributing file\n", 194 &tree, 195 &[&commit], 196 ) 197 .unwrap(); 198 let commit2 = repo.find_commit(oid2).unwrap(); 199 200 repo.checkout_tree(tree.as_object(), None).unwrap(); 201 202 fs::create_dir(workdir.join("dir1")).unwrap(); 203 fs::write( 204 workdir.join("dir1").join("README"), 205 "Hello World from dir1!\n", 206 ) 207 .unwrap(); 208 let mut index = repo.index().unwrap(); 209 index.add_path(Path::new("dir1/README")).unwrap(); 210 index.write().unwrap(); 211 212 let oid = index.write_tree().unwrap(); 213 let tree = repo.find_tree(oid).unwrap(); 214 215 let sig_time = radicle::git::raw::Time::new(1673003014, 0); 216 let sig = 217 radicle::git::raw::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap(); 218 repo.commit( 219 Some("HEAD"), 220 &sig, 221 &sig, 222 "Add another folder\n", 223 &tree, 224 &[&commit2], 225 ) 226 .unwrap(); 227 228 // rad init 229 let repo = radicle::git::raw::Repository::open(&workdir).unwrap(); 230 let name = project::ProjectName::from_str("hello-world").unwrap(); 231 let description = "Rad repository for tests".to_string(); 232 let branch = RefString::try_from(DEFAULT_BRANCH).unwrap(); 233 let visibility = Visibility::default(); 234 let (rid, _, _) = radicle::rad::init( 235 &repo, 236 name, 237 &description, 238 branch, 239 visibility, 240 signer, 241 &profile.storage, 242 ) 243 .unwrap(); 244 policies.seed(&rid, node::policy::Scope::All).unwrap(); 245 let node_handle = &mut Node::new(profile.socket()); 246 profile 247 .seed(rid, node::policy::Scope::All, node_handle) 248 .unwrap(); 249 profile.add_inventory(rid, node_handle).unwrap(); 250 251 let storage = &profile.storage; 252 let repo = storage.repository(rid).unwrap(); 253 let mut issues = profile.issues_mut(&repo).unwrap(); 254 let issue = issues 255 .create( 256 Title::new("Issue #1").unwrap(), 257 "Change 'hello world' to 'hello everyone'".to_string(), 258 &[], 259 &[], 260 [], 261 signer, 262 ) 263 .unwrap(); 264 tracing::debug!(target: "test", "Contributor issue: {}", issue.id()); 265 266 // eq. rad patch open 267 let mut patches = profile.patches_mut(&repo).unwrap(); 268 let oid = radicle::git::Oid::from_str(HEAD).unwrap(); 269 let base = radicle::git::Oid::from_str(PARENT).unwrap(); 270 let patch = patches 271 .create( 272 Title::new("A new `hello world`").unwrap(), 273 "change `hello world` in README to something else", 274 MergeTarget::Delegates, 275 base, 276 oid, 277 &[], 278 signer, 279 ) 280 .unwrap(); 281 tracing::debug!(target: "test", "Contributor patch: {}", patch.id()); 282 283 let workdir = dir.join("again-hello-world"); 284 285 env::set_var(env::GIT_COMMITTER_DATE, TIMESTAMP.to_string()); 286 287 fs::create_dir_all(&workdir).unwrap(); 288 289 // add commits to workdir (repo) 290 let mut opts = radicle::git::raw::RepositoryInitOptions::new(); 291 opts.initial_head(DEFAULT_BRANCH); 292 let repo = radicle::git::raw::Repository::init_opts(&workdir, &opts).unwrap(); 293 let tree = radicle::git::write_tree( 294 Path::new("README"), 295 "Hello World Again!\n".as_bytes(), 296 &repo, 297 ) 298 .unwrap(); 299 300 let sig_time = radicle::git::raw::Time::new(1673001014, 0); 301 let sig = 302 radicle::git::raw::Signature::new("Alice Liddell", "alice@radicle.xyz", &sig_time).unwrap(); 303 304 repo.commit(Some("HEAD"), &sig, &sig, "Initial commit\n", &tree, &[]) 305 .unwrap(); 306 307 repo.checkout_tree(tree.as_object(), None).unwrap(); 308 309 // rad init 310 let repo = radicle::git::raw::Repository::open(&workdir).unwrap(); 311 let name = project::ProjectName::from_str("again-hello-world").unwrap(); 312 let description = "Rad repository for sorting".to_string(); 313 let branch = RefString::try_from(DEFAULT_BRANCH).unwrap(); 314 let visibility = Visibility::default(); 315 let (rid, _, _) = radicle::rad::init( 316 &repo, 317 name, 318 &description, 319 branch, 320 visibility, 321 signer, 322 &profile.storage, 323 ) 324 .unwrap(); 325 policies.seed(&rid, node::policy::Scope::Followed).unwrap(); 326 profile 327 .seed(rid, node::policy::Scope::Followed, node_handle) 328 .unwrap(); 329 profile.add_inventory(rid, node_handle).unwrap(); 330 331 let options = crate::Options { 332 aliases: std::collections::HashMap::new(), 333 listen: axum_listener::DualAddr::Tcp(std::net::SocketAddr::from(([0, 0, 0, 0], 8080))), 334 cache: Some(crate::DEFAULT_CACHE_SIZE), 335 }; 336 337 let web_config = crate::api::WebConfig::from_profile(&profile); 338 Context::new(Arc::new(profile), web_config, &options) 339 } 340 341 pub async fn get(app: &Router, path: impl ToString) -> Response { 342 Response( 343 app.clone() 344 .oneshot(request(path, Method::GET, None)) 345 .await 346 .unwrap(), 347 ) 348 } 349 350 fn request(path: impl ToString, method: Method, body: Option<Body>) -> Request<Body> { 351 let request = Request::builder() 352 .method(method) 353 .uri(path.to_string()) 354 .header("Content-Type", "application/json"); 355 356 request.body(body.unwrap_or_else(Body::empty)).unwrap() 357 } 358 359 #[derive(Debug)] 360 pub struct Response(axum::response::Response); 361 362 impl Response { 363 pub async fn json(self) -> Value { 364 let body = self.body().await; 365 serde_json::from_slice(&body).unwrap() 366 } 367 368 pub fn status(&self) -> axum::http::StatusCode { 369 self.0.status() 370 } 371 372 pub async fn body(self) -> Bytes { 373 axum::body::to_bytes(self.0.into_body(), usize::MAX) 374 .await 375 .unwrap() 376 } 377 }