Files
IronStorage/crates/storage/tests/compatibility_fixtures.rs
2026-08-10 01:51:47 +00:00

270 lines
7.9 KiB
Rust

#![forbid(unsafe_code)]
mod support;
use std::collections::{BTreeMap, BTreeSet};
use ironstorage::command::parse_from;
use support::compatibility::{
FixtureSet, TestResult, decrypt_entry_with_passphrase, validate_entry_record,
validate_key_record, validate_recipient_signature, validate_repository,
};
#[test]
fn pins_the_selected_upstream_revisions() -> TestResult {
let fixture = FixtureSet::load()?;
assert_eq!(fixture.upstream.format, 1);
assert_eq!(fixture.upstream.project.len(), 2);
let projects = fixture
.upstream
.project
.iter()
.map(|project| (project.name.as_str(), project))
.collect::<BTreeMap<_, _>>();
let password_store = projects["password-store"];
assert_eq!(password_store.version, "1.7.4");
assert_eq!(password_store.reported_version, "1.7.4");
assert_eq!(
password_store.commit,
"1078f2514d579178d5df7042c6a790e9c9b731ad"
);
let pass_otp = projects["pass-otp"];
assert_eq!(pass_otp.version, "1.2.0");
assert_eq!(pass_otp.reported_version, "1.1.1");
assert_eq!(pass_otp.commit, "1e9d10ca75ae1a8672a7f192809713463657778e");
for project in projects.values() {
assert_eq!(project.commit.len(), 40);
assert!(project.commit.bytes().all(|byte| byte.is_ascii_hexdigit()));
assert!(project.source.starts_with("https://"));
assert!(!project.behavior_source.is_empty());
assert!(project.license.starts_with("GPL-"));
}
Ok(())
}
#[test]
fn behavior_catalog_covers_the_milestone_contract() -> TestResult {
let fixture = FixtureSet::load()?;
assert_eq!(fixture.behavior.format, 1);
assert!(fixture.behavior.cases.len() >= 65);
let mut ids = BTreeSet::new();
let mut commands = BTreeSet::new();
let mut flags = BTreeSet::new();
let mut areas = BTreeSet::new();
let mut has_success = false;
let mut has_failure = false;
for case in &fixture.behavior.cases {
assert!(
ids.insert(case.id.as_str()),
"duplicate case ID {}",
case.id
);
assert!(
matches!(case.status, 0 | 1),
"invalid status in {}",
case.id
);
has_success |= case.status == 0;
has_failure |= case.status != 0;
areas.insert(case.area.as_str());
if let Some(command) = case.argv.first() {
commands.insert(command.as_str());
}
for argument in &case.argv {
if argument.starts_with('-') {
let flag = argument.split('=').next().unwrap_or(argument);
flags.insert(flag);
}
}
if case.status != 0 {
assert!(case.recipients.is_empty(), "failed case has recipients");
assert!(case.commit.is_empty(), "failed case has a commit");
}
assert!(!case.outcome.is_empty());
assert!(!case.stdin.is_empty());
}
assert!(has_success && has_failure);
for area in [
"init", "show", "list", "find", "grep", "insert", "edit", "generate", "remove", "move",
"copy", "git", "otp", "meta",
] {
assert!(areas.contains(area), "missing behavior area {area}");
}
for command in [
"init",
"show",
"ls",
"list",
"find",
"search",
"grep",
"insert",
"add",
"edit",
"generate",
"rm",
"remove",
"delete",
"mv",
"rename",
"cp",
"copy",
"git",
"otp",
"help",
"version",
"--help",
"--version",
] {
assert!(
commands.contains(command),
"missing command or alias {command}"
);
}
for flag in [
"-p",
"--path",
"-c",
"--clip",
"-q2",
"--qrcode",
"-e",
"--echo",
"-m",
"--multiline",
"-f",
"--force",
"-n",
"--no-symbols",
"-i",
"--in-place",
"-r",
"--recursive",
"-s",
"--secret",
"-a",
"--account",
"--issuer",
] {
assert!(flags.contains(flag), "missing option spelling {flag}");
}
Ok(())
}
#[test]
fn every_compatibility_case_reaches_the_typed_command_contract() -> TestResult {
let fixture = FixtureSet::load()?;
for case in &fixture.behavior.cases {
let arguments = std::iter::once("ironstorage").chain(case.argv.iter().map(String::as_str));
let parsed = parse_from(arguments);
let parser_rejection = case.outcome.contains("usage-error")
|| case.outcome.contains("unsupported-option")
|| matches!(
case.id.as_str(),
"generate-zero" | "git-unsupported-passthrough"
);
assert_eq!(
parsed.is_err(),
parser_rejection,
"unexpected command-contract result for {}: {:?}",
case.id,
parsed
);
if let Err(error) = parsed {
assert_eq!(
error.exit_code(),
case.status as u8,
"status for {}",
case.id
);
}
}
Ok(())
}
#[test]
fn generated_openpgp_fixtures_are_self_consistent() -> TestResult {
let fixture = FixtureSet::load()?;
assert_eq!(fixture.generated.format, 1);
assert_eq!(fixture.generated.keys.len(), 2);
assert!(fixture.generated.entries.len() >= 8);
for key in &fixture.generated.keys {
validate_key_record(&fixture, key)?;
}
for entry in &fixture.generated.entries {
validate_entry_record(&fixture, entry)?;
}
let bob_entry = fixture
.generated
.entries
.iter()
.find(|entry| entry.store == "basic" && entry.path == "team/service.gpg")
.expect("Bob-only entry");
let bob = fixture.key("bob")?;
assert!(
decrypt_entry_with_passphrase(&fixture, bob_entry, bob, "fixture-alice-passphrase",)
.is_err(),
"the encrypted secret-key passphrase must be required"
);
validate_recipient_signature(&fixture, "basic", "alice")?;
validate_recipient_signature(&fixture, "basic/team", "bob")?;
Ok(())
}
#[test]
fn generated_git_repositories_have_valid_objects_and_refs() -> TestResult {
let fixture = FixtureSet::load()?;
assert_eq!(fixture.generated.repositories.len(), 3);
for repository in &fixture.generated.repositories {
validate_repository(&fixture, repository)?;
}
Ok(())
}
#[test]
fn fixtures_materialize_without_touching_a_user_store() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
assert!(store.path().join(".gpg-id").is_file());
assert!(store.path().join("email/personal.gpg").is_file());
assert!(store.path().join("team/.gpg-id").is_file());
let repository = fixture.materialize_repository("basic")?;
assert!(repository.path().join("HEAD").is_file());
assert!(repository.path().join("objects").is_dir());
assert!(repository.path().join("refs/heads/main").is_file());
Ok(())
}
#[test]
fn hostile_and_interruption_recipes_are_present() -> TestResult {
let fixture = FixtureSet::load()?;
let nodes = fixture
.layouts
.get("synthetic_node")
.and_then(toml::Value::as_array)
.expect("synthetic_node array");
assert!(nodes.len() >= 4);
let outcomes = nodes
.iter()
.filter_map(|node| node.get("expected"))
.filter_map(toml::Value::as_str)
.collect::<BTreeSet<_>>();
assert!(outcomes.contains("reject-symlink-escape"));
assert!(outcomes.contains("reject-unsupported-file-type"));
assert!(outcomes.contains("require-trailing-slash-disambiguation"));
let interruptions = fixture
.layouts
.get("interruption")
.and_then(toml::Value::as_array)
.expect("interruption array");
assert_eq!(interruptions.len(), 3);
Ok(())
}