Define configuration and CLI contracts (#2)
This commit is contained in:
328
crates/storage/tests/command_contract.rs
Normal file
328
crates/storage/tests/command_contract.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::{error::Error, num::NonZeroUsize, path::Path};
|
||||
|
||||
use ironstorage::command::{
|
||||
CliAction, CommandRequest, EXIT_USAGE, GeneratedPresentation, GitConfigRequest,
|
||||
GitRemoteRequest, GitRequest, HelpTopic, InputPlan, InsertInput, OtpInputSource, OtpRequest,
|
||||
OtpUriPresentation, Presentation, help_text, otp_version_text, parse_from, version_text,
|
||||
};
|
||||
|
||||
type TestResult = Result<(), Box<dyn Error>>;
|
||||
|
||||
fn request(arguments: &[&str]) -> Result<CommandRequest, Box<dyn Error>> {
|
||||
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 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user