411 lines
12 KiB
Rust
411 lines
12 KiB
Rust
mod common;
|
|
|
|
use std::fs;
|
|
use std::io::{BufRead, BufReader, Read};
|
|
use std::path::Path;
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn frxd() -> Command {
|
|
Command::new(env!("CARGO_BIN_EXE_frxd"))
|
|
}
|
|
|
|
fn frx() -> Command {
|
|
Command::new(env!("CARGO_BIN_EXE_frx"))
|
|
}
|
|
|
|
fn init_args(
|
|
config: &Path,
|
|
name: &str,
|
|
listen_port: u16,
|
|
relay_port: u16,
|
|
data: &Path,
|
|
) -> Vec<String> {
|
|
vec![
|
|
"--config".to_string(),
|
|
config.display().to_string(),
|
|
"init".to_string(),
|
|
"--name".to_string(),
|
|
name.to_string(),
|
|
"--listen".to_string(),
|
|
format!("127.0.0.1:{listen_port}"),
|
|
"--relay".to_string(),
|
|
format!("http://127.0.0.1:{relay_port}"),
|
|
"--data-dir".to_string(),
|
|
data.display().to_string(),
|
|
]
|
|
}
|
|
|
|
fn add_args(config: &Path, docs: &Path, name: &str, shared: bool) -> Vec<String> {
|
|
let mut args = vec![
|
|
"--config".to_string(),
|
|
config.display().to_string(),
|
|
"add".to_string(),
|
|
docs.display().to_string(),
|
|
"--name".to_string(),
|
|
name.to_string(),
|
|
"--exposure".to_string(),
|
|
"full".to_string(),
|
|
];
|
|
if shared {
|
|
args.push("--shared".to_string());
|
|
}
|
|
args
|
|
}
|
|
|
|
fn run_ok(args: &[String]) -> String {
|
|
let output = frxd().args(args).output().unwrap();
|
|
assert!(
|
|
output.status.success(),
|
|
"command failed: {}\n{}",
|
|
args.join(" "),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
String::from_utf8_lossy(&output.stdout).to_string()
|
|
}
|
|
|
|
struct Service {
|
|
child: Child,
|
|
}
|
|
|
|
impl Drop for Service {
|
|
fn drop(&mut self) {
|
|
let _ = self.child.kill();
|
|
let _ = self.child.wait();
|
|
}
|
|
}
|
|
|
|
fn spawn_service(args: &[String], needle: &str) -> Service {
|
|
let mut child = frxd()
|
|
.args(args)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.unwrap();
|
|
let reader = BufReader::new(child.stdout.take().unwrap());
|
|
let mut reader = reader;
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
loop {
|
|
let mut line = String::new();
|
|
let read = reader.read_line(&mut line).unwrap();
|
|
if read == 0 {
|
|
panic!("service exited before printing '{needle}'");
|
|
}
|
|
if line.contains(needle) {
|
|
break;
|
|
}
|
|
assert!(Instant::now() < deadline, "timeout waiting for '{needle}'");
|
|
}
|
|
std::thread::spawn(move || {
|
|
let mut sink = Vec::new();
|
|
let _ = reader.read_to_end(&mut sink);
|
|
});
|
|
Service { child }
|
|
}
|
|
|
|
#[test]
|
|
fn cli_init_add_search_status() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let config = root.path().join("frxd.toml");
|
|
let data = root.path().join("data");
|
|
let docs = root.path().join("docs");
|
|
fs::create_dir_all(&docs).unwrap();
|
|
fs::write(docs.join("note.txt"), "cli smoke rust document").unwrap();
|
|
|
|
let config_arg = config.display().to_string();
|
|
let stdout = run_ok(&init_args(&config, "alice", 0, 1, &data));
|
|
assert!(stdout.contains("pubkey"));
|
|
assert!(config.exists());
|
|
let key = data.join("key.hex");
|
|
assert!(key.exists());
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mode = fs::metadata(&key).unwrap().permissions().mode() & 0o777;
|
|
assert_eq!(mode, 0o600, "key file must not be world readable");
|
|
}
|
|
|
|
let stdout = run_ok(&[
|
|
"--config".to_string(),
|
|
config_arg.clone(),
|
|
"member".to_string(),
|
|
"add".to_string(),
|
|
"carol".to_string(),
|
|
"ab".repeat(32),
|
|
"--class".to_string(),
|
|
"enrichment".to_string(),
|
|
]);
|
|
assert!(stdout.contains("carol"));
|
|
let stdout = run_ok(&[
|
|
"--config".to_string(),
|
|
config_arg.clone(),
|
|
"member".to_string(),
|
|
"list".to_string(),
|
|
]);
|
|
assert!(stdout.contains("carol [enrichment]"));
|
|
|
|
let stdout = run_ok(&add_args(&config, &docs, "docs", true));
|
|
assert!(stdout.contains("indexed 1 file(s)"));
|
|
|
|
let stdout = run_ok(&[
|
|
"--config".to_string(),
|
|
config_arg.clone(),
|
|
"search".to_string(),
|
|
"rust".to_string(),
|
|
]);
|
|
assert!(stdout.contains("cli smoke rust document"));
|
|
|
|
let output = frx()
|
|
.args(["--config", &config_arg, "search", "rust"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(output.status.success());
|
|
assert!(String::from_utf8_lossy(&output.stdout).contains("cli smoke rust document"));
|
|
|
|
let stdout = run_ok(&["--config".to_string(), config_arg, "status".to_string()]);
|
|
assert!(stdout.contains("node not running"));
|
|
}
|
|
|
|
#[test]
|
|
fn cli_init_refuses_overwrite_without_force() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let config = root.path().join("frxd.toml");
|
|
let data = root.path().join("data");
|
|
let args = init_args(&config, "alice", 0, 1, &data);
|
|
run_ok(&args);
|
|
|
|
let output = frxd().args(&args).output().unwrap();
|
|
assert!(!output.status.success());
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("already exists"));
|
|
|
|
let mut forced = args.clone();
|
|
forced.push("--force".to_string());
|
|
run_ok(&forced);
|
|
}
|
|
|
|
fn http_get(port: u16, path: &str) -> String {
|
|
use std::io::{Read, Write};
|
|
let mut stream = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap();
|
|
stream
|
|
.write_all(format!("GET {path} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").as_bytes())
|
|
.unwrap();
|
|
let mut out = String::new();
|
|
stream.read_to_string(&mut out).unwrap();
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn cli_registry_lifecycle() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let dir = root.path().join("reg");
|
|
let dir_arg = dir.display().to_string();
|
|
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"init".to_string(),
|
|
]);
|
|
assert!(stdout.contains("MA key"), "{stdout}");
|
|
assert!(dir.join("registry.json").exists());
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mode = fs::metadata(dir.join("ma-key.hex"))
|
|
.unwrap()
|
|
.permissions()
|
|
.mode()
|
|
& 0o777;
|
|
assert_eq!(mode, 0o600);
|
|
}
|
|
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"show".to_string(),
|
|
]);
|
|
assert!(stdout.contains("version 1"));
|
|
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"add".to_string(),
|
|
"alice.frx.example".to_string(),
|
|
"ab".repeat(32),
|
|
]);
|
|
assert!(stdout.contains("added alice.frx.example"));
|
|
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"add-key".to_string(),
|
|
"alice.frx.example".to_string(),
|
|
"cd".repeat(32),
|
|
]);
|
|
assert!(stdout.contains("added key"));
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"list".to_string(),
|
|
]);
|
|
assert!(stdout.contains("alice.frx.example [source] (2 key(s))"));
|
|
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"revoke-key".to_string(),
|
|
"alice.frx.example".to_string(),
|
|
"ab".repeat(32),
|
|
]);
|
|
assert!(stdout.contains("revoked key"));
|
|
let stdout = run_ok(&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg.clone(),
|
|
"list".to_string(),
|
|
]);
|
|
assert!(stdout.contains("(1 key(s))"));
|
|
|
|
let port = common::free_port();
|
|
let _service = spawn_service(
|
|
&[
|
|
"registry".to_string(),
|
|
"--dir".to_string(),
|
|
dir_arg,
|
|
"serve".to_string(),
|
|
"--listen".to_string(),
|
|
format!("127.0.0.1:{port}"),
|
|
],
|
|
"registry serving",
|
|
);
|
|
let body = http_get(port, "/registry.json");
|
|
assert!(body.contains("\"members\""), "{body}");
|
|
assert!(body.contains("alice.frx.example"), "{body}");
|
|
}
|
|
|
|
#[test]
|
|
fn cli_key_rotation() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let config = root.path().join("frxd.toml");
|
|
let data = root.path().join("data");
|
|
let config_arg = config.display().to_string();
|
|
run_ok(&init_args(&config, "alice", 0, 1, &data));
|
|
|
|
let first = run_ok(&[
|
|
"--config".to_string(),
|
|
config_arg.clone(),
|
|
"key".to_string(),
|
|
"show".to_string(),
|
|
]);
|
|
let first = first.trim().to_string();
|
|
assert_eq!(first.len(), 64);
|
|
|
|
let stdout = run_ok(&[
|
|
"--config".to_string(),
|
|
config_arg.clone(),
|
|
"key".to_string(),
|
|
"rotate".to_string(),
|
|
]);
|
|
assert!(stdout.contains("old pubkey"), "{stdout}");
|
|
assert!(stdout.contains("new pubkey"), "{stdout}");
|
|
|
|
let second = run_ok(&[
|
|
"--config".to_string(),
|
|
config_arg,
|
|
"key".to_string(),
|
|
"show".to_string(),
|
|
]);
|
|
assert_ne!(first, second.trim(), "key did not change");
|
|
assert!(data.join("key.hex.bak").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn cli_full_network_pipeline() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let relay_port = common::free_port();
|
|
let relay_args = vec![
|
|
"relay".to_string(),
|
|
"--listen".to_string(),
|
|
format!("127.0.0.1:{relay_port}"),
|
|
];
|
|
let _relay = spawn_service(&relay_args, "relay listening");
|
|
|
|
let bob_port = common::free_port();
|
|
let alice_port = common::free_port();
|
|
let bob_config = root.path().join("bob.toml");
|
|
let alice_config = root.path().join("alice.toml");
|
|
let bob_docs = root.path().join("bob-docs");
|
|
let alice_docs = root.path().join("alice-docs");
|
|
fs::create_dir_all(&bob_docs).unwrap();
|
|
fs::create_dir_all(&alice_docs).unwrap();
|
|
fs::write(
|
|
bob_docs.join("tantivy.md"),
|
|
"# Tantivy BM25\nTantivy indexes rust documents.",
|
|
)
|
|
.unwrap();
|
|
fs::write(alice_docs.join("private.txt"), "alice private rust note").unwrap();
|
|
|
|
run_ok(&init_args(
|
|
&bob_config,
|
|
"bob",
|
|
bob_port,
|
|
relay_port,
|
|
&root.path().join("bob-data"),
|
|
));
|
|
run_ok(&init_args(
|
|
&alice_config,
|
|
"alice",
|
|
alice_port,
|
|
relay_port,
|
|
&root.path().join("alice-data"),
|
|
));
|
|
run_ok(&add_args(&bob_config, &bob_docs, "shared", true));
|
|
run_ok(&add_args(&alice_config, &alice_docs, "mine", false));
|
|
|
|
let bob_serve = vec![
|
|
"--config".to_string(),
|
|
bob_config.display().to_string(),
|
|
"serve".to_string(),
|
|
];
|
|
let alice_serve = vec![
|
|
"--config".to_string(),
|
|
alice_config.display().to_string(),
|
|
"serve".to_string(),
|
|
];
|
|
let _bob = spawn_service(&bob_serve, "frxd listening");
|
|
let _alice = spawn_service(&alice_serve, "frxd listening");
|
|
std::thread::sleep(Duration::from_millis(400));
|
|
|
|
let alice_config_arg = alice_config.display().to_string();
|
|
let stdout = run_ok(&[
|
|
"--config".to_string(),
|
|
alice_config_arg.clone(),
|
|
"query".to_string(),
|
|
"tantivy".to_string(),
|
|
]);
|
|
assert!(stdout.contains("Tantivy BM25"), "{stdout}");
|
|
assert!(stdout.contains("remote: 1 response(s)"), "{stdout}");
|
|
|
|
let stdout = run_ok(&[
|
|
"--config".to_string(),
|
|
alice_config_arg.clone(),
|
|
"query".to_string(),
|
|
"rust".to_string(),
|
|
"--local-only".to_string(),
|
|
]);
|
|
assert!(stdout.contains("alice private rust note"), "{stdout}");
|
|
assert!(stdout.contains("remote: 0 response(s)"), "{stdout}");
|
|
|
|
let output = frx()
|
|
.args(["--config", &alice_config_arg, "query", "tantivy"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(output.status.success());
|
|
assert!(String::from_utf8_lossy(&output.stdout).contains("Tantivy BM25"));
|
|
}
|