205 lines
7.4 KiB
Rust
205 lines
7.4 KiB
Rust
//! End-to-end fork test:
|
|
//! 1. Spin up an in-process levcs instance.
|
|
//! 2. Run the levcs binary to init a source repo, commit, point at the
|
|
//! instance, and push.
|
|
//! 3. Run the levcs binary in a fresh directory to fork the source.
|
|
//! 4. Run `levcs verify` in the fork to confirm the fork commit and chain.
|
|
|
|
use std::net::SocketAddr;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
|
|
use levcs_instance::{router, AppState, InstanceConfig};
|
|
|
|
fn levcs_bin() -> String {
|
|
env!("CARGO_BIN_EXE_levcs").to_string()
|
|
}
|
|
|
|
fn run(args: &[&str], cwd: &Path, xdg: &Path, extra: &[(&str, &str)]) -> (i32, String, String) {
|
|
let mut cmd = Command::new(levcs_bin());
|
|
cmd.args(args).current_dir(cwd).env("XDG_CONFIG_HOME", xdg);
|
|
for (k, v) in extra {
|
|
cmd.env(k, v);
|
|
}
|
|
let out = cmd.output().expect("run levcs");
|
|
(
|
|
out.status.code().unwrap_or(-1),
|
|
String::from_utf8_lossy(&out.stdout).to_string(),
|
|
String::from_utf8_lossy(&out.stderr).to_string(),
|
|
)
|
|
}
|
|
|
|
fn tempdir(prefix: &str) -> PathBuf {
|
|
let mut p = std::env::temp_dir();
|
|
let n = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_nanos())
|
|
.unwrap_or(0);
|
|
p.push(format!("{prefix}-{n}-{}", std::process::id()));
|
|
std::fs::create_dir_all(&p).unwrap();
|
|
p
|
|
}
|
|
|
|
/// Spin up an axum instance bound to an ephemeral port. Returns (addr,
|
|
/// shutdown_handle).
|
|
async fn start_instance(root: PathBuf) -> (SocketAddr, tokio::task::JoinHandle<()>) {
|
|
let cfg = InstanceConfig {
|
|
root,
|
|
storage_mode: "full".into(),
|
|
federation_peers: Vec::new(),
|
|
allowed_handlers: vec!["builtin".into()],
|
|
mirrors: Vec::new(),
|
|
};
|
|
let state = AppState::new(cfg);
|
|
let app = router(state);
|
|
let listener = tokio::net::TcpListener::bind::<SocketAddr>("127.0.0.1:0".parse().unwrap())
|
|
.await
|
|
.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let task = tokio::spawn(async move {
|
|
axum::serve(listener, app).await.ok();
|
|
});
|
|
(addr, task)
|
|
}
|
|
|
|
#[test]
|
|
fn fork_end_to_end() {
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
|
|
// 1. Boot an instance.
|
|
let instance_root = tempdir("levcs-fork-instance");
|
|
let instance_root_for_task = instance_root.clone();
|
|
let (addr, server_task) =
|
|
runtime.block_on(async move { start_instance(instance_root_for_task).await });
|
|
let base_url = format!("http://{addr}/levcs/v1");
|
|
|
|
// 2. Source repo.
|
|
let source = tempdir("levcs-fork-source");
|
|
let xdg = tempdir("levcs-fork-cfg");
|
|
|
|
let (code, _, e) = run(&["init", "--key", "alice"], &source, &xdg, &[]);
|
|
assert_eq!(code, 0, "init: {e}");
|
|
std::fs::write(source.join("README"), b"source repo content\n").unwrap();
|
|
let (code, _, e) = run(&["track", "--all"], &source, &xdg, &[]);
|
|
assert_eq!(code, 0, "track: {e}");
|
|
let (code, _, e) = run(&["commit", "-m", "initial"], &source, &xdg, &[]);
|
|
assert_eq!(code, 0, "commit: {e}");
|
|
|
|
// Point at the instance and push.
|
|
let (code, _, e) = run(&["instance", "--set", &base_url], &source, &xdg, &[]);
|
|
assert_eq!(code, 0, "instance --set: {e}");
|
|
let (code, _, e) = run(&["push"], &source, &xdg, &[]);
|
|
assert_eq!(code, 0, "push: {e}");
|
|
|
|
// Look up the source's repo_id.
|
|
let (code, repo_id_out, _) = run(&["root"], &source, &xdg, &[]);
|
|
assert_eq!(code, 0);
|
|
let _ = repo_id_out;
|
|
// Easier: read genesis authority's repo_id from .levcs.
|
|
let genesis_hex = std::fs::read_to_string(source.join(".levcs/refs/authority/genesis"))
|
|
.unwrap()
|
|
.trim()
|
|
.to_string();
|
|
// Now read the genesis object from the instance via the running server.
|
|
// Or simpler: derive repo_id from the source repo by parsing its genesis.
|
|
let genesis_path = source
|
|
.join(".levcs/objects")
|
|
.join(&genesis_hex[..2])
|
|
.join(&genesis_hex[2..]);
|
|
let bytes = std::fs::read(&genesis_path).unwrap();
|
|
let signed = levcs_core::object::SignedObject::parse(&bytes).unwrap();
|
|
let body = levcs_identity::authority::AuthorityBody::parse(&signed.body).unwrap();
|
|
let repo_id_hex = body.repo_id.to_hex();
|
|
|
|
// 3. Fork into a new directory using a different key (bob).
|
|
let fork_parent = tempdir("levcs-fork-dest-parent");
|
|
// Generate bob in the same xdg keychain so load_secret can find him.
|
|
let (code, _, e) = run(&["key", "generate", "bob"], &fork_parent, &xdg, &[]);
|
|
assert_eq!(code, 0, "key generate bob: {e}");
|
|
let (code, _, e) = run(&["instance", "--set", &base_url], &fork_parent, &xdg, &[]);
|
|
assert_eq!(code, 0, "instance --set (fork): {e}");
|
|
|
|
let (code, _o, err) = run(
|
|
&["fork", &repo_id_hex, "--name", "myfork", "--key", "bob"],
|
|
&fork_parent,
|
|
&xdg,
|
|
&[],
|
|
);
|
|
assert_eq!(code, 0, "fork: {err}");
|
|
|
|
// 4. Verify the fork.
|
|
let fork_dir = fork_parent.join("myfork");
|
|
assert!(fork_dir.is_dir(), "fork directory not created");
|
|
assert!(
|
|
fork_dir.join("README").is_file(),
|
|
"source content not checked out"
|
|
);
|
|
assert_eq!(
|
|
std::fs::read_to_string(fork_dir.join("README")).unwrap(),
|
|
"source repo content\n"
|
|
);
|
|
|
|
let (code, _, e) = run(&["verify"], &fork_dir, &xdg, &[]);
|
|
assert_eq!(code, 0, "verify of fork: {e}");
|
|
|
|
// 5. Confirm the fork's repo_id differs from the source's.
|
|
let fork_genesis_hex = std::fs::read_to_string(fork_dir.join(".levcs/refs/authority/genesis"))
|
|
.unwrap()
|
|
.trim()
|
|
.to_string();
|
|
let fork_genesis_path = fork_dir
|
|
.join(".levcs/objects")
|
|
.join(&fork_genesis_hex[..2])
|
|
.join(&fork_genesis_hex[2..]);
|
|
let fork_bytes = std::fs::read(&fork_genesis_path).unwrap();
|
|
let fork_signed = levcs_core::object::SignedObject::parse(&fork_bytes).unwrap();
|
|
let fork_body = levcs_identity::authority::AuthorityBody::parse(&fork_signed.body).unwrap();
|
|
assert_ne!(
|
|
fork_body.repo_id, body.repo_id,
|
|
"fork repo_id must differ from source"
|
|
);
|
|
// Bob is the sole owner of the new genesis.
|
|
assert_eq!(fork_body.members.len(), 1);
|
|
assert_eq!(
|
|
fork_body.members[0].role,
|
|
levcs_identity::authority::Role::Owner
|
|
);
|
|
|
|
// 6. Confirm the fork commit has both flags set and a single parent.
|
|
let head_hex = std::fs::read_to_string(fork_dir.join(".levcs/refs/branches/main"))
|
|
.unwrap()
|
|
.trim()
|
|
.to_string();
|
|
let head_id = levcs_core::ObjectId::from_hex(&head_hex).unwrap();
|
|
let head_path = fork_dir
|
|
.join(".levcs/objects")
|
|
.join(&head_hex[..2])
|
|
.join(&head_hex[2..]);
|
|
let head_bytes = std::fs::read(&head_path).unwrap();
|
|
let head_signed = levcs_core::object::SignedObject::parse(&head_bytes).unwrap();
|
|
let head_commit = levcs_core::Commit::from_signed(&head_signed).unwrap();
|
|
assert!(
|
|
head_commit.flags.is_fork(),
|
|
"fork commit must have fork flag set"
|
|
);
|
|
assert!(
|
|
head_commit.flags.modifies_authority(),
|
|
"fork commit must have modifies-authority flag set"
|
|
);
|
|
assert_eq!(head_commit.parents.len(), 1);
|
|
let _ = head_id;
|
|
|
|
// Done. Tear down.
|
|
server_task.abort();
|
|
runtime.shutdown_background();
|
|
let _ = Arc::new(()); // keep tokio import live
|
|
let _ = std::fs::remove_dir_all(instance_root);
|
|
let _ = std::fs::remove_dir_all(source);
|
|
let _ = std::fs::remove_dir_all(fork_parent);
|
|
let _ = std::fs::remove_dir_all(xdg);
|
|
}
|