mod common;
use common::*;
use std::time::{Duration, Instant};
use lonca_daemon::protocol::Request;
fn upstream(root: &Path, name: &str, text: &str) -> PathBuf {
let up = root.join(name);
std::fs::create_dir_all(&up).unwrap();
git(&up, &["init", "-q", "-b", "main"]);
git(&up, &["config", "user.email", "t@t"]);
git(&up, &["config", "user.name", "t"]);
std::fs::write(up.join("README.md"), text).unwrap();
git(&up, &["add", "."]);
git(&up, &["commit", "-q", "-m", text]);
up
}
fn silent_server() -> (u16, std::thread::JoinHandle<()>) {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let t = std::thread::spawn(move || {
let mut held = Vec::new();
for c in l.incoming().take(4).flatten() {
held.push(c);
}
std::thread::sleep(Duration::from_secs(30));
});
(port, t)
}
fn request(token: &str, method: &str, params: Value) -> Request {
Request {
id: 1,
method: method.into(),
repo: Some("demo".into()),
token: Some(token.into()),
params,
}
}
#[test]
fn a_silent_remote_times_out_and_holds_nobody_up() {
let mut e = Env::new();
e.settings.git_timeout = Duration::from_secs(2);
e.reopen();
e.init(None);
let token = e.login().unwrap();
let (port, _server) = silent_server();
let remote = format!("https://127.0.0.1:{port}/repo.git");
let svc = e.svc.clone();
let (t, r) = (token.clone(), remote.clone());
let started = Instant::now();
let fetch = std::thread::spawn(move || {
svc.handle(request(&t, "human.prepare_import", json!({"remote": r})))
});
let prep = e.ok(
"human.prepare_push",
Some("demo"),
None,
json!({"remote": remote}),
);
let op: Op = serde_json::from_value(prep["op"].clone()).unwrap();
let body = e.sign(&op);
let svc = e.svc.clone();
let t = token.clone();
let push = std::thread::spawn(move || svc.handle(request(&t, "human.submit", body)));
std::thread::sleep(Duration::from_millis(300));
for _ in 0..5 {
let asked = Instant::now();
e.ok("status", Some("demo"), None, json!({}));
assert!(
asked.elapsed() < Duration::from_millis(500),
"status waited"
);
std::thread::sleep(Duration::from_millis(100));
}
for (what, handle) in [("fetch", fetch), ("push", push)] {
let r = handle.join().unwrap();
let err = r.error.unwrap_or_else(|| panic!("{what} succeeded"));
assert_eq!(err.code, "git", "{what}: {}", err.message);
assert!(
err.message.contains("did not finish"),
"{what}: {}",
err.message
);
}
let took = started.elapsed();
assert!(
took >= Duration::from_secs(2) && took < Duration::from_secs(15),
"{took:?}"
);
}
#[test]
fn local_remotes_stay_out_of_the_daemon_home() {
let mut e = Env::new();
e.init(None);
let try_import = |e: &mut Env, remote: &Path| {
e.call(
"human.prepare_import",
Some("demo"),
None,
json!({"remote": remote}),
)
};
let store = e.root.join("home/repos/demo.git");
let r = try_import(&mut e, &store);
let (code, message) = r.unwrap_err();
assert_eq!(code, "forbidden");
assert!(message.contains("inside the daemon home"), "{message}");
let home = e.root.join("home");
let r = try_import(&mut e, &home);
assert_eq!(r.unwrap_err().0, "forbidden");
let link = e.root.join("link.git");
std::os::unix::fs::symlink(&store, &link).unwrap();
assert_eq!(try_import(&mut e, &link).unwrap_err().0, "forbidden");
let nowhere = e.root.join("nowhere");
let (code, message) = try_import(&mut e, &nowhere).unwrap_err();
assert_eq!(code, "forbidden");
assert!(message.contains("does not exist"), "{message}");
let passed = |r: Result<Value, (String, String)>| {
r.err().is_none_or(|(code, _)| code == "not_fast_forward")
};
let up = upstream(&e.root, "up", "one\n");
let r = try_import(&mut e, &up);
assert!(passed(r.clone()), "{r:?}");
e.settings.local_roots = Some(vec![e.root.join("allowed")]);
e.reopen();
let r = try_import(&mut e, &up);
let (code, message) = r.unwrap_err();
assert_eq!(code, "forbidden");
assert!(message.contains("--local-root"), "{message}");
let inside = upstream(&e.root.join("allowed"), "up", "two\n");
let r = try_import(&mut e, &inside);
assert!(passed(r.clone()), "{r:?}");
}
#[test]
fn the_import_directory_gives_git_only_a_copy_of_a_bundle() {
let mut e = Env::new();
let dir = e.root.join("import");
std::fs::create_dir_all(&dir).unwrap();
e.settings.local_roots = Some(vec![]);
e.settings.import_dir = Some(dir.clone());
e.reopen();
let up = upstream(&e.root, "up", "one\n");
let bundle = dir.join("up.bundle");
git(
&up,
&["bundle", "create", "-q", bundle.to_str().unwrap(), "main"],
);
let head = String::from_utf8(
std::process::Command::new("git")
.args(["-C", up.to_str().unwrap(), "rev-parse", "HEAD"])
.output()
.unwrap()
.stdout,
)
.unwrap();
e.init(Some(bundle.to_str().unwrap()));
let st = e.ok("status", Some("demo"), None, json!({}));
assert_eq!(st["trunk"], json!(head.trim()), "{st}");
let imports = e.root.join("home/imports");
assert_eq!(std::fs::read_dir(&imports).unwrap().count(), 0, "copy gone");
std::fs::write(imports.join("import-x.bundle"), "left").unwrap();
e.reopen();
assert!(!imports.exists());
let try_import = |e: &mut Env, remote: &Path| {
e.call(
"human.prepare_import",
Some("demo"),
None,
json!({"remote": remote}),
)
.unwrap_err()
};
let refused = |(code, message): (String, String), why: &str| {
assert_eq!(code, "forbidden", "{message}");
assert!(message.contains(why), "{message}");
};
let (code, _) = try_import(&mut e, &bundle);
assert_eq!(code, "up_to_date");
std::os::unix::fs::symlink(&bundle, dir.join("link.bundle")).unwrap();
refused(try_import(&mut e, &dir.join("link.bundle")), "a link");
let repo = dir.join("repo.git");
git(&e.root, &["init", "-q", "--bare", repo.to_str().unwrap()]);
refused(try_import(&mut e, &repo), "not a regular file");
std::fs::create_dir_all(dir.join("sub")).unwrap();
std::fs::copy(&bundle, dir.join("sub/up.bundle")).unwrap();
refused(
try_import(&mut e, &dir.join("sub/up.bundle")),
"directly in",
);
refused(try_import(&mut e, &dir), "directly in");
let text = dir.join("notes.txt");
std::fs::write(&text, "hello\n").unwrap();
refused(try_import(&mut e, &text), "not a git bundle");
let outside = e.root.join("outside.bundle");
std::os::unix::fs::symlink(&bundle, &outside).unwrap();
refused(try_import(&mut e, &outside), "only read");
let r = e.call(
"human.prepare_push",
Some("demo"),
None,
json!({"remote": dir.join("out.git")}),
);
let pushed_into = std::fs::create_dir(dir.join("out.git"));
assert!(pushed_into.is_ok());
let r2 = e.call(
"human.prepare_push",
Some("demo"),
None,
json!({"remote": dir.join("out.git")}),
);
assert_eq!(r.unwrap_err().0, "forbidden", "a missing path");
refused(r2.unwrap_err(), "only read");
assert!(!imports.exists() || std::fs::read_dir(&imports).unwrap().count() == 0);
}
#[test]
fn without_an_import_directory_a_bundle_is_a_local_path() {
let mut e = Env::new();
let up = upstream(&e.root, "up", "one\n");
let bundle = e.root.join("up.bundle");
git(
&up,
&["bundle", "create", "-q", bundle.to_str().unwrap(), "main"],
);
e.init(Some(bundle.to_str().unwrap()));
assert!(!e.root.join("home/imports").exists());
}