#![forbid(unsafe_code)] mod support; use std::collections::{BTreeMap, BTreeSet}; 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::>(); 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 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::>(); 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(()) }