use std::io::Write;
use std::process::{Command, Stdio};
use tempfile::TempDir;
struct TestContext {
_dir: TempDir,
seed_file: std::path::PathBuf,
_config_dir: std::path::PathBuf,
}
impl TestContext {
fn new() -> Self {
let _dir = TempDir::new().unwrap();
let seed_file = _dir.path().join("seed");
std::fs::write(&seed_file, b"test seed phrase for integration tests").unwrap();
let seed_file = seed_file.canonicalize().unwrap();
let config_dir = _dir.path().join("config");
std::fs::create_dir_all(&config_dir).unwrap();
let _config_dir = config_dir.canonicalize().unwrap();
TestContext {
_dir,
seed_file,
_config_dir,
}
}
fn cmd(&self) -> Command {
self.cmd_realm("default")
}
fn cmd_realm(&self, realm: &str) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_1seed"));
cmd.env("ONESEED_TEST_MODE", "1");
cmd.env("SEED_FILE", &self.seed_file);
cmd.arg("--realm");
cmd.arg(realm);
cmd
}
}
#[test]
fn pub_deterministic() {
let ctx = TestContext::new();
let out1 = ctx.cmd().args(["age", "pub"]).output().unwrap();
let out2 = ctx.cmd().args(["age", "pub"]).output().unwrap();
assert!(out1.status.success());
assert_eq!(out1.stdout, out2.stdout);
assert!(String::from_utf8_lossy(&out1.stdout).starts_with("age1"));
}
#[test]
fn different_realms_different_keys() {
let ctx = TestContext::new();
let out1 = ctx
.cmd_realm("realm1")
.args(["age", "pub"])
.output()
.unwrap();
let out2 = ctx
.cmd_realm("realm2")
.args(["age", "pub"])
.output()
.unwrap();
assert!(out1.status.success());
assert!(out2.status.success());
assert_ne!(out1.stdout, out2.stdout);
}
#[test]
fn ssh_pub_format() {
let ctx = TestContext::new();
let out = ctx.cmd().args(["ssh", "pub"]).output().unwrap();
assert!(out.status.success());
let key = String::from_utf8_lossy(&out.stdout);
assert!(key.starts_with("ssh-ed25519 "));
assert!(key.contains("1seed:"));
}
#[test]
fn encrypt_decrypt_roundtrip() {
let ctx = TestContext::new();
let plaintext = b"hello world";
let mut enc = ctx
.cmd()
.args(["age", "encrypt", "-a"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
enc.stdin.as_mut().unwrap().write_all(plaintext).unwrap();
let enc_out = enc.wait_with_output().unwrap();
assert!(enc_out.status.success());
let ciphertext = enc_out.stdout;
assert!(String::from_utf8_lossy(&ciphertext).contains("-----BEGIN AGE ENCRYPTED FILE-----"));
let mut dec = ctx
.cmd()
.args(["age", "decrypt"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
dec.stdin.as_mut().unwrap().write_all(&ciphertext).unwrap();
let dec_out = dec.wait_with_output().unwrap();
assert!(dec_out.status.success());
assert_eq!(dec_out.stdout, plaintext);
}
#[test]
fn password_deterministic() {
let ctx = TestContext::new();
let out1 = ctx
.cmd()
.args(["derive", "password", "github.com"])
.output()
.unwrap();
let out2 = ctx
.cmd()
.args(["derive", "password", "github.com"])
.output()
.unwrap();
assert!(out1.status.success());
assert_eq!(out1.stdout, out2.stdout);
assert_eq!(out1.stdout.len(), 16);
}
#[test]
fn password_counter_changes_output() {
let ctx = TestContext::new();
let out1 = ctx
.cmd()
.args(["derive", "password", "site", "-n", "1"])
.output()
.unwrap();
let out2 = ctx
.cmd()
.args(["derive", "password", "site", "-n", "2"])
.output()
.unwrap();
assert!(out1.status.success());
assert!(out2.status.success());
assert_ne!(out1.stdout, out2.stdout);
}
#[test]
fn password_length() {
let ctx = TestContext::new();
let out = ctx
.cmd()
.args(["derive", "password", "site", "-l", "32"])
.output()
.unwrap();
assert!(out.status.success());
assert_eq!(out.stdout.len(), 32);
}
#[test]
fn sign_verify_roundtrip() {
let ctx = TestContext::new();
let data = b"data to sign";
let mut sign = ctx
.cmd()
.args(["sign", "data"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
sign.stdin.as_mut().unwrap().write_all(data).unwrap();
let sign_out = sign.wait_with_output().unwrap();
assert!(sign_out.status.success());
let sig = String::from_utf8(sign_out.stdout)
.unwrap()
.trim()
.to_string();
let mut verify = ctx
.cmd()
.args(["sign", "verify", &sig])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
verify.stdin.as_mut().unwrap().write_all(data).unwrap();
let verify_out = verify.wait_with_output().unwrap();
assert!(verify_out.status.success());
}
#[test]
fn verify_fails_wrong_data() {
let ctx = TestContext::new();
let mut sign = ctx
.cmd()
.args(["sign", "data"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
sign.stdin.as_mut().unwrap().write_all(b"original").unwrap();
let sign_out = sign.wait_with_output().unwrap();
let sig = String::from_utf8(sign_out.stdout)
.unwrap()
.trim()
.to_string();
let mut verify = ctx
.cmd()
.args(["sign", "verify", &sig])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
verify
.stdin
.as_mut()
.unwrap()
.write_all(b"tampered")
.unwrap();
let verify_out = verify.wait_with_output().unwrap();
assert!(!verify_out.status.success());
}
#[test]
fn raw_hex_output() {
let ctx = TestContext::new();
let out = ctx
.cmd()
.args(["derive", "raw", "test", "-l", "16"])
.output()
.unwrap();
assert!(out.status.success());
let hex = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert_eq!(hex.len(), 32); assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn raw_base64_output() {
let ctx = TestContext::new();
let out = ctx
.cmd()
.args(["derive", "raw", "test", "-l", "32", "--base64"])
.output()
.unwrap();
assert!(out.status.success());
let b64 = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert_eq!(b64.len(), 44); }
#[test]
fn mnemonic_word_counts() {
let ctx = TestContext::new();
for words in [12, 15, 18, 21, 24] {
let out = ctx
.cmd()
.args(["derive", "mnemonic", "-w", &words.to_string()])
.output()
.unwrap();
assert!(out.status.success());
let mnemonic = String::from_utf8_lossy(&out.stdout);
let count = mnemonic.trim().split_whitespace().count();
assert_eq!(count, words, "expected {words} words, got {count}");
}
}
#[test]
fn derive_integer_range() {
let ctx = TestContext::new();
let out = ctx
.cmd()
.args(["derive", "int", "test", "--min", "10", "--max", "20"])
.output()
.unwrap();
assert!(out.status.success());
let val: i64 = String::from_utf8_lossy(&out.stdout).trim().parse().unwrap();
assert!(val >= 10 && val <= 20);
}
#[test]
fn derive_uuid_format() {
let ctx = TestContext::new();
let out = ctx.cmd().args(["derive", "uuid", "test"]).output().unwrap();
assert!(out.status.success());
let uuid = String::from_utf8_lossy(&out.stdout).trim().to_string();
assert_eq!(uuid.len(), 36);
let parts: Vec<&str> = uuid.split('-').collect();
assert_eq!(parts.len(), 5);
}