Implement testing for identity management

finchie
Aug 16, 2022, 9:06 AM
FOCBVLOUXYA7ZCUZA2CU3JU2QGF3ZOXW6EAVL5KZINN43GXNL7CQC

Dependencies

  • [2] SXEYMYF7 Fixing the bad changes in history (unfortunately, by rebooting).

Change contents

  • file addition: tests (d--r------)
    [2.21407]
  • file addition: identity.rs (----------)
    [0.17]
    #![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(())
    }
  • file addition: common (d--r------)
    [0.17]
  • file addition: mod.rs (----------)
    [0.10806]
    #![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(())
    }
  • file addition: identity.rs (----------)
    [0.10806]
    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
    }