/ src / test.rs
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  }