use crate::Result;
use crate::export::{items, uri_of};
use crate::vault::Vault;
pub fn write(vault: &Vault) -> Result<String> {
let mut out = String::new();
for item in items(vault) {
out.push_str(&uri_of(item.entry)?);
out.push('\n');
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::import::{ImportFormat, import};
use crate::vault::Entry;
fn entry(uri: &str, now: u64) -> Entry {
let parsed = crate::uri::parse(uri).expect("the uri is valid");
crate::vault::entry_from_parsed_uri(parsed, now).expect("the entry is valid")
}
const URIS: [&str; 5] = [
"otpauth://totp/GitHub:alice?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&period=60",
"otpauth://hotp/Bank:bob?secret=JBSWY3DPEHPK3PXP&issuer=Bank&counter=7&digits=8",
"otpauth://steam/alice?secret=JBSWY3DPEHPK3PXP",
"motp:/Example:alice?secret=deadbeef&issuer=Example&pin=GEZDGNBVGY3Q",
"otpauth://yaotp/alice?secret=LA2V6KMCGYMWWVEW64RNP3JA3I&pin=GEZDGNBVGY3Q",
];
fn vault() -> (Vault, Vec<Entry>) {
let mut vault = Vault::new();
let entries: Vec<Entry> = URIS.iter().map(|uri| entry(uri, 1_600_000_000)).collect();
for built in &entries {
vault
.insert(built.duplicate().expect("a copy is possible"))
.expect("the vault accepts it");
}
(vault, entries)
}
#[test]
fn one_uri_per_entry_in_the_vaults_order() {
let (vault, originals) = vault();
let text = write(&vault).expect("the export is written");
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), URIS.len());
assert!(text.ends_with('\n'), "every line ends with a newline");
for (index, original) in originals.iter().enumerate() {
let line = index + 1;
let parsed = crate::uri::parse(lines[index])
.unwrap_or_else(|error| panic!("line {line} must parse: {error}"));
assert_eq!(parsed.kind, original.kind, "line {line}");
assert_eq!(parsed.issuer, original.issuer, "line {line}");
assert_eq!(parsed.account, original.account, "line {line}");
assert_eq!(parsed.algorithm, original.algorithm, "line {line}");
assert_eq!(parsed.digits, original.digits, "line {line}");
assert_eq!(
parsed.secret.to_base32(),
original.secret.to_base32(),
"line {line}"
);
}
}
#[test]
fn an_export_round_trips_through_the_reader() {
let (vault, originals) = vault();
let text = write(&vault).expect("the export is written");
let report = import(text.as_bytes(), ImportFormat::Text, 1_700_000_000, None)
.unwrap_or_else(|error| panic!("the export must read back: {error}"));
assert!(report.problems.is_empty(), "actual {:?}", report.problems);
assert_eq!(report.entries.len(), originals.len());
for (index, original) in originals.iter().enumerate() {
let read = &report.entries[index].entry;
assert_eq!(read.kind, original.kind, "line {}", index + 1);
assert_eq!(read.issuer, original.issuer, "line {}", index + 1);
assert_eq!(read.algorithm, original.algorithm, "line {}", index + 1);
assert_eq!(read.digits, original.digits, "line {}", index + 1);
assert_eq!(
read.secret.to_base32(),
original.secret.to_base32(),
"line {}",
index + 1
);
}
}
#[test]
fn an_motp_line_uses_the_hexadecimal_form() {
let mut vault = Vault::new();
vault.insert(entry(URIS[3], 0)).expect("the vault is empty");
let text = write(&vault).expect("the export is written");
assert!(text.starts_with("motp:/Example:alice?"), "actual {text}");
assert!(text.contains("secret=deadbeef"), "actual {text}");
assert!(
!text.contains("otpauth"),
"mOTP is not written in the otpauth form: {text}"
);
}
#[test]
fn an_account_a_uri_cannot_carry_is_refused() {
let mut vault = Vault::new();
let mut built = entry(URIS[0], 0);
built.account = "a:b".to_owned();
vault.insert(built).expect("the vault is empty");
let result = write(&vault);
assert!(
matches!(result, Err(crate::Error::UriAccountContainsColon { .. })),
"actual {result:?}"
);
}
#[test]
fn the_label_is_the_account_with_the_name_as_fallback() {
let mut vault = Vault::new();
let mut without_account = entry(URIS[0], 0);
without_account.name = "GitHub".to_owned();
without_account.account = String::new();
without_account.issuer = Some("GitHub".to_owned());
vault.insert(without_account).expect("the vault is empty");
let text = write(&vault).expect("the export is written");
assert_eq!(
text.trim(),
"otpauth://totp/GitHub:GitHub?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&period=60",
"an entry with no account is labelled with its name"
);
}
#[test]
fn an_empty_vault_writes_an_empty_file() {
let vault = Vault::new();
assert_eq!(write(&vault).expect("nothing to write"), "");
}
}