Blob: tests/serve.rs
Raw · Blame
use std::env;
use std::fs;
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use tempfile::TempDir;
mod ssh_fixture;
use ssh_fixture::create_ssh_key;
#[test]
fn serves_an_imported_repository_through_http_and_ssh() {
let instance = TempDir::new().expect("create an instance directory");
let http = free_address();
let ssh = free_address();
let config = instance.path().join("config.toml");
fs::write(
&config,
format!(
"version = 1\npublic_url = \"http://{http}/\"\n\n\
[http]\nlisten = \"{http}\"\n\n\
[ssh]\nlisten = \"{ssh}\"\npublic_host = \"127.0.0.1\"\npublic_port = {}\n",
ssh.port()
),
)
.expect("write the server configuration");
let private_key = instance.path().join("administrator");
create_ssh_key(&private_key);
let public_key = fs::read_to_string(private_key.with_extension("pub"))
.expect("read the administrator public key");
run_tit(
instance.path(),
&config,
&["setup", "admin", "alice", public_key.trim()],
);
let source = create_source_repository(instance.path());
run_tit(
instance.path(),
&config,
&[
"admin",
"repository",
"import",
"alice",
"example",
source.to_str().expect("a UTF-8 source path"),
],
);
let mut server = spawn_server(&config);
wait_for_listener(http, &mut server);
wait_for_listener(ssh, &mut server);
let health = http_get(http, "/healthz");
assert!(health.starts_with("HTTP/1.1 200"));
assert!(health.ends_with("\r\n\r\nready\n"));
let repository = http_get(http, "/alice/example");
assert!(repository.starts_with("HTTP/1.1 200"));
assert!(repository.contains("serve fixture"));
let http_clone = instance.path().join("http-clone");
run_git(
instance.path(),
&[
"clone",
"-q",
&format!("http://{http}/alice/example.git"),
http_clone.to_str().expect("a UTF-8 clone path"),
],
);
assert_eq!(
fs::read(http_clone.join("README.md")).expect("read the HTTP clone"),
b"serve fixture\n"
);
let ssh_clone = instance.path().join("ssh-clone");
let ssh_command = format!(
"ssh -F /dev/null -i {} -o BatchMode=yes -o IdentitiesOnly=yes \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR",
private_key.display()
);
let output = Command::new("git")
.args([
"clone",
"-q",
&format!("ssh://ignored@127.0.0.1:{}/alice/example.git", ssh.port()),
])
.arg(&ssh_clone)
.env("GIT_SSH_COMMAND", ssh_command)
.output()
.expect("clone through the SSH server");
assert!(
output.status.success(),
"SSH clone failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
fs::read(ssh_clone.join("README.md")).expect("read the SSH clone"),
b"serve fixture\n"
);
server.terminate();
}
fn free_address() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind a temporary port");
listener.local_addr().expect("read the temporary address")
}
fn create_source_repository(parent: &Path) -> PathBuf {
let worktree = parent.join("source-worktree");
run_git(
parent,
&[
"init",
"-q",
"-b",
"main",
worktree.to_str().expect("a UTF-8 worktree path"),
],
);
fs::write(worktree.join("README.md"), b"serve fixture\n").expect("write source content");
run_git(&worktree, &["add", "."]);
let output = Command::new("git")
.args(["commit", "-q", "-m", "initial"])
.env("GIT_AUTHOR_NAME", "Tit Test")
.env("GIT_AUTHOR_EMAIL", "tit@example.test")
.env("GIT_COMMITTER_NAME", "Tit Test")
.env("GIT_COMMITTER_EMAIL", "tit@example.test")
.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "commit.gpgsign")
.env("GIT_CONFIG_VALUE_0", "false")
.current_dir(&worktree)
.output()
.expect("commit source content");
assert!(
output.status.success(),
"Git commit failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let bare = parent.join("source.git");
run_git(
parent,
&[
"clone",
"-q",
"--bare",
worktree.to_str().expect("a UTF-8 worktree path"),
bare.to_str().expect("a UTF-8 bare path"),
],
);
bare
}
fn run_git(directory: &Path, arguments: &[&str]) {
let output = Command::new("git")
.args(arguments)
.current_dir(directory)
.output()
.expect("run Git");
assert!(
output.status.success(),
"Git command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn run_tit(directory: &Path, config: &Path, arguments: &[&str]) {
let output = Command::new(tit_binary())
.arg("--config")
.arg(config)
.args(arguments)
.current_dir(directory)
.output()
.expect("run tit");
assert!(
output.status.success(),
"tit command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn tit_binary() -> PathBuf {
env::var_os("TIT_RELEASE_BINARY")
.map(Into::into)
.unwrap_or_else(|| env!("CARGO_BIN_EXE_tit").into())
}
fn spawn_server(config: &Path) -> ChildGuard {
let child = Command::new(tit_binary())
.arg("--config")
.arg(config)
.arg("serve")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("start the tit server");
ChildGuard(Some(child))
}
fn wait_for_listener(address: SocketAddr, server: &mut ChildGuard) {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
if TcpStream::connect(address).is_ok() {
return;
}
if let Some(status) = server
.0
.as_mut()
.expect("a running server")
.try_wait()
.expect("check the server")
{
let mut stderr = String::new();
if let Some(pipe) = server.0.as_mut().expect("a stopped server").stderr.as_mut() {
pipe.read_to_string(&mut stderr)
.expect("read the stopped server error output");
}
panic!("tit serve stopped early with {status}: {stderr}");
}
assert!(
Instant::now() < deadline,
"listener {address} did not start"
);
thread::sleep(Duration::from_millis(20));
}
}
fn http_get(address: SocketAddr, path: &str) -> String {
let mut stream = TcpStream::connect(address).expect("connect to the HTTP server");
let timeout = Some(Duration::from_secs(10));
stream
.set_read_timeout(timeout)
.expect("set the HTTP read timeout");
stream
.set_write_timeout(timeout)
.expect("set the HTTP write timeout");
write!(
stream,
"GET {path} HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n\r\n"
)
.expect("write an HTTP request");
let mut response = String::new();
stream
.read_to_string(&mut response)
.expect("read an HTTP response");
response
}
struct ChildGuard(Option<Child>);
impl ChildGuard {
fn terminate(&mut self) {
let mut child = self.0.take().expect("the server process is active");
let signal = Command::new("kill")
.args(["-TERM", &child.id().to_string()])
.output()
.expect("send SIGTERM to the tit server");
assert!(signal.status.success(), "cannot send SIGTERM");
let status = child.wait().expect("wait for the tit server");
assert!(status.success(), "tit serve did not stop cleanly: {status}");
}
}
impl Drop for ChildGuard {
fn drop(&mut self) {
if let Some(mut child) = self.0.take() {
child.kill().expect("stop the tit server");
child.wait().expect("wait for the tit server");
}
}
}