Files
IronStorage/crates/storage/tests/read_domains.rs
2026-08-09 22:22:21 +00:00

299 lines
10 KiB
Rust

#![forbid(unsafe_code)]
mod support;
use std::{collections::BTreeMap, fs, num::NonZeroUsize, path::Path};
use ironstorage::{
command::{EXIT_FAILURE, GrepRequest, Presentation, ShowRequest},
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
read::{PresentationChannel, ReadError, ShowOutput, ShowResult, TreeNodeKind, VaultReader},
repository::{DirectoryPath, EntryPath, Repository, RepositoryError, SecretBytes},
};
use support::compatibility::{FixtureSet, TestResult};
struct FixtureSecrets {
values: BTreeMap<String, Vec<u8>>,
}
impl FixtureSecrets {
fn all(fixture: &FixtureSet) -> Self {
Self {
values: fixture
.generated
.keys
.iter()
.map(|key| {
(
key.primary_fingerprint.clone(),
key.passphrase.as_bytes().to_vec(),
)
})
.collect(),
}
}
}
impl SecretProvider for FixtureSecrets {
fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
self.values
.get(key.fingerprint().as_str())
.cloned()
.map(SecretBytes::new)
.ok_or(SecretProviderError::Unavailable)
}
}
#[test]
fn deterministic_root_and_subtree_models_render_without_ciphertext_suffixes() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let reader = VaultReader::new(&repository, &keys);
let root = reader.list(&DirectoryPath::root())?;
assert_eq!(root.title(), "Password Store");
assert_eq!(
root.render_plain(),
concat!(
"Password Store\n",
"├── email\n",
"│ └── personal\n",
"├── otp\n",
"│ ├── hotp\n",
"│ └── totp\n",
"├── shared\n",
"│ └── multiple\n",
"├── team\n",
"│ └── service\n",
"└── unicode\n",
" └── 咖啡\n",
)
);
assert!(!root.render_plain().contains(".gpg"));
assert!(!root.render_plain().contains(".gpg-id"));
let team = reader.list(&DirectoryPath::parse("team")?)?;
assert_eq!(team.title(), "team");
assert_eq!(team.render_plain(), "team\n└── service\n");
assert!(matches!(
reader.list(&DirectoryPath::parse("missing")?),
Err(ReadError::Repository(RepositoryError::NotFound { .. }))
));
Ok(())
}
#[test]
fn explicit_and_implicit_show_dispatch_to_entry_or_directory() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let reader = VaultReader::new(&repository, &keys);
let mut provider = FixtureSecrets::all(&fixture);
let expected = fixture.read("expected/basic/email/personal.txt")?;
match reader.show(Some("email/personal"), &mut provider)? {
ShowResult::Entry(secret) => assert_eq!(secret.expose(), expected),
ShowResult::Directory(_) => panic!("entry dispatched as directory"),
}
match reader.show(Some("team"), &mut provider)? {
ShowResult::Directory(tree) => assert_eq!(tree.render_plain(), "team\n└── service\n"),
ShowResult::Entry(_) => panic!("directory dispatched as entry"),
}
match reader.show(None, &mut provider)? {
ShowResult::Directory(tree) => assert_eq!(tree.title(), "Password Store"),
ShowResult::Entry(_) => panic!("default show did not list root"),
}
assert!(matches!(
reader.show(Some("does-not-exist"), &mut provider),
Err(ReadError::Repository(RepositoryError::NotFound { .. }))
));
Ok(())
}
#[test]
fn typed_clipboard_and_qr_requests_select_lines_without_rendered_state() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let reader = VaultReader::new(&repository, &keys);
let mut provider = FixtureSecrets::all(&fixture);
let clipboard = ShowRequest {
entry: Some("email/personal".to_owned()),
presentation: Presentation::Clipboard {
line: NonZeroUsize::new(1).expect("nonzero"),
},
};
let ShowOutput::Present(clipboard) = reader.execute_show(&clipboard, &mut provider)? else {
panic!("clipboard request was not selected")
};
assert_eq!(clipboard.channel(), PresentationChannel::Clipboard);
assert_eq!(clipboard.line(), 1);
assert_eq!(clipboard.contents().expose(), b"correct horse fixture");
assert!(!format!("{clipboard:?}").contains("correct horse"));
let qr = ShowRequest {
entry: Some("email/personal".to_owned()),
presentation: Presentation::QrCode {
line: NonZeroUsize::new(2).expect("nonzero"),
},
};
let ShowOutput::Present(qr) = reader.execute_show(&qr, &mut provider)? else {
panic!("QR request was not selected")
};
assert_eq!(qr.channel(), PresentationChannel::QrCode);
assert_eq!(qr.contents().expose(), b"login: alice@example.test");
let missing = reader.select_line(&EntryPath::parse("email/personal")?, 99, &mut provider);
assert!(matches!(
&missing,
Err(ReadError::LineNotFound { line: 99, .. })
));
assert_eq!(missing.unwrap_err().exit_code(), EXIT_FAILURE);
Ok(())
}
#[test]
fn find_matches_entry_and_directory_names_case_insensitively() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let reader = VaultReader::new(&repository, &keys);
let results = reader.find(&["service".to_owned(), "咖啡".to_owned()])?;
assert_eq!(
results
.matches()
.iter()
.map(|matched| (matched.path(), matched.kind()))
.collect::<Vec<_>>(),
vec![
("team/service", TreeNodeKind::Entry),
("unicode/咖啡", TreeNodeKind::Entry),
]
);
assert_eq!(
results.tree().render_plain(),
concat!(
"Password Store\n",
"├── team\n",
"│ └── service\n",
"└── unicode\n",
" └── 咖啡\n",
)
);
assert!(
results
.render_plain()
.starts_with("Search Terms: service,咖啡\nPassword Store\n")
);
let personal = reader.find(&["PERSONAL".to_owned()])?;
assert_eq!(personal.matches()[0].path(), "email/personal");
assert!(matches!(
reader.find(&[]),
Err(ReadError::MissingSearchTerms)
));
Ok(())
}
#[test]
fn decrypted_grep_supports_regex_case_inversion_fixed_strings_and_lines() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let reader = VaultReader::new(&repository, &keys);
let mut provider = FixtureSecrets::all(&fixture);
let mut numbered = grep("alice@example\\.test");
numbered.line_number = true;
let basic = reader.grep(&numbered, &mut provider)?;
assert_eq!(basic.entries().len(), 1);
assert_eq!(basic.entries()[0].path().to_string(), "email/personal");
assert_eq!(basic.entries()[0].lines()[0].number(), 2);
assert_eq!(
basic.entries()[0].lines()[0].contents().expose(),
b"login: alice@example.test"
);
assert!(!format!("{basic:?}").contains("alice@example"));
assert_eq!(
basic.render_plain().expose(),
b"email/personal:\n2:login: alice@example.test\n"
);
let mut insensitive = grep("FIXTURE");
insensitive.ignore_case = true;
assert!(reader.grep(&insensitive, &mut provider)?.entries().len() >= 4);
let mut fixed = grep("alice@example.test");
fixed.fixed_strings = true;
assert_eq!(reader.grep(&fixed, &mut provider)?.entries().len(), 1);
let mut inverted = grep("fixture");
inverted.invert_match = true;
let inverted = reader.grep(&inverted, &mut provider)?;
assert!(inverted.entries().iter().any(|entry| {
entry.path().to_string() == "email/personal"
&& entry
.lines()
.iter()
.any(|line| line.contents().expose() == b"login: alice@example.test")
}));
let invalid = grep("[");
assert!(matches!(
reader.grep(&invalid, &mut provider),
Err(ReadError::InvalidRegex)
));
Ok(())
}
#[test]
fn search_excludes_extensions_git_metadata_and_ciphertext_names() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
fs::create_dir_all(store.path().join(".extensions/nested"))?;
fs::copy(
store.path().join("email/personal.gpg"),
store.path().join(".extensions/nested/leaked-secret.gpg"),
)?;
fs::create_dir(store.path().join(".git"))?;
fs::write(store.path().join(".git/secret.gpg"), b"not a packet")?;
fs::write(store.path().join("visible-metadata.txt"), b"fixture")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let reader = VaultReader::new(&repository, &keys);
let mut provider = FixtureSecrets::all(&fixture);
let tree = reader.list(&DirectoryPath::root())?.render_plain();
assert!(!tree.contains("extensions"));
assert!(!tree.contains(".git"));
assert!(!tree.contains("metadata"));
assert!(!tree.contains(".gpg"));
assert!(reader.find(&["leaked".to_owned()])?.matches().is_empty());
assert!(reader.find(&["metadata".to_owned()])?.matches().is_empty());
assert!(
reader
.grep(&grep("correct horse"), &mut provider)?
.entries()
.iter()
.all(|entry| !entry.path().as_path().starts_with(Path::new(".extensions")))
);
Ok(())
}
fn grep(pattern: &str) -> GrepRequest {
GrepRequest {
pattern: pattern.to_owned(),
ignore_case: false,
invert_match: false,
line_number: false,
fixed_strings: false,
}
}