blob: ce5b5763620ae771d7136028394558e68a598d4f [file] [log] [blame] [edit]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{Context, Result, anyhow};
use errors::{ffx_bail, ffx_bail_with_code};
use ffx_config::api::ConfigError;
use ffx_config::{
ConfigLevel, EnvironmentContext, disable_metrics, enable_basic_metrics,
enable_enhanced_metrics, print_config, show_metrics_status,
};
use ffx_config_plugin_args::{
AddCommand, AnalyticsCommand, AnalyticsControlCommand, ConfigCommand, EnvAccessCommand,
EnvCommand, EnvSetCommand, GetCommand, MappingMode, RemoveCommand, SetCommand, SshKeyCommand,
SubCommand,
};
use ffx_ssh::{SshKeyErrorKind, SshKeyFiles};
use ffx_writer::{ToolIO, VerifiedMachineWriter};
use fho::{FfxMain, FfxTool};
use schemars::JsonSchema;
use serde::Serialize;
use serde_json::Value;
use std::fs::{File, OpenOptions};
use std::io::Write;
#[derive(FfxTool)]
#[target(None)]
pub struct ConfigTool {
#[command]
config: ConfigCommand,
ctx: EnvironmentContext,
}
fho::embedded_plugin!(ConfigTool);
#[derive(Debug, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ConfigToolMessage {
Message(String),
Data(Value),
}
impl std::fmt::Display for ConfigToolMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::Message(message) => message.to_owned(),
Self::Data(data) => format!("{}", data),
};
write!(f, "{}", message)
}
}
#[async_trait::async_trait(?Send)]
impl FfxMain for ConfigTool {
type Writer = VerifiedMachineWriter<ConfigToolMessage>;
async fn main(self, mut writer: Self::Writer) -> fho::Result<()> {
match &self.config.sub {
SubCommand::CheckSshKeys(check_ssh_cmd) => {
exec_check_ssh_keys(&self.ctx, check_ssh_cmd, &mut writer).await
}
SubCommand::Env(env) => exec_env(&self.ctx, env, writer).await,
SubCommand::Get(get_cmd) => exec_get(&self.ctx, get_cmd, writer),
SubCommand::Set(set_cmd) => exec_set(&self.ctx, set_cmd).await,
SubCommand::Remove(remove_cmd) => exec_remove(&self.ctx, remove_cmd).await,
SubCommand::Add(add_cmd) => exec_add(&self.ctx, add_cmd).await,
SubCommand::Analytics(analytics_cmd) => exec_analytics(analytics_cmd).await,
}
.map_err(fho::Error::from)
}
}
fn output<W: Write>(mut writer: W, value: Option<Value>) -> Result<()> {
match value {
Some(v) => writeln!(writer, "{}", serde_json::to_string_pretty(&v).unwrap())
.map_err(|e| anyhow!("{}", e)),
// Use 2 error code so wrapper scripts don't need check for the string to differentiate
// errors.
None => ffx_bail_with_code!(2, "Value not found"),
}
}
fn output_array<W: Write>(
mut writer: W,
values: std::result::Result<Vec<Value>, ConfigError>,
) -> Result<()> {
match values {
Ok(v) => {
if v.len() == 1 {
writeln!(writer, "{}", serde_json::to_string_pretty(&v[0]).unwrap())
.map_err(|e| anyhow!("{}", e))
} else {
writeln!(writer, "{}", serde_json::to_string_pretty(&Value::Array(v)).unwrap())
.map_err(|e| anyhow!("{}", e))
}
}
// Use 2 error code so wrapper scripts don't need check for the string to differentiate
// errors.
Err(_) => ffx_bail_with_code!(2, "Value not found"),
}
}
fn output_first_element<W: Write>(mut writer: W, value: Option<Value>) -> Result<()> {
match value {
Some(Value::Array(vals)) => {
if !vals.is_empty() {
writeln!(writer, "{}", serde_json::to_string_pretty(&vals[0]).unwrap())
.map_err(|e| anyhow!("{}", e))
} else {
ffx_bail_with_code!(2, "Value not found")
}
}
Some(v) => writeln!(writer, "{}", serde_json::to_string_pretty(&v).unwrap())
.map_err(|e| anyhow!("{}", e)),
// Use 2 error code so wrapper scripts don't need check for the string to differentiate
// errors.
None => ffx_bail_with_code!(2, "Value not found"),
}
}
fn exec_get<W: Write>(ctx: &EnvironmentContext, get_cmd: &GetCommand, writer: W) -> Result<()> {
match get_cmd.name.as_ref() {
Some(_) => match get_cmd.process {
MappingMode::Raw => {
let value: Option<Value> = get_cmd.query(ctx).get_raw(ctx)?;
output(writer, value)
}
MappingMode::Substitute => {
let value: std::result::Result<Vec<Value>, _> = get_cmd.query(ctx).get(ctx);
output_array(writer, value)
}
MappingMode::File => {
let value = get_cmd.query(ctx).get_file(ctx)?;
output_first_element(writer, value)
}
},
None => print_config(ctx, writer),
}
}
async fn exec_set(ctx: &EnvironmentContext, set_cmd: &SetCommand) -> Result<()> {
log::debug!("Set command running...");
set_cmd.query(ctx).set(ctx, set_cmd.value.clone())
}
async fn exec_remove(ctx: &EnvironmentContext, remove_cmd: &RemoveCommand) -> Result<()> {
let entry = remove_cmd.query(ctx);
// Check that there is a value before removing it.
if let Ok(Some(_val)) = entry.get_raw::<Option<Value>>(ctx) {
entry.remove(ctx)
} else {
ffx_bail_with_code!(2, "Configuration key not found")
}
}
async fn exec_add(ctx: &EnvironmentContext, add_cmd: &AddCommand) -> Result<()> {
add_cmd.query(ctx).add(ctx, Value::String(format!("{}", add_cmd.value)))
}
async fn exec_env_set<W: Write>(
env_context: &EnvironmentContext,
mut writer: W,
s: &EnvSetCommand,
) -> Result<()> {
let env_file = env_context.env_file_path().context("Getting ffx environment file path")?;
if !env_file.exists() {
writeln!(writer, "\"{}\" does not exist, creating empty json file", env_file.display())?;
let mut file = File::create(&env_file).context("opening write buffer")?;
file.write_all(b"{}").context("writing configuration file")?;
if !env_context.env_kind().is_isolated() {
file.sync_all().context("syncing configuration file to filesystem")?;
}
}
// Double check read/write permissions and create the file if it doesn't exist.
let _ = OpenOptions::new().read(true).write(true).create(true).open(&s.file)?;
let mut env = env_context.load().context("Loading environment file")?;
match &s.level {
ConfigLevel::User => env.set_user(Some(&s.file)),
ConfigLevel::Build => env.set_build(&s.file)?,
ConfigLevel::Global => env.set_global(Some(&s.file)),
_ => ffx_bail!("This configuration is not stored in the environment."),
}
env.save()
}
async fn exec_env<W: Write>(
ctx: &EnvironmentContext,
env_command: &EnvCommand,
mut writer: W,
) -> Result<()> {
match &env_command.access {
Some(a) => match a {
EnvAccessCommand::Set(s) => exec_env_set(ctx, writer, s).await,
EnvAccessCommand::Get(g) => {
writeln!(
writer,
"{}",
&ctx.load().context("Loading environment file")?.display(&g.level)
)?;
Ok(())
}
},
None => {
writeln!(
writer,
"{}",
&ctx.load().context("Loading environment file")?.display(&None)
)?;
Ok(())
}
}
}
async fn exec_analytics(analytics_cmd: &AnalyticsCommand) -> Result<()> {
let writer = Box::new(std::io::stdout());
match &analytics_cmd.sub {
AnalyticsControlCommand::EnableEnhanced(_) => {
enable_enhanced_metrics().await.with_context(|| "Failed to enable metrics")?;
show_metrics_status(writer).await.with_context(|| "Failed to read metrics state")?
}
AnalyticsControlCommand::Enable(_) => {
enable_basic_metrics().await.with_context(|| "Failed to enable metrics")?;
show_metrics_status(writer).await.with_context(|| "Failed to read metrics state")?
}
AnalyticsControlCommand::Disable(_) => {
disable_metrics().await.with_context(|| "Failed to disable metrics")?;
show_metrics_status(writer).await.with_context(|| "Failed to read metrics state")?
}
AnalyticsControlCommand::Show(_) => {
show_metrics_status(writer).await.with_context(|| "Failed to read metrics state")?
}
}
Ok(())
}
async fn exec_check_ssh_keys(
ctx: &EnvironmentContext,
_check_ssh_command: &SshKeyCommand,
writer: &mut VerifiedMachineWriter<ConfigToolMessage>,
) -> Result<()> {
match SshKeyFiles::load(&ctx).await {
Ok(ssh_files) => {
match ssh_files.check_keys(true) {
Ok(message) => {
writer.item(&ConfigToolMessage::Message(message))?;
}
Err(e) => match e.kind {
SshKeyErrorKind::BadKeyType => writer.item(&ConfigToolMessage::Message(
format!("SSH keys type not supported: {}", e.message),
))?,
SshKeyErrorKind::BadConfiguration => writer.item(
&ConfigToolMessage::Message(format!("SSH keys configuration problem: {e}")),
)?,
_ => writer
.item(&ConfigToolMessage::Message(format!("SSH keys problem: {e}.")))?,
},
};
}
Err(e) => {
writer.item(&ConfigToolMessage::Message(format!("Could not get SSH key paths {e}")))?;
}
};
Ok(())
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use std::{env, fs};
use super::*;
use errors::{FfxError, IntoExitCode};
use ffx_config::{SelectMode, test_init};
use ffx_writer::{Format, TestBuffers};
use serde_json::json;
#[fuchsia::test]
async fn test_exec_env_set_set_values() -> Result<()> {
let test_env = test_init()?;
let writer = Vec::<u8>::new();
let cmd = EnvSetCommand { file: "test.json".into(), level: ConfigLevel::User };
exec_env_set(&test_env.context, writer, &cmd).await?;
assert_eq!(cmd.file, test_env.load().get_user().unwrap());
Ok(())
}
#[fuchsia::test]
async fn test_gey_key() {
let test_env = test_init().expect("test env initialized");
test_env
.context
.query("some-key")
.level(Some(ConfigLevel::User))
.build()
.set(&test_env.context, "a value".into())
.expect("setting value");
let get_cmd = GetCommand {
name: Some("some-key".into()),
process: MappingMode::Substitute,
select: ffx_config::SelectMode::First,
};
let mut writer = Vec::<u8>::new();
exec_get(&test_env.context, &get_cmd, &mut writer).expect("getting value");
assert_eq!(String::from_utf8(writer).unwrap(), "\"a value\"\n".to_string());
}
#[fuchsia::test]
async fn test_remove_key() {
let test_env = test_init().expect("test env initialized");
test_env
.context
.query("some-key")
.level(Some(ConfigLevel::User))
.build()
.set(&test_env.context, "a value".into())
.expect("setting value");
let remove_cmd = RemoveCommand { name: "some-key".into() };
let get_cmd = GetCommand {
name: Some("some-key".into()),
process: MappingMode::Substitute,
select: ffx_config::SelectMode::First,
};
exec_remove(&test_env.context, &remove_cmd).await.expect("remove");
let mut writer = Vec::<u8>::new();
match exec_get(&test_env.context, &get_cmd, &mut writer) {
Ok(_) => panic!("Expected error getting removed key"),
Err(e) => assert_eq!(e.to_string(), "Value not found"),
};
}
#[fuchsia::test]
async fn test_remove_nonexistant_key() {
let test_env = test_init().expect("test env initialized");
let remove_cmd = RemoveCommand { name: "some-key".into() };
match exec_remove(&test_env.context, &remove_cmd).await {
Ok(_) => panic!("Expected error getting removed key"),
Err(e) => {
if let Some(ffx_err) = e.downcast_ref::<FfxError>() {
assert_eq!(ffx_err.to_string(), "Configuration key not found");
assert!(ffx_err.exit_code() != 0, "Expected non-zero exit code");
} else {
}
}
};
}
#[fuchsia::test]
async fn test_list_processed_by_raw() {
let test_env = test_init().expect("test env");
let mut writer = Vec::<u8>::new();
let private_path1 = test_env.isolate_root.path().join("privatekey1");
let private_path2 = test_env.isolate_root.path().join("privatekey2");
fs::write(&private_path1, "path1").expect("key 1 written");
fs::write(&private_path2, "path2").expect("key 2 written");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path1.to_string_lossy(),
private_path2.to_string_lossy(),
]),
)
.expect("set ssh.priv");
exec_get(
&test_env.context,
&GetCommand {
name: Some("ssh.priv".into()),
process: MappingMode::Raw,
select: SelectMode::First,
},
&mut writer,
)
.expect("exec_get");
let got = String::from_utf8_lossy(&writer);
let want = serde_json::to_string_pretty(&json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path1,
private_path2
]))
.expect("json output");
assert_eq!(got, format!("{}\n", want));
}
#[fuchsia::test]
async fn test_list_processed_by_substitute() {
let test_env = test_init().expect("test env");
let mut writer = Vec::<u8>::new();
let private_path1 = test_env.isolate_root.path().join("privatekey1");
let private_path2 = test_env.isolate_root.path().join("privatekey2");
fs::write(&private_path1, "path1").expect("key 1 written");
fs::write(&private_path2, "path2").expect("key 2 written");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path1.to_string_lossy(),
private_path2.to_string_lossy(),
]),
)
.expect("set ssh.priv");
exec_get(
&test_env.context,
&GetCommand {
name: Some("ssh.priv".into()),
process: MappingMode::Substitute,
select: SelectMode::First,
},
&mut writer,
)
.expect("exec_get");
let got = String::from_utf8_lossy(&writer);
let want = serde_json::to_string_pretty(&json!([private_path1, private_path2]))
.expect("json output");
assert_eq!(got, format!("{}\n", want));
}
#[fuchsia::test]
async fn test_list_processed_by_substitute_with_env() {
// Set the env before the test env
// assert the key is not already in the environment
assert!(
env::var("ENV_SSH_PATH_FOR_TESTING_").is_err(),
"Expected weird testing env variable to be unset"
);
unsafe { env::set_var("ENV_SSH_PATH_FOR_TESTING_", "private_path1") };
let test_env = test_init().expect("test env");
let mut writer = Vec::<u8>::new();
let private_path1 = test_env.isolate_root.path().join("privatekey1");
let private_path2 = test_env.isolate_root.path().join("privatekey2");
fs::write(&private_path1, "path1").expect("key 1 written");
fs::write(&private_path2, "path2").expect("key 2 written");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!(["$ENV_SSH_PATH_FOR_TESTING_", private_path2.to_string_lossy(),]),
)
.expect("set ssh.priv");
exec_get(
&test_env.context,
&GetCommand {
name: Some("ssh.priv".into()),
process: MappingMode::Substitute,
select: SelectMode::First,
},
&mut writer,
)
.expect("exec_get");
let got = String::from_utf8_lossy(&writer);
let want = serde_json::to_string_pretty(&json!(["private_path1", private_path2]))
.expect("json output");
assert_eq!(got, format!("{}\n", want));
}
#[fuchsia::test]
async fn test_list_single_by_file() {
let test_env = test_init().expect("test env");
let mut writer = Vec::<u8>::new();
let private_path1 = test_env.isolate_root.path().join("privatekey1");
fs::write(&private_path1, "path1").expect("key 1 written");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(&test_env.context, json!([private_path1.to_string_lossy(),]))
.expect("set ssh.priv");
exec_get(
&test_env.context,
&GetCommand {
name: Some("ssh.priv".into()),
process: MappingMode::File,
select: SelectMode::First,
},
&mut writer,
)
.expect("exec_get");
let got = String::from_utf8_lossy(&writer);
assert_eq!(got, format!("{}\n", json!(private_path1)));
}
#[fuchsia::test]
async fn test_list_processed_by_file() {
let test_env = test_init().expect("test env");
let mut writer = Vec::<u8>::new();
let private_path1 = test_env.isolate_root.path().join("privatekey1");
let private_path2 = test_env.isolate_root.path().join("privatekey2");
fs::write(&private_path1, "path1").expect("key 1 written");
fs::write(&private_path2, "path2").expect("key 2 written");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path1.to_string_lossy(),
private_path2.to_string_lossy(),
]),
)
.expect("set ssh.priv");
exec_get(
&test_env.context,
&GetCommand {
name: Some("ssh.priv".into()),
process: MappingMode::File,
select: SelectMode::First,
},
&mut writer,
)
.expect("exec_get");
let got = String::from_utf8_lossy(&writer);
assert_eq!(got, format!("{}\n", json!(private_path1)));
}
#[fuchsia::test]
async fn test_list_processed_by_file_with_env() {
let private_path1 = tempfile::NamedTempFile::new().expect("temp file 1");
// Set the env before the test env
// assert the key is not already in the environment
assert!(
env::var("ENV_SSH_PATH_FOR_TESTING_2").is_err(),
"Expected weird testing env variable to be unset"
);
unsafe { env::set_var("ENV_SSH_PATH_FOR_TESTING_2", private_path1.path()) };
let test_env = test_init().expect("test env");
let mut writer = Vec::<u8>::new();
let private_path2 = test_env.isolate_root.path().join("privatekey2");
fs::write(&private_path2, "path2").expect("key 2 written");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!(["$ENV_SSH_PATH_FOR_TESTING_2", private_path2.to_string_lossy(),]),
)
.expect("set ssh.priv");
exec_get(
&test_env.context,
&GetCommand {
name: Some("ssh.priv".into()),
process: MappingMode::File,
select: SelectMode::First,
},
&mut writer,
)
.expect("exec_get");
let got = String::from_utf8_lossy(&writer);
assert_eq!(got, format!("{}\n", json!(private_path1.path())));
}
#[fuchsia::test]
async fn test_exec_check_mismatched_ssh_keys() {
let test_env = test_init().expect("test env");
let auth_key_path1 = test_env.isolate_root.path().join("authorized_keys1");
let private_path1 = test_env.isolate_root.path().join("privatekey1");
let auth_key_path2 = test_env.isolate_root.path().join("authorized_keys2");
let private_path2 = test_env.isolate_root.path().join("privatekey2");
test_env
.context
.query("ssh.pub")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!(["$ENV_PATH_THAT_IS_NOT_SET", auth_key_path2.to_string_lossy(), "someother"]),
)
.expect("set ssh.pub");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path1.to_string_lossy(),
"someother/place"
]),
)
.expect("set ssh.priv");
let keys = SshKeyFiles {
authorized_keys: auth_key_path1.clone(),
private_key: private_path1.clone(),
};
keys.create_keys_if_needed(false).expect("Initializing keys");
let other_keys = SshKeyFiles {
authorized_keys: auth_key_path2.clone(),
private_key: private_path2.clone(),
};
other_keys.create_keys_if_needed(false).expect("Initializing other keys");
let cmd = ConfigCommand { sub: SubCommand::CheckSshKeys(SshKeyCommand {}) };
let tool = ConfigTool { config: cmd, ctx: test_env.context.clone() };
let buffers = TestBuffers::default();
let writer = <ConfigTool as FfxMain>::Writer::new_test(Some(Format::Json), &buffers);
let result = tool.main(writer).await;
assert!(result.is_ok());
let output = buffers.into_stdout_str();
let expected_message = format!("{}\n",
serde_json::to_string(
&ConfigToolMessage::Message(
format!("Keys repaired: KeyMismatch:Could not find matching public key for the private key {}.", private_path1.to_string_lossy())
)
).expect("Should be a string")
);
assert_eq!(expected_message, output);
let cmd = ConfigCommand { sub: SubCommand::CheckSshKeys(SshKeyCommand {}) };
let tool = ConfigTool { config: cmd, ctx: test_env.context.clone() };
let buffers = TestBuffers::default();
let writer = <ConfigTool as FfxMain>::Writer::new_test(Some(Format::Json), &buffers);
let result = tool.main(writer).await;
let output = buffers.into_stdout_str();
assert!(result.is_ok());
let expected_message = format!(
"{}\n",
serde_json::to_string(&ConfigToolMessage::Message(format!(
"SSH Public/Private keys match"
)))
.expect("Should be a string")
);
assert_eq!(expected_message, output);
}
#[fuchsia::test]
async fn test_exec_check_ok_ssh_keys() {
let test_env = test_init().expect("test env");
let auth_key_path = test_env.isolate_root.path().join("authorized_keys");
let private_path = test_env.isolate_root.path().join("privatekey");
test_env
.context
.query("ssh.pub")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!(["$ENV_PATH_THAT_IS_NOT_SET", auth_key_path.to_string_lossy(), "someother"]),
)
.expect("set ssh.pub");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path.to_string_lossy(),
"someother/place"
]),
)
.expect("set ssh.priv");
let keys = SshKeyFiles::load(&test_env.context).await.expect("new ssh keys");
keys.create_keys_if_needed(false).expect("Initializing keys");
assert_eq!(
keys.authorized_keys.display().to_string(),
auth_key_path.to_string_lossy().to_string()
);
assert_eq!(
keys.private_key.display().to_string(),
private_path.to_string_lossy().to_string()
);
let cmd = ConfigCommand { sub: SubCommand::CheckSshKeys(SshKeyCommand {}) };
let tool = ConfigTool { config: cmd, ctx: test_env.context.clone() };
let buffers = TestBuffers::default();
let writer = <ConfigTool as FfxMain>::Writer::new_test(Some(Format::Json), &buffers);
let result = tool.main(writer).await;
assert!(result.is_ok());
let expected_message = format!(
"{}\n",
serde_json::to_string(&ConfigToolMessage::Message(
"SSH Public/Private keys match".to_string()
))
.expect("Should be a string")
);
let output = buffers.into_stdout_str();
assert_eq!(expected_message, output);
}
#[fuchsia::test]
async fn test_exec_check_empty_ssh_keys() {
let test_env = test_init().expect("test env");
let auth_key_path = test_env.isolate_root.path().join("authorized_keys");
let private_path = test_env.isolate_root.path().join("privatekey");
test_env
.context
.query("ssh.pub")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!(["$ENV_PATH_THAT_IS_NOT_SET", auth_key_path.to_string_lossy(), "someother"]),
)
.expect("set ssh.pub");
test_env
.context
.query("ssh.priv")
.level(Some(ConfigLevel::User))
.build()
.set(
&test_env.context,
json!([
"$ENV_PATH_THAT_IS_NOT_SET_2",
private_path.to_string_lossy(),
"someother/place"
]),
)
.expect("set ssh.priv");
let keys = SshKeyFiles::load(&test_env.context).await.expect("new ssh keys");
assert_eq!(
keys.authorized_keys.display().to_string(),
auth_key_path.to_string_lossy().to_string()
);
assert_eq!(
keys.private_key.display().to_string(),
private_path.to_string_lossy().to_string()
);
let cmd = ConfigCommand { sub: SubCommand::CheckSshKeys(SshKeyCommand {}) };
let tool = ConfigTool { config: cmd, ctx: test_env.context.clone() };
let buffers = TestBuffers::default();
let writer = <ConfigTool as FfxMain>::Writer::new_test(Some(Format::Json), &buffers);
let result = tool.main(writer).await;
assert!(result.is_ok());
let output = buffers.into_stdout_str();
let expected_message = format!(
"{}\n",
serde_json::to_string(&ConfigToolMessage::Message(format!(
"Keys repaired: FileNotFound:Private key {} does not exist.",
private_path.to_string_lossy()
),))
.expect("Should be a string")
);
assert_eq!(expected_message, output);
}
}