use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
const ENGLISH_12: &str =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
fn japanese_12_word() -> String {
bip39::Mnemonic::from_entropy_in(bip39::Language::Japanese, &[0xABu8; 16])
.expect("valid entropy length")
.to_string()
}
fn split_shares(source_args: &[&str], k: &str, n: &str) -> Vec<String> {
let mut args = vec!["split"];
args.extend_from_slice(source_args);
args.extend_from_slice(&["-k", k, "-n", n, "--json"]);
let out = Command::cargo_bin("ms")
.unwrap()
.args(&args)
.assert()
.success()
.get_output()
.stdout
.clone();
let v: Value = serde_json::from_slice(&out).unwrap();
v["shares"]
.as_array()
.unwrap()
.iter()
.map(|s| s.as_str().unwrap().to_string())
.collect()
}
fn comma5(s: &str) -> String {
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if i > 0 && i % 5 == 0 {
out.push(',');
}
out.push(c);
}
out
}
#[test]
fn combine_accepts_comma_grouped_positional_shares() {
let shares = split_shares(&["--phrase", ENGLISH_12], "2", "3");
let g0 = comma5(&shares[0]);
let g2 = comma5(&shares[2]);
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &g0, &g2])
.assert()
.success()
.stdout(predicate::str::contains(ENGLISH_12));
}
#[test]
fn combine_dash_stdin_round_trips() {
let shares = split_shares(&["--phrase", ENGLISH_12], "2", "3");
let stdin = format!("{}\n{}\n", comma5(&shares[0]), comma5(&shares[1]));
Command::cargo_bin("ms")
.unwrap()
.args(["combine", "-"])
.write_stdin(stdin)
.assert()
.success()
.stdout(predicate::str::contains(ENGLISH_12));
}
#[test]
fn combine_english_round_trip_to_phrase() {
let shares = split_shares(&["--phrase", ENGLISH_12], "2", "3");
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[0], &shares[2]])
.assert()
.success()
.stdout(predicate::str::contains(ENGLISH_12));
}
#[test]
fn combine_japanese_round_trip_preserves_language() {
let ja = japanese_12_word();
let shares = split_shares(&["--language", "japanese", "--phrase", &ja], "2", "3");
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[1], &shares[2]])
.assert()
.success()
.stdout(predicate::str::contains(&ja))
.stdout(predicate::str::contains("japanese"));
}
#[test]
fn combine_to_entropy_emits_hex() {
let shares = split_shares(&["--hex", &"ab".repeat(16)], "2", "3");
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[0], &shares[1], "--to", "entropy"])
.assert()
.success()
.stdout(predicate::str::contains("abababababababababababababababab"));
}
#[test]
fn combine_to_ms1_emits_single_string_that_decodes() {
let shares = split_shares(&["--phrase", ENGLISH_12], "2", "3");
let out = Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[0], &shares[1], "--to", "ms1"])
.assert()
.success()
.get_output()
.stdout
.clone();
let s = String::from_utf8(out).unwrap();
let ms1 = s
.lines()
.find(|l| l.starts_with("ms1") && !l.contains(' '))
.expect("an ms1 line");
Command::cargo_bin("ms")
.unwrap()
.args(["decode", ms1])
.assert()
.success()
.stdout(predicate::str::contains(ENGLISH_12));
}
#[test]
fn combine_json_shape_entr() {
let shares = split_shares(&["--hex", &"ab".repeat(16)], "2", "3");
let out = Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[0], &shares[1], "--json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: Value = serde_json::from_slice(&out).unwrap();
assert_eq!(v["schema_version"], "1");
assert_eq!(v["kind"], "entr");
assert_eq!(v["entropy_hex"], "abababababababababababababababab");
assert!(v["phrase"].is_string());
assert_eq!(v["language"], "english");
}
#[test]
fn combine_below_threshold_friendly_error() {
let shares = split_shares(&["--phrase", ENGLISH_12], "3", "4");
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[0], &shares[1]])
.assert()
.failure()
.stderr(predicate::str::contains("not enough shares"));
}
#[test]
fn combine_secret_share_index_s_rejected() {
let shares = split_shares(&["--phrase", ENGLISH_12], "2", "3");
let (_, _, id) = {
let sh = &shares[0];
let sep = sh.rfind('1').unwrap();
(
sh.as_bytes()[sep + 1] as char,
sh.as_bytes()[sep + 6] as char,
sh[sep + 2..sep + 6].to_string(),
)
};
let mut data = vec![0x00u8];
data.extend_from_slice(&[0x00u8; 16]);
let secret_s =
ms_codec::codex32::Codex32String::from_seed("ms", 2, &id, ms_codec::codex32::Fe::S, &data)
.unwrap()
.to_string();
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &secret_s, &shares[0]])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("secret share"));
}
#[test]
fn combine_duplicate_index_rejected() {
let shares = split_shares(&["--phrase", ENGLISH_12], "2", "3");
Command::cargo_bin("ms")
.unwrap()
.args(["combine", &shares[0], &shares[0]])
.assert()
.failure()
.stderr(predicate::str::contains("repeated"));
}