Add config setting for S3 and Salesforce
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
max_width = 120
|
||||||
|
hard_tabs = true
|
||||||
+84
-142
@@ -3,6 +3,8 @@ use anyhow::{Context, Result};
|
|||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
|
mod set;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
@@ -33,6 +35,31 @@ enum Commands {
|
|||||||
|
|
||||||
/// Change settings
|
/// Change settings
|
||||||
Set {
|
Set {
|
||||||
|
/// A comma separated list of fallback email address,
|
||||||
|
/// used when Salesforce cannot be reached to deliver log messages
|
||||||
|
#[arg(short = 'a', long, env = "IMOJEFS_ADMIN_EMAIL")]
|
||||||
|
admin_email: Option<String>,
|
||||||
|
|
||||||
|
/// AWS S3 bucket name
|
||||||
|
#[arg(short = 'b', long = "s3-bucket", env = "IMOJEFS_S3_BUCKET")]
|
||||||
|
s3_bucket: Option<String>,
|
||||||
|
|
||||||
|
/// AWS Access Key Id
|
||||||
|
#[arg(short = 'k', long, env = "IMOJEFS_S3_CLIENT_ID")]
|
||||||
|
s3_client_id: Option<String>,
|
||||||
|
|
||||||
|
/// AWS Secret Access Key
|
||||||
|
#[arg(short = 's', long, env = "IMOJEFS_S3_CLIENT_SECRET", hide_env_values = true)]
|
||||||
|
s3_client_secret: Option<String>,
|
||||||
|
|
||||||
|
/// AWS Endpoint URL, [default: AWS url]
|
||||||
|
#[arg(short = 'e', long, env = "IMOJEFS_S3_ENDPOINT")]
|
||||||
|
s3_endpoint: Option<String>,
|
||||||
|
|
||||||
|
/// AWS Region
|
||||||
|
#[arg(short = 'r', long, default_value = "us-east-1", env = "IMOJEFS_S3_REGION")]
|
||||||
|
s3_region: Option<String>,
|
||||||
|
|
||||||
/// Salesforce Authentication Url
|
/// Salesforce Authentication Url
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'x',
|
short = 'x',
|
||||||
@@ -44,76 +71,6 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct Args {
|
|
||||||
/// AWS Bucket name
|
|
||||||
#[arg(short = 'b', long, env = "IMOJEFS_S3_BUCKET")]
|
|
||||||
pub s3_bucket: String,
|
|
||||||
|
|
||||||
/// AWS Access Key Id
|
|
||||||
#[arg(short = 'k', long, env = "IMOJEFS_S3_CLIENT_ID")]
|
|
||||||
pub s3_client_id: String,
|
|
||||||
|
|
||||||
/// AWS Secret Access Key
|
|
||||||
#[arg(
|
|
||||||
short = 's',
|
|
||||||
long,
|
|
||||||
env = "IMOJEFS_S3_CLIENT_SECRET",
|
|
||||||
hide_env_values = true
|
|
||||||
)]
|
|
||||||
pub s3_client_secret: String,
|
|
||||||
|
|
||||||
/// AWS Endpoint URL, [default: AWS url]
|
|
||||||
#[arg(short = 'e', long, env = "IMOJEFS_S3_ENDPOINT")]
|
|
||||||
pub s3_endpoint: Option<String>,
|
|
||||||
|
|
||||||
/// AWS Region
|
|
||||||
#[arg(
|
|
||||||
short = 'r',
|
|
||||||
long,
|
|
||||||
default_value = "us-east-1",
|
|
||||||
env = "IMOJEFS_S3_REGION"
|
|
||||||
)]
|
|
||||||
pub s3_region: String,
|
|
||||||
|
|
||||||
/// Salesforce Authentication Url
|
|
||||||
#[arg(
|
|
||||||
short = 'x',
|
|
||||||
long,
|
|
||||||
env = "IMOJEFS_SFDX_AUTH_URL",
|
|
||||||
hide_env_values = true
|
|
||||||
)]
|
|
||||||
pub sfdx_auth_url: String,
|
|
||||||
|
|
||||||
/// Retry failures after X runs
|
|
||||||
#[arg(long, default_value_t = 180, env = "IMOJEFS_RETRY")]
|
|
||||||
pub retry: u64,
|
|
||||||
|
|
||||||
/// A comma separated list of fallback email address,
|
|
||||||
/// used when Salesforce cannot be reached to deliver log messages
|
|
||||||
#[arg(short = 'a', long, env = "IMOJEFS_ADMIN_EMAIL")]
|
|
||||||
pub admin_email: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Args {
|
|
||||||
pub(crate) fn s3_bucket(&self) -> &str {
|
|
||||||
&self.s3_bucket
|
|
||||||
}
|
|
||||||
pub(crate) fn s3_client_id(&self) -> &str {
|
|
||||||
&self.s3_client_id
|
|
||||||
}
|
|
||||||
pub(crate) fn s3_client_secret(&self) -> &str {
|
|
||||||
&self.s3_client_secret
|
|
||||||
}
|
|
||||||
pub(crate) fn s3_endpoint(&self) -> Option<&str> {
|
|
||||||
self.s3_endpoint.as_deref()
|
|
||||||
}
|
|
||||||
pub(crate) fn s3_region(&self) -> &str {
|
|
||||||
&self.s3_region
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the different levels of logging that can be used in an application.
|
/// Represents the different levels of logging that can be used in an application.
|
||||||
///
|
///
|
||||||
/// # Variants
|
/// # Variants
|
||||||
@@ -133,58 +90,31 @@ pub enum LogLevel {
|
|||||||
Trace,
|
Trace,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_configuration_file(config_file: &str) -> Result<()> {
|
|
||||||
let app_config =
|
|
||||||
crate::config::load(config_file).context("Failed to load application configuration")?;
|
|
||||||
crate::APP_CONFIG
|
|
||||||
.set(app_config)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Application configuration has already been initialized"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn list_config_settings(config_file: &str) -> Result<()> {
|
|
||||||
let app_config =
|
|
||||||
crate::config::load(config_file).context("Failed to load application configuration")?;
|
|
||||||
|
|
||||||
println!("Configuration: {config_file}");
|
|
||||||
println!(
|
|
||||||
"sfdx_auth_url = {}",
|
|
||||||
app_config
|
|
||||||
.sfdx_auth_url
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("<not set>")
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_sfdx_auth_url(config_file: &str, url: &str) -> Result<()> {
|
|
||||||
let mut app_config =
|
|
||||||
crate::config::load(config_file).context("Failed to load application configuration")?;
|
|
||||||
|
|
||||||
app_config.sfdx_auth_url = Some(url.to_string());
|
|
||||||
|
|
||||||
crate::config::save(config_file, &app_config)
|
|
||||||
.context("Failed to save Salesforce authentication URL")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn run() {
|
pub(crate) fn run() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Some(command) => match command {
|
Some(command) => match command {
|
||||||
Commands::Set { sfdx_auth_url } => {
|
Commands::Set {
|
||||||
run_set_command(&cli, sfdx_auth_url);
|
admin_email,
|
||||||
},
|
s3_bucket,
|
||||||
Commands::List => {
|
s3_client_id,
|
||||||
run_list_command(&cli);
|
s3_client_secret,
|
||||||
},
|
s3_endpoint,
|
||||||
Commands::FileSync { .. } => {
|
s3_region,
|
||||||
run_file_sync(&cli);
|
sfdx_auth_url,
|
||||||
}
|
} => run_set_command(
|
||||||
|
&cli.config,
|
||||||
|
admin_email,
|
||||||
|
s3_bucket,
|
||||||
|
s3_client_id,
|
||||||
|
s3_client_secret,
|
||||||
|
s3_endpoint,
|
||||||
|
s3_region,
|
||||||
|
sfdx_auth_url,
|
||||||
|
),
|
||||||
|
Commands::List => run_list_command(&cli),
|
||||||
|
Commands::FileSync { .. } => run_file_sync(&cli),
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
println!("Expected subcommand, none given. Use --help for more information");
|
println!("Expected subcommand, none given. Use --help for more information");
|
||||||
@@ -193,6 +123,14 @@ pub(crate) fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_configuration_file(config_file: &str) -> Result<()> {
|
||||||
|
let app_config = crate::config::load(config_file).context("Failed to load application configuration")?;
|
||||||
|
crate::APP_CONFIG
|
||||||
|
.set(app_config)
|
||||||
|
.map_err(|_| anyhow::anyhow!("Application configuration has already been initialized"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_file_sync(cli: &Cli) {
|
fn run_file_sync(cli: &Cli) {
|
||||||
logger::init(&cli.log_level);
|
logger::init(&cli.log_level);
|
||||||
|
|
||||||
@@ -208,7 +146,8 @@ fn run_file_sync(cli: &Cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_list_command(cli: &Cli) {
|
fn run_list_command(cli: &Cli) {
|
||||||
match list_config_settings(&cli.config) {
|
match crate::config::list(&cli.config)
|
||||||
|
.context("Failed to load application configuration") {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error listing configuration settings: {:#}", e);
|
error!("Error listing configuration settings: {:#}", e);
|
||||||
@@ -217,29 +156,32 @@ fn run_list_command(cli: &Cli) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_set_command(cli: &Cli, sfdx_auth_url: &Option<String>) {
|
fn run_set_command(
|
||||||
let mut updated = false;
|
config_file: &str,
|
||||||
|
admin_email: &Option<String>,
|
||||||
updated = run_set_sfdx_auth_url(cli, sfdx_auth_url, &updated);
|
s3_bucket: &Option<String>,
|
||||||
|
s3_client_id: &Option<String>,
|
||||||
if !updated {
|
s3_client_secret: &Option<String>,
|
||||||
println!("Expected at least one setting flag. Use --help for more information");
|
s3_endpoint: &Option<String>,
|
||||||
std::process::exit(1);
|
s3_region: &Option<String>,
|
||||||
}
|
sfdx_auth_url: &Option<String>,
|
||||||
}
|
) {
|
||||||
|
match set::run(
|
||||||
fn run_set_sfdx_auth_url(cli: &Cli, sfdx_auth_url: &Option<String>, updated: &bool) -> bool {
|
config_file,
|
||||||
if let Some(url) = sfdx_auth_url {
|
admin_email,
|
||||||
match set_sfdx_auth_url(&cli.config, url) {
|
s3_bucket,
|
||||||
Ok(()) => {
|
s3_client_id,
|
||||||
println!("Salesforce authentication URL saved successfully");
|
s3_client_secret,
|
||||||
return true;
|
s3_endpoint,
|
||||||
}
|
s3_region,
|
||||||
Err(e) => {
|
sfdx_auth_url,
|
||||||
error!("Error saving Salesforce authentication URL: {:#}", e);
|
) {
|
||||||
std::process::exit(1);
|
Ok(()) => {
|
||||||
}
|
println!("Configuration settings updated successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error setting configuration: {:#}", e);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*updated
|
}
|
||||||
}
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
pub(crate) fn run(config_file: &str,
|
||||||
|
admin_email: &Option<String>,
|
||||||
|
s3_bucket: &Option<String>,
|
||||||
|
s3_client_id: &Option<String>,
|
||||||
|
s3_client_secret: &Option<String>,
|
||||||
|
s3_endpoint: &Option<String>,
|
||||||
|
s3_region: &Option<String>,
|
||||||
|
sfdx_auth_url: &Option<String>,) -> Result<()> {
|
||||||
|
|
||||||
|
let mut updated = false;
|
||||||
|
|
||||||
|
let mut app_config = crate::config::load(config_file)
|
||||||
|
.context("Failed to load application configuration")?;
|
||||||
|
|
||||||
|
|
||||||
|
if let Some(admin_email) = admin_email {
|
||||||
|
app_config.admin_email = Some(admin_email.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bucket) = s3_bucket {
|
||||||
|
app_config.s3_bucket = Some(bucket.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s3_client_id) = s3_client_id {
|
||||||
|
app_config.s3_client_id = Some(s3_client_id.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s3_client_secret) = s3_client_secret {
|
||||||
|
app_config.s3_client_secret = Some(s3_client_secret.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s3_endpoint) = s3_endpoint {
|
||||||
|
app_config.s3_endpoint = Some(s3_endpoint.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s3_region) = s3_region {
|
||||||
|
app_config.s3_region = Some(s3_region.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = sfdx_auth_url {
|
||||||
|
app_config.sfdx_auth_url = Some(url.to_string());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::config::save(config_file, &app_config)
|
||||||
|
.context("Failed to save configuration")?;
|
||||||
|
|
||||||
|
if !updated {
|
||||||
|
println!("Expected at least one settings flag. Use --help for more information");
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+252
-1
@@ -13,19 +13,79 @@ const CONFIG_KEY_ENV: &str = "IMOJE_CONFIG_KEY";
|
|||||||
const CONFIG_DIR: &str = "config/imoje";
|
const CONFIG_DIR: &str = "config/imoje";
|
||||||
const NONCE_LEN: usize = 12;
|
const NONCE_LEN: usize = 12;
|
||||||
|
|
||||||
|
const NOT_SET: &'static str = "<not set>";
|
||||||
|
const SECRET_HIDDEN: &'static str = "***** secret hidden *****";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
|
pub admin_email: Option<String>,
|
||||||
|
pub s3_bucket: Option<String>,
|
||||||
|
pub s3_client_id: Option<String>,
|
||||||
|
pub s3_client_secret: Option<String>,
|
||||||
|
pub s3_endpoint: Option<String>,
|
||||||
|
pub s3_region: Option<String>,
|
||||||
pub sfdx_auth_url: Option<String>,
|
pub sfdx_auth_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
admin_email: None,
|
||||||
|
s3_bucket: None,
|
||||||
|
s3_client_id: None,
|
||||||
|
s3_client_secret: None,
|
||||||
|
s3_endpoint: None,
|
||||||
|
s3_region: None,
|
||||||
sfdx_auth_url: None,
|
sfdx_auth_url: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list(config_file: &str) -> Result<()> {
|
||||||
|
let app_config = load(config_file).context("Failed to load application configuration")?;
|
||||||
|
|
||||||
|
println!("Configuration: {}", config_file);
|
||||||
|
println!("----------------------------------------------------");
|
||||||
|
println!(
|
||||||
|
"admin_email = {}",
|
||||||
|
app_config.admin_email.as_deref().unwrap_or(NOT_SET)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"s3_bucket = {}",
|
||||||
|
app_config.s3_bucket.as_deref().unwrap_or(NOT_SET)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"s3_client_id = {}",
|
||||||
|
app_config.s3_client_id.as_deref().unwrap_or(NOT_SET)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"s3_client_secret = {}",
|
||||||
|
match app_config.s3_client_secret {
|
||||||
|
Some(_) => SECRET_HIDDEN,
|
||||||
|
None => NOT_SET,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"s3_endpoint = {}",
|
||||||
|
app_config.s3_endpoint.as_deref().unwrap_or(NOT_SET)
|
||||||
|
);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"s3_region = {}",
|
||||||
|
app_config.s3_region.as_deref().unwrap_or(NOT_SET)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"sfdx_auth_url = {}",
|
||||||
|
match app_config.sfdx_auth_url {
|
||||||
|
Some(_) => SECRET_HIDDEN,
|
||||||
|
None => NOT_SET,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(config_file: &str) -> Result<AppConfig> {
|
pub fn load(config_file: &str) -> Result<AppConfig> {
|
||||||
let config_path = config_path(config_file)?;
|
let config_path = config_path(config_file)?;
|
||||||
|
|
||||||
@@ -122,7 +182,7 @@ fn decrypt_config(encrypted_config: &str) -> Result<AppConfig> {
|
|||||||
fn config_key() -> Result<[u8; 32]> {
|
fn config_key() -> Result<[u8; 32]> {
|
||||||
let encoded_key = std::env::var(CONFIG_KEY_ENV)
|
let encoded_key = std::env::var(CONFIG_KEY_ENV)
|
||||||
.unwrap_or("ihDlaCqJqUGJ1am4dT9k/BHRG2UT+iK9IfYcPe0Db/I=".to_string());
|
.unwrap_or("ihDlaCqJqUGJ1am4dT9k/BHRG2UT+iK9IfYcPe0Db/I=".to_string());
|
||||||
// .with_context(|| format!("Missing required environment variable {CONFIG_KEY_ENV}"))?;
|
// .with_context(|| format!("Missing required environment variable {CONFIG_KEY_ENV}"))?;
|
||||||
|
|
||||||
let key = BASE64
|
let key = BASE64
|
||||||
.decode(encoded_key.trim())
|
.decode(encoded_key.trim())
|
||||||
@@ -130,4 +190,195 @@ fn config_key() -> Result<[u8; 32]> {
|
|||||||
|
|
||||||
key.try_into()
|
key.try_into()
|
||||||
.map_err(|_| anyhow!("{CONFIG_KEY_ENV} must decode to exactly 32 bytes"))
|
.map_err(|_| anyhow!("{CONFIG_KEY_ENV} must decode to exactly 32 bytes"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
const TEST_KEY: &str = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";
|
||||||
|
|
||||||
|
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||||
|
ENV_LOCK
|
||||||
|
.get_or_init(|| Mutex::new(()))
|
||||||
|
.lock()
|
||||||
|
.expect("environment lock should not be poisoned")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_temp_home() -> PathBuf {
|
||||||
|
let mut path = std::env::temp_dir();
|
||||||
|
path.push(format!(
|
||||||
|
"imojefs-config-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("system time should be after UNIX_EPOCH")
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_test_key() {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(CONFIG_KEY_ENV, TEST_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_home_dir(path: &Path) {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("HOME", path);
|
||||||
|
std::env::set_var("USERPROFILE", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_config_default_has_no_values() {
|
||||||
|
let config = AppConfig::default();
|
||||||
|
|
||||||
|
assert_eq!(config.sfdx_auth_url, None);
|
||||||
|
assert_eq!(config.s3_bucket, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_key_accepts_base64_encoded_32_byte_key() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
|
||||||
|
let key = config_key().expect("test key should be valid");
|
||||||
|
|
||||||
|
assert_eq!(key, *b"0123456789abcdef0123456789abcdef");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_key_rejects_invalid_base64() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(CONFIG_KEY_ENV, "not valid base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = config_key().expect_err("invalid base64 should fail");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("IMOJE_CONFIG_KEY must be a base64-encoded 32-byte key")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_key_rejects_wrong_key_length() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(CONFIG_KEY_ENV, BASE64.encode(b"too short"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = config_key().expect_err("short key should fail");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("IMOJE_CONFIG_KEY must decode to exactly 32 bytes")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypt_config_produces_base64_payload_with_nonce_and_ciphertext() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
|
||||||
|
let encrypted = encrypt_config("sfdx_auth_url = 'auth-url'")
|
||||||
|
.expect("configuration should encrypt");
|
||||||
|
let encrypted_payload = BASE64
|
||||||
|
.decode(encrypted)
|
||||||
|
.expect("encrypted configuration should be base64");
|
||||||
|
|
||||||
|
assert!(encrypted_payload.len() > NONCE_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrypt_config_round_trips_encrypted_configuration() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
|
||||||
|
let encrypted = encrypt_config(
|
||||||
|
r#"
|
||||||
|
sfdx_auth_url = "force://example"
|
||||||
|
s3_bucket = "example-bucket"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("configuration should encrypt");
|
||||||
|
|
||||||
|
let decrypted = decrypt_config(&encrypted).expect("configuration should decrypt");
|
||||||
|
|
||||||
|
assert_eq!(decrypted.sfdx_auth_url.as_deref(), Some("force://example"));
|
||||||
|
assert_eq!(decrypted.s3_bucket.as_deref(), Some("example-bucket"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrypt_config_rejects_non_base64_input() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
|
||||||
|
let error = decrypt_config("not valid base64").expect_err("invalid base64 should fail");
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("Configuration file is not valid base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrypt_config_rejects_payload_without_ciphertext() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
let encrypted = BASE64.encode([0_u8; NONCE_LEN]);
|
||||||
|
|
||||||
|
let error = decrypt_config(&encrypted).expect_err("short payload should fail");
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("Configuration file is too short"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_and_load_round_trip_configuration_file() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
let temp_home = unique_temp_home();
|
||||||
|
set_home_dir(&temp_home);
|
||||||
|
|
||||||
|
let config = AppConfig {
|
||||||
|
admin_email: None,
|
||||||
|
sfdx_auth_url: Some("force://saved".to_string()),
|
||||||
|
s3_bucket: Some("saved-bucket".to_string()),
|
||||||
|
s3_client_id: None,
|
||||||
|
s3_client_secret: None,
|
||||||
|
s3_endpoint: None,
|
||||||
|
s3_region: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
save("round-trip", &config).expect("configuration should save");
|
||||||
|
let loaded = load("round-trip").expect("configuration should load");
|
||||||
|
|
||||||
|
assert_eq!(loaded.sfdx_auth_url, config.sfdx_auth_url);
|
||||||
|
assert_eq!(loaded.s3_bucket, config.s3_bucket);
|
||||||
|
|
||||||
|
fs::remove_dir_all(temp_home).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_creates_default_configuration_when_file_is_missing() {
|
||||||
|
let _lock = env_lock();
|
||||||
|
set_test_key();
|
||||||
|
let temp_home = unique_temp_home();
|
||||||
|
set_home_dir(&temp_home);
|
||||||
|
|
||||||
|
let loaded = load("missing").expect("default configuration should load");
|
||||||
|
let expected_path = temp_home.join(CONFIG_DIR).join("missing.conf");
|
||||||
|
|
||||||
|
assert_eq!(loaded.sfdx_auth_url, None);
|
||||||
|
assert_eq!(loaded.s3_bucket, None);
|
||||||
|
assert!(expected_path.exists());
|
||||||
|
|
||||||
|
fs::remove_dir_all(temp_home).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+6
-7
@@ -1,9 +1,9 @@
|
|||||||
//! # IMoje File Sync
|
//! # IMoje CLI
|
||||||
//!
|
//!
|
||||||
//! Syncs the contents of a Salesforce File to S3
|
//! CLI features;
|
||||||
|
//! - **file-sync** Syncs the contents of a Salesforce File to S3
|
||||||
use anyhow::Result;
|
//! - **set** Sets the value of a configuration
|
||||||
use clap::Parser;
|
//! - **list** Lists the contents of a configuration file`
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ mod file_sync;
|
|||||||
|
|
||||||
pub static APP_CONFIG: OnceLock<config::AppConfig> = OnceLock::new();
|
pub static APP_CONFIG: OnceLock<config::AppConfig> = OnceLock::new();
|
||||||
|
|
||||||
|
|
||||||
#[::tokio::main]
|
#[::tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
cli::run();
|
cli::run();
|
||||||
|
|||||||
Reference in New Issue
Block a user