Adds some basic test cases & infrastructure for testing pijul identity
. Hopefully the testing infrastructure can be adapted with (relative) ease to support other commands, not just pijul identity
.
FOCBVLOUXYA7ZCUZA2CU3JU2QGF3ZOXW6EAVL5KZINN43GXNL7CQC
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![warn(clippy::cargo)]
mod common;
use anyhow::Error;
use common::identity::{default, prompt, Identity, SubCommand};
use common::{Interaction, InteractionType, SecondAttempt};
fn default_id_name() -> Interaction {
Interaction::new(
prompt::ID_NAME,
InteractionType::Input(default::ID_NAME.to_string()),
)
}
#[test]
fn new_minimal() -> Result<(), Error> {
let identity = Identity::new(
"new_minimal",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn new_full() -> Result<(), Error> {
let identity = Identity::new(
"new_full",
default_id_name(),
Some(default::FULL_NAME.to_string()),
Some(Interaction::new(
prompt::EMAIL,
InteractionType::Input(default::EMAIL.to_string()),
)),
Some(Interaction::new(
prompt::EXPIRY_DATE,
InteractionType::Input(default::EXPIRY.to_string()),
)),
Some(Interaction::new(
prompt::LOGIN,
InteractionType::Input(default::LOGIN.to_string()),
)),
Some(Interaction::new(
prompt::ORIGIN,
InteractionType::Input(default::ORIGIN.to_string()),
)),
Some(Interaction::new(
prompt::PASSWORD,
InteractionType::Password {
input: default::PASSWORD.to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
)),
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn new_email() -> Result<(), Error> {
let identity = Identity::new(
"new_email",
default_id_name(),
None,
Some(
Interaction::new(
prompt::EMAIL,
InteractionType::Input(String::from("BAD-EMAIL")),
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Input(default::EMAIL.to_string()),
"Invalid email address",
)?)?,
),
None,
None,
None,
None,
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn new_expiry() -> Result<(), Error> {
let identity = Identity::new(
"new_expiry",
default_id_name(),
None,
None,
Some(
Interaction::new(
prompt::EXPIRY_DATE,
InteractionType::Input(String::from("BAD-EXPIRY")),
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Input(default::EXPIRY.to_string()),
"Invalid date",
)?)?,
),
None,
None,
None,
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn new_login() -> Result<(), Error> {
let identity = Identity::new(
"new_login",
default_id_name(),
None,
None,
None,
Some(Interaction::new(
prompt::LOGIN,
InteractionType::Input(default::LOGIN.to_string()),
)),
None,
None,
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn new_origin() -> Result<(), Error> {
let identity = Identity::new(
"new_origin",
default_id_name(),
None,
None,
None,
None,
Some(Interaction::new(
prompt::ORIGIN,
InteractionType::Input(default::ORIGIN.to_string()),
)),
None,
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn new_password() -> Result<(), Error> {
let identity = Identity::new(
"new_password",
default_id_name(),
None,
None,
None,
None,
None,
Some(
Interaction::new(
prompt::PASSWORD,
InteractionType::Password {
input: default::PASSWORD.to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Password {
input: "Good-Password".to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
"Password mismatch",
)?)?,
),
)?;
identity.run(&SubCommand::New, Vec::new())?;
Ok(())
}
#[test]
fn edit_full() -> Result<(), Error> {
let old_identity = Identity::new(
"edit_full",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
let new_identity = Identity::new(
"edit_full",
Interaction::new(
prompt::ID_NAME,
InteractionType::Input(String::from("new_id_name")),
),
Some(default::FULL_NAME.to_string()),
Some(
Interaction::new(
prompt::EMAIL,
InteractionType::Input(String::from("BAD_EMAIL")),
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Input(default::EMAIL.to_string()),
"Invalid email address",
)?)?,
),
Some(
Interaction::new(
prompt::EXPIRY_DATE,
InteractionType::Input(String::from("BAD-EXPIRY")),
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Input(default::EXPIRY.to_string()),
"Invalid date",
)?)?,
),
Some(Interaction::new(
prompt::LOGIN,
InteractionType::Input(default::LOGIN.to_string()),
)),
Some(Interaction::new(
prompt::ORIGIN,
InteractionType::Input(default::ORIGIN.to_string()),
)),
Some(
Interaction::new(
prompt::PASSWORD,
InteractionType::Password {
input: default::PASSWORD.to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Password {
input: "Good-Password".to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
"Password mismatch",
)?)?,
),
)?;
new_identity.run(
&SubCommand::Edit(old_identity.id_name.valid_input().as_string()),
vec![old_identity],
)?;
Ok(())
}
#[test]
fn edit_id_name() -> Result<(), Error> {
let old_identity = Identity::new(
"edit_id_name",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
let new_identity = Identity::new(
"edit_id_name",
Interaction::new(
prompt::ID_NAME,
InteractionType::Input(String::from("new_id_name")),
),
None,
None,
None,
None,
None,
None,
)?;
new_identity.run(
&SubCommand::Edit(old_identity.id_name.valid_input().as_string()),
vec![old_identity],
)?;
Ok(())
}
#[test]
fn edit_email() -> Result<(), Error> {
let old_identity = Identity::new(
"edit_email",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
let new_identity = Identity::new(
"edit_email",
default_id_name(),
None,
Some(
Interaction::new(
prompt::EMAIL,
InteractionType::Input(String::from("BAD_EMAIL")),
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Input(default::EMAIL.to_string()),
"Invalid email address",
)?)?,
),
None,
None,
None,
None,
)?;
new_identity.run(
&SubCommand::Edit(old_identity.id_name.valid_input().as_string()),
vec![old_identity],
)?;
Ok(())
}
#[test]
fn edit_expiry() -> Result<(), Error> {
let old_identity = Identity::new(
"edit_expiry",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
let new_identity = Identity::new(
"edit_expiry",
default_id_name(),
None,
None,
Some(
Interaction::new(
prompt::EXPIRY_DATE,
InteractionType::Input(String::from("BAD-EXPIRY")),
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Input(default::EXPIRY.to_string()),
"Invalid date",
)?)?,
),
None,
None,
None,
)?;
new_identity.run(
&SubCommand::Edit(old_identity.id_name.valid_input().as_string()),
vec![old_identity],
)?;
Ok(())
}
#[test]
fn edit_password() -> Result<(), Error> {
let old_identity = Identity::new(
"edit_password",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
let new_identity = Identity::new(
"edit_password",
default_id_name(),
None,
None,
None,
None,
None,
Some(
Interaction::new(
prompt::PASSWORD,
InteractionType::Password {
input: default::PASSWORD.to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
)
.with_second_attempt(SecondAttempt::new(
InteractionType::Password {
input: "Good-Password".to_string(),
confirm: Some(prompt::PASSWORD_REPROMPT.to_string()),
},
"Password mismatch",
)?)?,
),
)?;
new_identity.run(
&SubCommand::Edit(old_identity.id_name.valid_input().as_string()),
vec![old_identity],
)?;
Ok(())
}
#[test]
fn remove() -> Result<(), Error> {
let identity = Identity::new(
"new_minimal",
default_id_name(),
None,
None,
None,
None,
None,
None,
)?;
identity.run(&SubCommand::Remove, vec![identity.clone()])?;
Ok(())
}
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![warn(clippy::cargo)]
pub mod identity;
use std::io::{Read, Write};
use anyhow::{bail, Error};
use expectrl::{
process::{unix::UnixProcess, NonBlocking},
ControlCode, Regex, Session,
};
#[derive(Clone, Debug)]
pub enum InteractionType {
Confirm(bool),
Input(String),
Password {
input: String,
confirm: Option<String>,
},
}
impl InteractionType {
pub fn as_string(&self) -> String {
match self {
Self::Confirm(confirm) => {
if *confirm {
String::from('y')
} else {
String::from('n')
}
}
Self::Input(input) | Self::Password { input, .. } => input.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct SecondAttempt {
input: InteractionType,
error_message: String,
}
impl SecondAttempt {
pub fn new<S: Into<String>>(input: InteractionType, error_msg: S) -> Result<Self, Error> {
let error_message: String = error_msg.into();
if matches!(input, InteractionType::Confirm(_)) && !error_message.is_empty() {
bail!("Cannot have error message for confirm propmt");
}
Ok(Self {
input,
error_message,
})
}
}
#[derive(Clone, Debug)]
pub struct Interaction {
prompt_message: String,
input: InteractionType,
second_attempt: Option<SecondAttempt>,
}
impl Interaction {
pub fn new<S: Into<String>>(prompt_message: S, input: InteractionType) -> Self {
Self {
prompt_message: prompt_message.into(),
input,
second_attempt: None,
}
}
pub fn with_second_attempt(mut self, second_attempt: SecondAttempt) -> Result<Self, Error> {
if let Some(second_attempt) = self.second_attempt.clone() {
let interaction_type = second_attempt.input;
if !matches!(&self.input, interaction_type) {
bail!("Cannot have non-matching second input!");
}
}
self.second_attempt = Some(second_attempt);
Ok(self)
}
pub fn get_input(&self, valid: bool) -> String {
if let Some(invalid) = self.invalid_input() && !valid {
invalid.as_string()
} else {
self.valid_input().as_string()
}
}
pub fn invalid_input(&self) -> Option<InteractionType> {
if self.second_attempt.is_some() {
Some(self.input.clone())
} else {
None
}
}
pub fn valid_input(&self) -> InteractionType {
if let Some(second_input) = &self.second_attempt {
second_input.input.clone()
} else {
self.input.clone()
}
}
pub fn interact<S: NonBlocking + Write + Read>(
&self,
session: &mut Session<UnixProcess, S>,
) -> Result<(), Error> {
// Wait for the text to come in
println!("Expecting prompt message: {}", self.prompt_message);
session.expect(&self.prompt_message)?;
match &self.input {
InteractionType::Confirm(confirm) => {
println!("Sending confirmation: {confirm}");
session.send(&self.input.as_string())?;
}
InteractionType::Input(_) => {
if let Some(invalid_input) = self.invalid_input() {
clear_prompt(session)?;
println!("Sending invalid input: {}", invalid_input.as_string());
session.send(invalid_input.as_string())?;
session.send_control(ControlCode::CarriageReturn)?;
let error_message = self.second_attempt.clone().unwrap().error_message;
println!("Expecting error message: {error_message}");
session.expect(error_message)?;
}
clear_prompt(session)?;
let valid_input = self.valid_input().as_string();
println!("Sending valid input: {}", valid_input);
session.send(valid_input)?;
session.send_control(ControlCode::CarriageReturn)?;
}
InteractionType::Password { confirm, .. } => {
let valid_password = self.valid_input().as_string();
println!("Sending valid password: {valid_password}");
session.send(&valid_password)?;
session.send_control(ControlCode::CarriageReturn)?;
// If there is a second attempt, send the invalid password
if let Some(second_attempt) = self.invalid_input() {
let confirm_prompt = confirm.as_ref().unwrap();
println!("Expecting password re-prompt: {confirm_prompt}");
session.expect(confirm_prompt)?;
let invalid_password = second_attempt.as_string();
println!("Sending invalid password: {invalid_password}");
session.send(&invalid_password)?;
session.send_control(ControlCode::CarriageReturn)?;
let error_message = self.second_attempt.clone().unwrap().error_message;
println!("Expecting error message: {error_message}");
session.expect(&error_message)?;
}
// Sometimes the password needs to be confirmed
if let Some(confirm_prompt) = confirm {
// In the case of invalid input, we have to send twice
if self.invalid_input().is_some() {
println!("Expecting prompt message: {}", self.prompt_message);
session.expect(&self.prompt_message)?;
println!("Sending valid password: {valid_password}");
session.send(&valid_password)?;
session.send_control(ControlCode::CarriageReturn)?;
}
println!("Expecting password re-prompt: {confirm_prompt}");
session.expect(confirm_prompt)?;
println!("Re-sending valid password: {valid_password}");
session.send(&valid_password)?;
session.send_control(ControlCode::CarriageReturn)?;
}
}
}
Ok(())
}
}
fn clear_prompt<S: NonBlocking + Write + Read>(
session: &mut Session<UnixProcess, S>,
) -> Result<(), Error> {
println!("Clearing prompt");
// Use regex to find where the prompt ends
let prompt_regex = r":.*";
let captures = session.expect(Regex(prompt_regex))?;
let matches = captures.matches();
// Clear default text by sending backspaces
for _ in 0..matches.last().unwrap().len() {
session.send_control(ControlCode::Backspace)?;
}
Ok(())
}
use std::{
ffi::OsStr,
io::Read,
path::{Path, PathBuf},
process::Command,
};
use anyhow::Error;
use expectrl::{Session, WaitStatus};
use super::{Interaction, InteractionType};
pub mod default {
pub const ID_NAME: &str = "my_identity";
pub const FULL_NAME: &str = "Firstname Lastname";
pub const EMAIL: &str = "person@example.com";
pub const EXPIRY: &str = "2056-01-01";
pub const LOGIN: &str = "my_username";
pub const ORIGIN: &str = "ssh.pijul.com";
pub const PASSWORD: &str = "correct-horse-battery-staple";
}
pub mod prompt {
pub const ID_NAME: &str = "Unique identity name";
pub const FULL_NAME: &str = "Display name";
pub const EMAIL: &str = "Email (leave blank for none)";
pub const EXPIRY_DATE: &str = "Expiry date (YYYY-MM-DD)";
pub const LOGIN: &str = "Remote username";
pub const ORIGIN: &str = "Remote URL";
pub const PASSWORD: &str = "Secret key password";
pub const PASSWORD_REPROMPT: &str = "Confirm password";
pub mod confirm {
pub const EXPIRY: &str = "Do you want this key to expire?";
pub const REMOTE: &str = "Do you want to link this identity to a remote?";
pub const ENCRYPTION: &str = "Do you want to change the encryption?";
}
}
const CONFIG_DATA: &str = "colors = 'never'
[author]
login = ''";
#[derive(Clone)]
pub enum SubCommand {
New,
Edit(String),
Remove,
}
#[derive(Clone)]
pub struct Identity {
pub id_name: Interaction,
pub full_name: Option<String>,
pub email: Option<Interaction>,
pub expiry: Option<Interaction>,
pub login: Option<Interaction>,
pub origin: Option<Interaction>,
pub password: Option<Interaction>,
config_path: PathBuf,
}
impl Identity {
pub fn new<P: AsRef<Path>>(
path_name: P,
id_name: Interaction,
full_name: Option<String>,
email: Option<Interaction>,
expiry: Option<Interaction>,
login: Option<Interaction>,
remote: Option<Interaction>,
password: Option<Interaction>,
) -> Result<Self, Error> {
let config_path = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(path_name);
let identity = Self {
id_name,
full_name,
email,
expiry,
login,
origin: remote,
password,
config_path,
};
identity.reset_fs(Vec::new().as_slice())?;
Ok(identity)
}
pub fn reset_fs(&self, existing_identities: &[Identity]) -> Result<(), Error> {
let mut config_path = self.config_path.clone();
config_path.push("identities");
if config_path.exists() {
std::fs::remove_dir_all(&config_path)?;
}
std::fs::create_dir_all(&config_path)?;
config_path.pop();
config_path.push("config.toml");
std::fs::write(&config_path, CONFIG_DATA)?;
config_path.pop();
// Create every identity that should exist
for existing_id in existing_identities {
assert_eq!(existing_id.config_path, config_path);
println!(
"Creating existing identity with name: {}",
existing_id.id_name.valid_input().as_string()
);
existing_id.run_cli_edit(
generate_command(&config_path, &SubCommand::New),
true,
&SubCommand::New,
)?;
}
Ok(())
}
fn verify(&self) -> Result<(), Error> {
let identity_path = self
.config_path
.join("identities")
.join(&self.id_name.valid_input().as_string())
.join("identity.toml");
// Parse the generated TOML and verify
let identity_data = std::fs::read_to_string(identity_path)?;
let toml_data = identity_data.parse::<toml::Value>().unwrap();
self.full_name.as_ref().map_or_else(
|| {
if let Some(full_name) = toml_data.get("name") {
assert_eq!(full_name.as_str().unwrap(), whoami::realname());
}
},
|full_name| {
assert_eq!(
full_name.as_str(),
toml_data.get("name").unwrap().as_str().unwrap()
);
},
);
self.email.as_ref().map_or_else(
|| {
assert!(toml_data.get("email").is_none());
},
|email| {
assert_eq!(
email.valid_input().as_string().as_str(),
toml_data.get("email").unwrap().as_str().unwrap()
);
},
);
self.login.as_ref().map_or_else(
|| {
let default = toml::value::Value::String(String::new());
let data = toml_data.get("login").unwrap_or(&default).as_str().unwrap();
assert!(data.is_empty() || data == whoami::username());
},
|login| {
assert_eq!(
login.valid_input().as_string().as_str(),
toml_data.get("login").unwrap().as_str().unwrap()
);
},
);
self.origin.as_ref().map_or_else(
|| {
let default = toml::value::Value::String(String::new());
let data = toml_data
.get("origin")
.unwrap_or(&default)
.as_str()
.unwrap();
assert!(data.is_empty() || data == "ssh.pijul.com");
},
|origin| {
assert_eq!(
origin.valid_input().as_string().as_str(),
toml_data.get("origin").unwrap().as_str().unwrap()
);
},
);
if let Some(expiry) = &self.expiry {
let time_stamp = toml_data
.get("public_key")
.unwrap()
.get("expires")
.unwrap()
.as_str()
.unwrap();
let parsed_time_stamp =
dateparser::parse_with_timezone(time_stamp, &chrono::offset::Utc)?;
assert_eq!(
expiry.valid_input().as_string(),
parsed_time_stamp.format("%Y-%m-%d").to_string()
);
} else {
assert!(toml_data
.get("public_key")
.unwrap()
.get("expires")
.is_none());
}
let mut secret_key_file = std::fs::File::open(
self.config_path
.join("identities")
.join(self.id_name.valid_input().as_string())
.join("secret_key.json"),
)?;
let mut secret_key_text = String::new();
secret_key_file.read_to_string(&mut secret_key_text)?;
let secret_key: libpijul::key::SecretKey = serde_json::from_str(&secret_key_text)?;
assert_eq!(secret_key.encryption.is_some(), self.password.is_some());
self.password.as_ref().map_or_else(
|| {
secret_key.load(None).unwrap();
},
|password| {
secret_key
.load(Some(password.valid_input().as_string().as_str()))
.unwrap();
},
);
Ok(())
}
pub fn run_cli_edit(
&self,
mut pijul_cmd: Command,
valid: bool,
subcmd: &SubCommand,
) -> Result<WaitStatus, Error> {
pijul_cmd.arg("--no-prompt").arg("--name");
match subcmd {
SubCommand::New => {
pijul_cmd.arg(self.id_name.get_input(valid));
}
SubCommand::Edit(old_name) => {
pijul_cmd
.arg(&old_name)
.arg("--new-name")
.arg(self.id_name.get_input(valid));
}
SubCommand::Remove => {
panic!("Wrong function call!");
}
};
if let Some(full_name) = self.full_name.clone() {
pijul_cmd.arg("--display-name").arg(full_name);
}
if let Some(email) = self.email.clone() {
pijul_cmd.arg("--email").arg(email.get_input(valid));
}
if let Some(expiry) = self.expiry.clone() {
pijul_cmd.arg("--expiry").arg(expiry.get_input(valid));
}
if let Some(login) = self.login.clone() {
pijul_cmd.arg("--username").arg(login.get_input(valid));
}
if let Some(origin) = self.origin.clone() {
pijul_cmd.arg("--remote").arg(origin.get_input(valid));
}
if self.password.is_some() {
pijul_cmd.arg("--read-password");
}
println!(
"Running pijul with args: {:#?}",
pijul_cmd
.get_args()
.collect::<Vec<_>>()
.join(OsStr::new(" "))
);
let mut session = Session::spawn(pijul_cmd)?;
if let Some(password) = self.password.clone() && valid {
password.interact(&mut session)?;
}
Ok(session.wait()?)
}
fn run_interactive_edit(&self, pijul_cmd: Command) -> Result<WaitStatus, Error> {
let mut session = Session::spawn(pijul_cmd)?;
self.id_name.interact(&mut session)?;
if let Some(full_name) = self.full_name.clone() {
Interaction::new(prompt::FULL_NAME, InteractionType::Input(full_name))
.interact(&mut session)?;
} else {
Interaction::new(prompt::FULL_NAME, InteractionType::Input(String::new()))
.interact(&mut session)?;
}
self.email
.clone()
.unwrap_or(Interaction::new(
prompt::EMAIL,
InteractionType::Input(String::new()),
))
.interact(&mut session)?;
Interaction::new(
format!("{} (Current expiry: never)", prompt::confirm::EXPIRY),
InteractionType::Confirm(self.expiry.is_some()),
)
.interact(&mut session)?;
if let Some(expiry) = self.expiry.clone() {
expiry.interact(&mut session)?;
}
let remote_data = self.login.is_some() || self.origin.is_some();
Interaction::new(
prompt::confirm::REMOTE,
InteractionType::Confirm(remote_data),
)
.interact(&mut session)?;
if remote_data {
if let Some(login) = self.login.clone() {
login.interact(&mut session)?;
} else {
// Use an empty login
Interaction::new(prompt::LOGIN, InteractionType::Input(String::new()))
.interact(&mut session)?;
}
if let Some(origin) = self.origin.clone() {
origin.interact(&mut session)?;
} else {
// Use an empty origin
Interaction::new(prompt::ORIGIN, InteractionType::Input(String::new()))
.interact(&mut session)?;
}
}
Interaction::new(
format!(
"{} (Current status: not encrypted)",
prompt::confirm::ENCRYPTION
),
InteractionType::Confirm(self.password.is_some()),
)
.interact(&mut session)?;
if let Some(password) = self.password.clone() {
password.interact(&mut session)?;
}
Ok(session.wait()?)
}
pub fn run_edit(
&self,
subcmd: &SubCommand,
existing_identities: Vec<Self>,
) -> Result<(), Error> {
let invalid_interactions = [
self.id_name.invalid_input(),
self.email.as_ref().and_then(Interaction::invalid_input),
self.expiry.as_ref().and_then(Interaction::invalid_input),
];
// If any of the items have invalid values, we need to test the program correctly errors out
if invalid_interactions.iter().any(Option::is_some) {
println!("Detected invalid inputs, expecting failure with --no-prompt");
self.reset_fs(&existing_identities)?;
let cli_status =
self.run_cli_edit(generate_command(&self.config_path, subcmd), false, subcmd)?;
assert!(!matches!(cli_status, WaitStatus::Exited(_, exitcode::OK)));
println!("Program failed as expected");
}
self.reset_fs(&existing_identities)?;
let cli_status =
self.run_cli_edit(generate_command(&self.config_path, subcmd), true, subcmd)?;
assert!(matches!(cli_status, WaitStatus::Exited(_, exitcode::OK)));
self.verify()?;
println!("Successfully ran pijul in CLI mode");
self.reset_fs(&existing_identities)?;
let interactive_status =
self.run_interactive_edit(generate_command(&self.config_path, subcmd))?;
assert!(matches!(
interactive_status,
WaitStatus::Exited(_, exitcode::OK)
));
self.verify()?;
println!("Successfully ran pijul in interactive mode");
Ok(())
}
pub fn run(&self, subcmd: &SubCommand, existing_identities: Vec<Self>) -> Result<(), Error> {
match subcmd {
SubCommand::New | SubCommand::Edit(_) => {
self.run_edit(subcmd, existing_identities)?;
}
SubCommand::Remove => {
self.reset_fs(&existing_identities)?;
let pijul_cmd = generate_command(&self.config_path, subcmd);
println!(
"Running pijul with args: {:#?}",
pijul_cmd
.get_args()
.collect::<Vec<_>>()
.join(OsStr::new(" "))
);
let mut session = Session::spawn(pijul_cmd)?;
Interaction::new("Do you wish to continue?", InteractionType::Confirm(true))
.interact(&mut session)?;
let status = session.wait()?;
assert!(matches!(status, WaitStatus::Exited(_, exitcode::OK)));
assert!(!self
.config_path
.join("identities")
.join(&self.id_name.valid_input().as_string())
.exists());
}
}
Ok(())
}
}
fn subcommand_name(subcmd: &SubCommand) -> String {
match subcmd {
SubCommand::New => String::from("new"),
SubCommand::Edit(_) => String::from("edit"),
SubCommand::Remove => String::from("remove"),
}
}
fn generate_command(config_path: &PathBuf, subcmd: &SubCommand) -> Command {
let mut pijul_cmd = Command::new(env!("CARGO_BIN_EXE_pijul"));
pijul_cmd.env("PIJUL_CONFIG_DIR", config_path);
pijul_cmd.arg("identity");
let subcommand = subcommand_name(&subcmd);
pijul_cmd.arg(&subcommand);
if subcommand == "edit" || subcommand == "new" {
pijul_cmd.arg("--no-link");
}
pijul_cmd
}