#![forbid(unsafe_code)] use std::{error::Error, num::NonZeroUsize, path::Path}; use ironstorage::command::{ CliAction, CommandRequest, CompletionShell, EXIT_USAGE, GeneratedPresentation, GitConfigRequest, GitRemoteRequest, GitRequest, HelpTopic, InputPlan, InsertInput, OtpInputSource, OtpRequest, OtpUriPresentation, Presentation, completion_script, help_text, otp_version_text, parse_from, version_text, }; type TestResult = Result<(), Box>; fn request(arguments: &[&str]) -> Result> { let mut complete = vec!["ironstorage"]; complete.extend_from_slice(arguments); match parse_from(complete)? { CliAction::Run(invocation) => Ok(invocation.into_parts().1), CliAction::Display(_) => Err("expected a command request".into()), } } #[test] fn completion_scripts_are_generated_in_process_for_supported_shells() -> TestResult { for (name, shell, marker) in [ ("bash", CompletionShell::Bash, "complete"), ("elvish", CompletionShell::Elvish, "edit:completion"), ("fish", CompletionShell::Fish, "complete"), ( "powershell", CompletionShell::PowerShell, "Register-ArgumentCompleter", ), ("zsh", CompletionShell::Zsh, "compdef"), ] { assert_eq!( request(&["completion", name])?, CommandRequest::Completion { shell } ); let script = String::from_utf8(completion_script(shell))?; assert!(script.contains("ironstorage")); assert!( script.contains(marker), "missing {marker} in {name} completion" ); } assert_eq!( parse_from(["ironstorage", "completion", "unsupported"]) .unwrap_err() .exit_code(), EXIT_USAGE ); Ok(()) } #[test] fn implicit_show_and_list_aliases_are_canonical_requests() -> TestResult { assert!(matches!( request(&[])?, CommandRequest::Show(ref show) if show.entry.is_none() && show.presentation == Presentation::Terminal )); assert!(matches!( request(&["email/personal"] )?, CommandRequest::Show(ref show) if show.entry.as_deref() == Some("email/personal") )); assert_eq!(request(&["ls", "team"])?, request(&["list", "team"])?); assert_eq!( request(&["find", "service"])?, request(&["search", "service"])? ); Ok(()) } #[test] fn mutation_aliases_map_to_the_same_storage_request() -> TestResult { assert_eq!( request(&["insert", "-f", "new/entry"])?, request(&["add", "--force", "new/entry"])? ); assert_eq!( request(&["rm", "-rf", "team/"])?, request(&["remove", "--recursive", "--force", "team/"])? ); assert_eq!(request(&["rm", "entry"])?, request(&["delete", "entry"])?); assert_eq!( request(&["mv", "old", "new"])?, request(&["rename", "old", "new"])? ); assert_eq!( request(&["cp", "old", "new"])?, request(&["copy", "old", "new"])? ); Ok(()) } #[test] fn show_presentation_lines_are_typed_and_conflicts_fail() -> TestResult { assert!(matches!( request(&["show", "--clip=2", "entry"] )?, CommandRequest::Show(ref show) if show.presentation == Presentation::Clipboard { line: NonZeroUsize::new(2).expect("nonzero") } )); assert!(matches!( request(&["show", "-q3", "entry"] )?, CommandRequest::Show(ref show) if show.presentation == Presentation::QrCode { line: NonZeroUsize::new(3).expect("nonzero") } )); for invalid in [ vec!["show", "--clip", "--qrcode", "entry"], vec!["show", "--clip=0", "entry"], vec!["show", "--clip=not-a-number", "entry"], ] { let error = parse_from(std::iter::once("ironstorage").chain(invalid)) .expect_err("invalid presentation"); assert_eq!(error.exit_code(), EXIT_USAGE); } Ok(()) } #[test] fn insert_modes_conflicts_and_noninteractive_input_are_explicit() -> TestResult { let hidden = request(&["insert", "entry"])?; let CommandRequest::Insert(hidden) = hidden else { return Err("expected insert".into()); }; assert_eq!(hidden.input, InsertInput::HiddenConfirmed); assert_eq!(hidden.input_plan(true), InputPlan::HiddenConfirmed); assert_eq!(hidden.input_plan(false), InputPlan::StandardInputLine); let multiline = request(&["insert", "--multiline", "entry"])?; let CommandRequest::Insert(multiline) = multiline else { return Err("expected multiline insert".into()); }; assert_eq!(multiline.input, InsertInput::Multiline); assert_eq!(multiline.input_plan(false), InputPlan::StandardInputToEnd); let echo = request(&["insert", "--echo", "entry"])?; let CommandRequest::Insert(echo) = echo else { return Err("expected echo insert".into()); }; assert_eq!(echo.input_plan(true), InputPlan::EchoedLine); assert_eq!( parse_from(["ironstorage", "insert", "--echo", "--multiline", "entry"]) .expect_err("conflicting insert modes") .exit_code(), EXIT_USAGE ); Ok(()) } #[test] fn generate_options_defaults_and_conflicts_are_validated() -> TestResult { let generated = request(&["generate", "--no-symbols", "--clip", "entry", "32"])?; assert!(matches!( generated, CommandRequest::Generate(ref request) if request.no_symbols && request.length == NonZeroUsize::new(32) && request.presentation == GeneratedPresentation::Clipboard )); for invalid in [ ["generate", "--clip", "--qrcode", "entry"].as_slice(), ["generate", "--force", "--in-place", "entry"].as_slice(), ["generate", "entry", "0"].as_slice(), ] { let error = parse_from(std::iter::once("ironstorage").chain(invalid.iter().copied())) .expect_err("invalid generate request"); assert_eq!(error.exit_code(), EXIT_USAGE); } Ok(()) } #[test] fn grep_support_is_explicit_and_unknown_gnu_options_fail() -> TestResult { assert!(matches!( request(&["grep", "-ivnF", "fixture"] )?, CommandRequest::Grep(ref grep) if grep.ignore_case && grep.invert_match && grep.line_number && grep.fixed_strings )); let error = parse_from(["ironstorage", "grep", "--binary-files=text", "fixture"]) .expect_err("unsupported grep option"); assert_eq!(error.exit_code(), EXIT_USAGE); Ok(()) } #[test] fn git_workflows_are_typed_and_arbitrary_passthrough_is_rejected() -> TestResult { assert_eq!( request(&["git", "init"])?, CommandRequest::Git(GitRequest::Init) ); assert_eq!( request(&["git", "remote"])?, CommandRequest::Git(GitRequest::Remote(GitRemoteRequest::List)) ); assert_eq!( request(&[ "git", "remote", "add", "origin", "https://example.test/store.git", ])?, CommandRequest::Git(GitRequest::Remote(GitRemoteRequest::Add { name: "origin".to_owned(), url: "https://example.test/store.git".to_owned(), })) ); assert_eq!( request(&["git", "config", "--get", "remote.origin.url"])?, CommandRequest::Git(GitRequest::Config(GitConfigRequest::Get { key: "remote.origin.url".to_owned() })) ); assert_eq!( request(&[ "git", "config", "remote.origin.url", "https://example.test/store.git", ])?, CommandRequest::Git(GitRequest::Config(GitConfigRequest::Set { key: "remote.origin.url".to_owned(), value: "https://example.test/store.git".to_owned(), })) ); for command in ["rebase", "cherry-pick", "credential"] { assert_eq!( parse_from(["ironstorage", "git", command]) .expect_err("unsupported Git passthrough") .exit_code(), EXIT_USAGE ); } Ok(()) } #[test] fn otp_default_dispatch_aliases_and_input_contract_are_complete() -> TestResult { let default = request(&["otp", "otp/totp"])?; assert_eq!(default, request(&["otp", "code", "otp/totp"])?); assert_eq!(default, request(&["otp", "show", "otp/totp"])?); assert_eq!( request(&["otp", "insert", "otp/new"])?, request(&["otp", "add", "otp/new"])? ); let inserted = request(&[ "otp", "insert", "--secret", "--issuer", "Issuer", "--account", "account", ])?; let CommandRequest::Otp(OtpRequest::Insert(inserted)) = inserted else { return Err("expected OTP insert".into()); }; assert_eq!( inserted.source, OtpInputSource::Secret { issuer: Some("Issuer".to_owned()), account: Some("account".to_owned()) } ); assert_eq!(inserted.input_plan(true), InputPlan::HiddenConfirmed); assert_eq!(inserted.input_plan(false), InputPlan::StandardInputLine); assert_eq!( parse_from(["ironstorage", "otp", "insert", "--secret"]) .expect_err("secret without identity") .exit_code(), EXIT_USAGE ); assert_eq!( parse_from(["ironstorage", "otp", "insert", "--issuer", "Issuer"]) .expect_err("issuer without secret") .exit_code(), EXIT_USAGE ); Ok(()) } #[test] fn otp_uri_presentation_and_conflicts_are_typed() -> TestResult { assert!(matches!( request(&["otp", "uri", "--qrcode", "otp/totp"] )?, CommandRequest::Otp(OtpRequest::Uri(ref uri)) if uri.presentation == OtpUriPresentation::QrCode )); assert_eq!( parse_from([ "ironstorage", "otp", "uri", "--clip", "--qrcode", "otp/totp", ]) .expect_err("conflicting OTP URI presentation") .exit_code(), EXIT_USAGE ); Ok(()) } #[test] fn configuration_option_and_meta_commands_have_stable_contracts() -> TestResult { let action = parse_from([ "ironstorage", "--config", "relative/config.toml", "show", "entry", ])?; let CliAction::Run(invocation) = action else { return Err("expected invocation".into()); }; assert_eq!(invocation.config(), Some(Path::new("relative/config.toml"))); assert_eq!( request(&["help", "search"])?, CommandRequest::Help { topic: Some(HelpTopic::Find) } ); assert_eq!(request(&["version"])?, CommandRequest::Version); assert!(!help_text(None).is_empty()); assert!(help_text(Some(HelpTopic::Otp)).contains("Usage")); assert!(version_text().starts_with("IronStorage ")); assert_eq!(otp_version_text(), "1.1.1\n"); for option in ["--help", "--version"] { assert!(matches!( parse_from(["ironstorage", option])?, CliAction::Display(ref text) if !text.is_empty() )); } assert!(matches!( parse_from(["ironstorage", "otp", "--help"] )?, CliAction::Display(ref text) if text.contains("Usage") )); assert_eq!( request(&["otp", "--version"])?, CommandRequest::Otp(OtpRequest::Version) ); assert_eq!( parse_from(["ironstorage", "help", "not-a-command"]) .expect_err("unknown help topic") .exit_code(), EXIT_USAGE ); Ok(()) }