use ratdb_core::{file::FileStorage, Config, Database}; use ratdb_server::{ auth::{credential_path, Credentials}, transport::Server, }; use serde_json::Value; use std::{ io::Write, net::SocketAddr, path::Path, process::{Command, Output, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, thread, }; fn with_server(path: &Path, run: impl FnOnce(SocketAddr)) { let server = Server::bind(path, "127.0.0.1:0".parse().unwrap(), 8).unwrap(); let address = server.local_addr().unwrap(); let stop = Arc::new(AtomicBool::new(false)); let flag = stop.clone(); let worker = thread::spawn(move || server.run(flag).unwrap()); run(address); stop.store(true, Ordering::Relaxed); worker.join().unwrap(); } fn cli(address: SocketAddr, mode: Option<&str>, input: &str) -> Output { let mut command = Command::new(env!("CARGO_BIN_EXE_ratdb")); if let Some(mode) = mode { command.arg(mode); } let mut child = command .env("RATDB_ADDR", address.to_string()) .env("RATDB_USER", "admin") .env("RATDB_PASSWORD", "sample password for tests") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); child .stdin .take() .unwrap() .write_all(input.as_bytes()) .unwrap(); child.wait_with_output().unwrap() } fn batch(address: SocketAddr, input: &str) -> Vec { let output = cli(address, Some("batch"), input); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout) .unwrap() .lines() .map(|s| serde_json::from_str::(s).unwrap()["data"].clone()) .collect() } #[test] fn check_and_backup_reject_semantic_damage() { let path = std::env::temp_dir().join(format!("ratdb-invalid-doc-{}.db", std::process::id())); let backup = path.with_extension("backup"); let mut db = Database::open(FileStorage::open(&path).unwrap(), Config::default()).unwrap(); db.put(b"users\0alice", br#"{"id":"bob"}"#).unwrap(); drop(db); let binary = env!("CARGO_BIN_EXE_ratdb"); let check = Command::new(binary) .arg("check") .arg(&path) .output() .unwrap(); assert!(!check.status.success()); for operation in ["backup", "restore"] { assert!(!Command::new(binary) .arg(operation) .arg(&path) .arg(&backup) .output() .unwrap() .status .success()); assert!(!backup.exists()); } Credentials::new("admin", "sample password for tests") .unwrap() .save_new(&credential_path(&path)) .unwrap(); with_server(&path, |addr| { let response = cli(addr, Some("batch"), "check\n"); assert!(!response.status.success()); assert_eq!( serde_json::from_slice::(&response.stdout).unwrap()["error"]["code"], "operation_failed" ); }); std::fs::remove_file(credential_path(&path)).unwrap(); std::fs::remove_file(path).unwrap(); } #[test] fn crm_batch_restart_and_reconnect() { let path = std::env::temp_dir().join(format!("ratdb-crm-{}.db", std::process::id())); drop(Database::open(FileStorage::open(&path).unwrap(), Config::default()).unwrap()); Credentials::new("admin", "sample password for tests") .unwrap() .save_new(&credential_path(&path)) .unwrap(); with_server(&path, |addr| { let rows = batch( addr, r#"use crm/companies insert co-1 {"name":"Sample Co"} use contacts insert c-1 {"name":"Jo","company":"co-1"} update c-1 {"stage":"active"} find stage=active use opportunities insert opp-1 {"contact":"c-1","value":300} use notes insert n-1 {"contact":"c-1","text":"Follow up"} use activities insert a-1 {"contact":"c-1","done":false} delete a-1 create warren empty use /empty create den incoming "#, ); assert_eq!(rows[5]["documents"][0]["name"], "Jo"); let result = cli( addr, Some("batch"), "use crm/contacts\ninsert c-1 {}\ndelete c-1\n", ); assert!(!result.status.success()); let lines = String::from_utf8(result.stdout).unwrap(); assert_eq!(lines.lines().count(), 2); assert_eq!( serde_json::from_str::(lines.lines().nth(1).unwrap()).unwrap()["ok"], false ); }); with_server(&path, |addr| { let rows = batch( addr, "use crm/contacts\npage 10\nuse notes\nget n-1\nuse activities\nget a-1\n", ); assert_eq!(rows[1]["documents"][0]["stage"], "active"); assert_eq!(rows[3]["contact"], "c-1"); assert!(rows[5].is_null()); let refused = cli(addr, Some("batch"), "drop-empty warren empty\n"); assert!(!refused.status.success()); let rows = batch(addr,"use empty\nshow dens\nuse incoming\ndrop-empty den incoming\ndrop-empty warren empty\nshow warrens\n"); assert_eq!(rows[1]["names"], serde_json::json!(["incoming"])); assert_eq!(rows[3]["warren"], "empty"); assert!(rows[3]["den"].is_null()); assert!(rows[4]["warren"].is_null()); assert_eq!(rows[5]["names"], serde_json::json!(["crm"])); let output = cli( addr, None, "use crm/contacts\nreconnect\nuse crm/notes\nget n-1\nexit\n", ); assert!(output.status.success()); let text = String::from_utf8(output.stdout).unwrap(); assert!(text.contains("Reconnected") && text.contains("Follow up")); }); std::fs::remove_file(credential_path(&path)).unwrap(); std::fs::remove_file(path).unwrap(); }