270 lines
7.9 KiB
Rust
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(())
|
|
}
|