use codex32::{Codex32String, Fe};
use ms_codec::error::Error;
use ms_codec::{
combine_shares, decode, decode_with_correction, encode, encode_shares, inspect, Payload, Tag,
Threshold,
};
const HRP: &str = "ms";
fn entr_single() -> String {
encode(Tag::ENTR, &Payload::Entr(vec![0xAAu8; 16])).unwrap()
}
#[test]
fn u1_decode_uppercase_equals_lowercase_twin() {
let lower = entr_single();
let upper = lower.to_uppercase();
let (lt, lp) = decode(&lower).expect("lowercase twin must decode");
let (ut, up) = decode(&upper).expect("all-uppercase ms1 must decode (BIP-173 QR form)");
assert_eq!(ut, lt, "tag must match the lowercase twin");
assert_eq!(up, lp, "payload must match the lowercase twin");
}
#[test]
fn u2_inspect_uppercase_report_equals_lowercase_report() {
let lower = encode(
Tag::ENTR,
&Payload::Mnem { language: 1, entropy: vec![0xBBu8; 16] },
)
.unwrap();
let upper = lower.to_uppercase();
let rl = inspect(&lower).expect("lowercase report");
let ru = inspect(&upper).expect("uppercase ms1 must inspect");
assert_eq!(ru.hrp, rl.hrp, "hrp");
assert_eq!(ru.threshold, rl.threshold, "threshold");
assert_eq!(ru.tag, rl.tag, "tag");
assert_eq!(ru.share_index, rl.share_index, "share_index");
assert_eq!(ru.prefix_byte, rl.prefix_byte, "prefix_byte");
assert_eq!(ru.payload_bytes, rl.payload_bytes, "payload_bytes");
assert_eq!(ru.checksum_valid, rl.checksum_valid, "checksum_valid");
assert_eq!(ru.kind, rl.kind, "kind");
assert_eq!(ru.language, rl.language, "language");
}
#[test]
fn u3_uniform_uppercase_share_set_combines() {
let p = Payload::Entr(vec![0xCDu8; 16]);
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 3, &p).unwrap();
let (lt, lp) = combine_shares(&shares[..2]).expect("lowercase set combines");
let upper: Vec<String> = shares[..2].iter().map(|s| s.to_uppercase()).collect();
let (ut, up) = combine_shares(&upper).expect("uniform-uppercase set must combine");
assert_eq!(ut, lt);
assert_eq!(up, lp, "recovered payload must match the lowercase combine");
}
#[test]
fn u3_mixed_set_one_uppercase_share_combines() {
let p = Payload::Entr(vec![0xCDu8; 16]);
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 3, &p).unwrap();
let mixed = vec![shares[0].to_uppercase(), shares[1].clone()];
let (tag, recovered) =
combine_shares(&mixed).expect("one-uppercase-among-lowercase set must combine");
assert_eq!(tag, Tag::ENTR);
assert_eq!(recovered, p, "mixed-case set must recover the exact secret");
}
#[test]
fn u3_guard_uppercase_secret_at_s_is_rejected_not_leaked() {
let mut wire_bytes = vec![0x00u8];
wire_bytes.extend_from_slice(&[0xABu8; 16]);
let secret_s = Codex32String::from_seed(HRP, 2, "tst7", Fe::S, &wire_bytes)
.unwrap()
.to_string()
.to_uppercase();
let filler = vec![0u8; wire_bytes.len()];
let companion = Codex32String::from_seed(HRP, 2, "tst7", Fe::A, &filler)
.unwrap()
.to_string()
.to_uppercase();
let res = combine_shares(&[secret_s, companion]);
assert!(
matches!(res, Err(Error::SecretShareSuppliedToCombine)),
"uppercase secret-at-S must be rejected by the index-s guard, \
not leaked through interpolate_at's short-circuit; got {res:?}"
);
}
#[test]
fn u4_mixed_case_within_one_string_still_rejects() {
let lower = entr_single();
let mut mixed = lower.clone();
mixed.replace_range(0..1, "M");
assert!(
matches!(
decode(&mixed),
Err(Error::Codex32(codex32::Error::InvalidCase(..)))
),
"within-one-string mixed case must stay InvalidCase on decode"
);
let p = Payload::Entr(vec![0xCDu8; 16]);
let shares = encode_shares(Tag::ENTR, Threshold::new(2).unwrap(), 2, &p).unwrap();
let mut bad = shares[0].clone();
bad.replace_range(0..1, "M");
let res = combine_shares(&[bad, shares[1].clone()]);
assert!(
matches!(res, Err(Error::Codex32(codex32::Error::InvalidCase(..)))),
"within-one-string mixed case must stay InvalidCase on combine; got {res:?}"
);
}
#[test]
fn u5_true_wrong_hrp_uppercase_reports_lowercased_hrp() {
let mut wire_bytes = vec![0x00u8];
wire_bytes.extend_from_slice(&[0xAAu8; 16]);
let xs_upper = Codex32String::from_seed("xs", 0, "entr", Fe::S, &wire_bytes)
.unwrap()
.to_string()
.to_uppercase();
match decode(&xs_upper) {
Err(Error::WrongHrp { got }) => {
assert_eq!(got, "xs", "WrongHrp must report the canonicalized HRP");
}
other => panic!("expected WrongHrp {{ got: \"xs\" }}, got {other:?}"),
}
}
#[test]
fn u6_clean_uppercase_decodes_with_empty_corrections() {
let lower = entr_single();
let upper = lower.to_uppercase();
let (lt, lp, ld) = decode_with_correction(&lower).expect("lowercase twin");
assert!(ld.is_empty(), "lowercase clean codeword has no corrections");
let (ut, up, ud) =
decode_with_correction(&upper).expect("pristine uppercase card must decode");
assert_eq!(ut, lt);
assert_eq!(up, lp);
assert!(ud.is_empty(), "pristine uppercase card must report no corrections");
}
#[test]
fn u6_corrupted_uppercase_repairs() {
let lower = entr_single();
let (lt, lp) = decode(&lower).unwrap();
let mut upper = lower.to_uppercase().into_bytes();
upper[10] = if upper[10] == b'Q' { b'P' } else { b'Q' };
let upper = String::from_utf8(upper).unwrap();
let (ut, up, details) =
decode_with_correction(&upper).expect("1-error uppercase card must repair");
assert_eq!(ut, lt);
assert_eq!(up, lp, "repaired payload must match the pristine lowercase twin");
assert_eq!(details.len(), 1, "exactly one correction");
assert_eq!(details[0].position, 10 - 3, "data-part position (post-HRP+separator)");
}