From 5d5b0ae89aeb37d0b1d6bd29cb17cde45c58a407 Mon Sep 17 00:00:00 2001 From: Maaz Khokhar Date: Thu, 16 Jul 2026 11:23:35 -0500 Subject: [PATCH] switched to using the Store trait also added another dependency for expanding ~ paths --- Cargo.lock | 48 +++++++ crates/cli/Cargo.toml | 7 +- crates/cli/src/lib.rs | 240 ++------------------------------- crates/cli/src/main.rs | 62 ++++----- crates/cli/src/store/mod.rs | 34 ++++- crates/cli/src/store/sqlite.rs | 220 ++++++++++++++++++++++++++++++ crates/cli/tests/lib_tests.rs | 112 +++++++-------- 7 files changed, 398 insertions(+), 325 deletions(-) create mode 100644 crates/cli/src/store/sqlite.rs diff --git a/Cargo.lock b/Cargo.lock index b057abe..96e03b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,7 @@ dependencies = [ "colored", "crypto", "diesel", + "dirs", "dotenvy", "hex", "keyring", @@ -653,6 +654,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1024,6 +1046,15 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.37.0" @@ -1171,6 +1202,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -1322,6 +1359,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.13.0" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index e2e5f20..87efbd7 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -13,17 +13,18 @@ diesel = { version = "2.2.0", features = [ "chrono", ] } clap = { version = "4.6.1", features = ["derive", "cargo"] } +serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.52.3", features = ["full"] } +uuid = { version = "1.23.5", features = ["v4"] } crypto = { path = "../crypto" } keyring = "4.1.4" colored = "3.1.1" base64 = "0.22.1" chrono = "0.4.45" dotenvy = "0.15" -uuid = { version = "1.23.5", features = ["v4"] } -hex = "0.4" -serde = { version = "1.0.228", features = ["derive"] } toml = "1.1.2" +hex = "0.4" +dirs = "6" [[bin]] name = "harbor" diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 6c08f0b..26ddbfa 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -10,9 +10,10 @@ use keyring::Entry; use serde::Deserialize; use std::error::Error; use std::io::{self, Write}; +use std::path::PathBuf; pub mod config; mod db; -mod store; +pub mod store; #[derive( Debug, Clone, Copy, PartialEq, Eq, Deserialize, diesel::AsExpression, diesel::FromSqlRow, @@ -175,30 +176,6 @@ pub fn parse_secret_pairs(raw: &[String]) -> Result, Box String { - let mut command = String::from("doppler secrets set"); - - if secrets.is_empty() { - return command; - } - - command.push_str(" \\"); - - for (index, (key, value)) in secrets.iter().enumerate() { - command.push_str("\n "); - command.push_str(key); - command.push_str("=\""); - command.push_str(value); - command.push('"'); - - if index + 1 != secrets.len() { - command.push_str(" \\"); - } - } - - command -} - #[derive(Subcommand)] pub enum ProjectCommands { List {}, @@ -206,217 +183,16 @@ pub enum ProjectCommands { Delete { name: String }, } -const DB_URL: &str = "/Users/Maaz/Documents/Git/harbor/crates/cli/harbor.db"; - -pub mod interactions { - use super::DB_URL; - use super::db::{establish_connection, models::Project}; - use crate::Environment; - use crate::db::schema::projects::id; - use diesel::dsl::{insert_into, update}; - use diesel::result::{DatabaseErrorKind, Error as DieselError}; - use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; - use std::error::Error; - use uuid::Uuid; - - type Result = std::result::Result>; - - fn db_url() -> String { - std::env::var("DATABASE_URL").unwrap_or_else(|_| DB_URL.to_string()) - } - - fn construct_error(message: &str) -> Result<()> { - Err(Box::new(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - message.to_string(), - ))) - } - - pub fn project_exists(proj_name: &str) -> Result { - use super::db::schema::projects::dsl::{name, projects}; - let mut conn = establish_connection(db_url()); - - let existing_project = projects - .filter(name.eq(proj_name)) - .select(Project::as_select()) - .first::(&mut conn) - .optional()?; - - Ok(existing_project.is_some()) - } - - pub fn create_project(proj_name: &str) -> Result<()> { - use super::db::schema::projects::dsl::{created_at, id, name, projects}; - let mut conn = establish_connection(db_url()); - - if project_exists(proj_name)? { - return construct_error("Project already exists"); - } - - let proj_id = Uuid::new_v4().to_string(); - - match insert_into(projects) - .values(( - id.eq(proj_id), - name.eq(proj_name), - created_at.eq(chrono::Utc::now().naive_utc()), - )) - .execute(&mut conn) - { - Ok(_) => Ok(()), - Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => { - construct_error("Project already exists") - } - Err(err) => Err(Box::new(err)), +pub fn expand_tilde(path: &str) -> String { + if let Some(stripped) = path.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(stripped).to_string_lossy().to_string(); } } - pub fn delete_project(proj_name: &str) -> Result<()> { - use super::db::schema::projects::dsl::{name, projects}; - let mut conn = establish_connection(db_url()); - - if !project_exists(proj_name)? { - return construct_error("Project does not exist"); - } - - diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?; - - Ok(()) - } - - pub fn get_projects() -> Result> { - use super::db::schema::projects::dsl::projects; - let mut conn = establish_connection(db_url()); - - let results = projects - .select(Project::as_select()) - .load::(&mut conn)?; - - Ok(results) - } - - pub fn secret_exists( - proj_id: &str, - secret_name: &str, - environment: Environment, - ) -> Result { - use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets}; - let mut conn = establish_connection(db_url()); - - let existing_secret = secrets - .filter(project_id.eq(proj_id)) - .filter(name.eq(secret_name)) - .filter(config.eq(environment)) - .select(id) - .first::(&mut conn) - .optional()?; - - Ok(existing_secret.is_some()) - } - - pub fn get_project_id(proj_name: &str) -> Result { - use super::db::schema::projects::dsl::{id, name, projects}; - let mut conn = establish_connection(db_url()); - - let project_id = projects - .filter(name.eq(proj_name)) - .select(id) - .first::(&mut conn) - .optional()?; - - match project_id { - Some(pid) => Ok(pid), - None => Err(Box::new(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Project not found", - ))), - } - } - - pub fn set_secret( - proj_id: &str, - secret_name: &str, - secret_value: Vec, - conf: Environment, - non: crypto::Nonce, - ) -> Result<()> { - use super::db::schema::secrets::dsl::{ - config, created_at, id, name, nonce, project_id, secret, secrets, - }; - let mut conn = establish_connection(db_url()); - - if secret_exists(proj_id, secret_name, conf)? { - update( - secrets - .filter(project_id.eq(proj_id)) - .filter(name.eq(secret_name)) - .filter(config.eq(conf)), - ) - .set((secret.eq(secret_value), nonce.eq(non.to_vec()))) - .execute(&mut conn)?; - } else { - insert_into(secrets) - .values(( - id.eq(Uuid::new_v4().to_string()), - name.eq(secret_name), - secret.eq(secret_value), - project_id.eq(proj_id), - config.eq(conf), - nonce.eq(non.to_vec()), - created_at.eq(chrono::Utc::now().naive_utc()), - )) - .execute(&mut conn)?; - } - - Ok(()) - } - - pub fn get_project_secrets( - proj_id: &str, - environment: Environment, - ) -> Result, Vec)>> { - use super::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets}; - let mut conn = establish_connection(db_url()); - - let results = secrets - .filter(project_id.eq(proj_id)) - .filter(config.eq(environment)) - .select((name, secret, nonce)) - .load::<(String, Vec, Vec)>(&mut conn)?; - - Ok(results) - } - - pub fn delete_secret(proj_id: &str, secret_name: &str, environment: Environment) -> Result<()> { - use super::db::schema::secrets::dsl::{config, id, name, project_id, secrets}; - let mut conn = establish_connection(db_url()); - - let existing_secret = secrets - .filter(project_id.eq(proj_id)) - .filter(name.eq(secret_name)) - .filter(config.eq(environment)) - .select(id) - .first::(&mut conn) - .optional()?; - - if existing_secret.is_none() { - return Err(Box::new(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Secret does not exist", - ))); - } - - diesel::delete( - secrets - .filter(project_id.eq(proj_id)) - .filter(name.eq(secret_name)) - .filter(config.eq(environment)), - ) - .execute(&mut conn)?; - - Ok(()) - } + path.to_string() } + pub fn gen_or_get_key() -> Result> { let entry = Entry::new("harbor", "encryption-key")?; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 68feb9e..56b003e 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -6,14 +6,11 @@ use clap::crate_version; use cli::Cli; use cli::Environment; use cli::config::{Config, ConfigError}; +use cli::expand_tilde; use cli::gen_or_get_key; use cli::get_input; -use cli::interactions::delete_secret; -use cli::interactions::get_project_id; -use cli::interactions::get_project_secrets; -use cli::interactions::get_projects; -use cli::interactions::secret_exists; -use cli::interactions::set_secret; +use cli::store::Store; +use cli::store::sqlite::SqliteStore; use colored::*; use crypto::encrypt; @@ -47,8 +44,10 @@ fn main() { } }; + let db_path = expand_tilde("~/.local/share/harbor/database.db"); let config_path = root.join(".harbor.toml"); let has_config = config_path.exists(); + let store = SqliteStore::new(db_path); match cli.command { Some(c) => match c { @@ -69,7 +68,7 @@ fn main() { None => config.default_env.into(), }; - let project = match get_project_id(&config.name) { + let project = match store.get_project_id(&config.name) { Ok(p) => p, Err(e) => { print_error(format!( @@ -80,7 +79,7 @@ fn main() { } }; - let secrets = match get_project_secrets(&project, environment) { + let secrets = match store.get_project_secrets(&project, environment) { Ok(s) => s, Err(e) => { print_error(format!("Error getting secrets: {}", e)); @@ -156,7 +155,7 @@ fn main() { process::exit(1); } - let project = match get_project_id(&config.name) { + let project = match store.get_project_id(&config.name) { Ok(p) => p, Err(e) => { print_error(format!( @@ -168,7 +167,7 @@ fn main() { }; for pair in pairs { - let existed = match secret_exists(&project, &pair.0, environment) { + let existed = match store.secret_exists(&project, &pair.0, environment) { Ok(exists) => exists, Err(e) => { print_error(format!( @@ -180,11 +179,8 @@ fn main() { }; if existed { let confirmation = match get_input( - format!( - "Secret '{}' already exists. Overwrite? (y/N)", - pair.0 - ) - .yellow(), + format!("Secret '{}' already exists. Overwrite? (y/N)", pair.0) + .yellow(), ':', false, ) { @@ -198,12 +194,8 @@ fn main() { if confirmation.to_lowercase() != "y" { println!( "{}", - format!( - "Skipped secret '{}' in {}.", - pair.0, - environment.as_str() - ) - .dimmed() + format!("Skipped secret '{}' in {}.", pair.0, environment.as_str()) + .dimmed() ); continue; } @@ -226,7 +218,7 @@ fn main() { process::exit(1); } }; - match set_secret(&project, &pair.0, encrypted, environment, nonce) { + match store.set_secret(&project, &pair.0, encrypted, environment, nonce) { Ok(()) => { let verb = if existed { "Updated" } else { "Set" }; println!( @@ -263,7 +255,7 @@ fn main() { }, None => config.default_env.into(), }; - let proj_id = match get_project_id(&config.name) { + let proj_id = match store.get_project_id(&config.name) { Ok(p) => p, Err(e) => { print_error(format!( @@ -291,7 +283,7 @@ fn main() { } for key in cleaned { - match delete_secret(&proj_id, &key, environment) { + match store.delete_secret(&proj_id, &key, environment) { Ok(()) => { println!("Deleted secret for key '{}'", key); } @@ -314,7 +306,7 @@ fn main() { }, None => config.default_env.into(), }; - let proj_id = match get_project_id(&config.name) { + let proj_id = match store.get_project_id(&config.name) { Ok(p) => p, Err(e) => { print_error(format!( @@ -334,7 +326,7 @@ fn main() { cmd.args(&after[1..]); } - let secrets = match get_project_secrets(&proj_id, environment) { + let secrets = match store.get_project_secrets(&proj_id, environment) { Ok(s) => s, Err(e) => { print_error(format!("Error getting secrets: {}", e)); @@ -392,7 +384,7 @@ fn main() { Commands::List {} => { // Get all secrets, then use colorize and split them between environments, and print them out in a table format. let config = require_config(&root); - let proj_id = match get_project_id(&config.name) { + let proj_id = match store.get_project_id(&config.name) { Ok(p) => p, Err(e) => { print_error(format!( @@ -403,7 +395,7 @@ fn main() { } }; - let secrets = match get_project_secrets(&proj_id, Environment::Dev) { + let secrets = match store.get_project_secrets(&proj_id, Environment::Dev) { Ok(s) => s, Err(e) => { print_error(format!("Error getting secrets: {}", e)); @@ -429,7 +421,7 @@ fn main() { } Commands::Project { command } => match command { ProjectCommands::List {} => { - let projects = match get_projects() { + let projects = match store.get_projects() { Ok(projects) => projects, Err(e) => { print_error(format!("Error getting projects: {}", e)); @@ -449,7 +441,7 @@ fn main() { } }; - match cli::interactions::create_project(&project_name) { + match store.create_project(&project_name) { Ok(()) => { println!("{}", "Project created successfully.".bright_green().bold()) } @@ -461,7 +453,7 @@ fn main() { } ProjectCommands::Delete { name } => { // We'll check if the project exists before prompting for confirmation - let exists = match cli::interactions::project_exists(&name) { + let exists = match store.project_exists(&name) { Ok(exists) => exists, Err(e) => { print_error(format!("Error checking if project exists: {}", e)); @@ -492,7 +484,7 @@ fn main() { }; if confirmation.to_lowercase() == "y" { - match cli::interactions::delete_project(&name) { + match store.delete_project(&name) { Ok(()) => { println!( "{}", @@ -525,7 +517,7 @@ fn main() { }, None => config.default_env.into(), }; - let proj_id = match get_project_id(&config.name) { + let proj_id = match store.get_project_id(&config.name) { Ok(p) => p, Err(e) => { print_error(format!( @@ -551,7 +543,7 @@ fn main() { cmd.arg(command); } - let secrets = match get_project_secrets(&proj_id, environment) { + let secrets = match store.get_project_secrets(&proj_id, environment) { Ok(s) => s, Err(e) => { print_error(format!("Error getting secrets: {}", e)); @@ -618,7 +610,7 @@ fn main() { use std::process; // Setup will get all projects and prompt the user to select one, // then create a .harbor.toml file in the current directory with the selected project name. - let projects = match get_projects() { + let projects = match store.get_projects() { Ok(projects) => projects, Err(e) => { print_error(format!("Error getting projects: {}", e)); diff --git a/crates/cli/src/store/mod.rs b/crates/cli/src/store/mod.rs index 8cf3d82..1e59804 100644 --- a/crates/cli/src/store/mod.rs +++ b/crates/cli/src/store/mod.rs @@ -3,5 +3,35 @@ // With this trait, we can implement other savers like Postgres, MySQL, etc. // Or even third party services like Hashicorp Vault, AWS Secrets Manager, etc. -// For now, I'll just get an MVP in place. -pub trait Store {} +use std::error::Error; + +pub mod sqlite; + +use crate::{Environment, db::models::Project}; + +type Result = std::result::Result>; + +pub trait Store { + fn project_exists(&self, proj_name: &str) -> Result; + fn create_project(&self, proj_name: &str) -> Result<()>; + fn delete_project(&self, proj_name: &str) -> Result<()>; + fn get_projects(&self) -> Result>; + fn get_project_id(&self, proj_name: &str) -> Result; + fn secret_exists(&self, proj_id: &str, secret_name: &str, environment: Environment) + -> Result; + fn set_secret( + &self, + proj_id: &str, + secret_name: &str, + secret_value: Vec, + conf: Environment, + non: crypto::Nonce, + ) -> Result<()>; + fn get_project_secrets( + &self, + proj_id: &str, + environment: Environment, + ) -> Result, Vec)>>; + fn delete_secret(&self, proj_id: &str, secret_name: &str, environment: Environment) + -> Result<()>; +} diff --git a/crates/cli/src/store/sqlite.rs b/crates/cli/src/store/sqlite.rs new file mode 100644 index 0000000..c56428f --- /dev/null +++ b/crates/cli/src/store/sqlite.rs @@ -0,0 +1,220 @@ +use crate::db::models::Project; +use crate::store::Store; +use diesel::dsl::{insert_into, update}; +use diesel::result::{DatabaseErrorKind, Error as DieselError}; +use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; +use uuid::Uuid; + +pub struct SqliteStore { + database_url: String, +} + +impl SqliteStore { + pub fn new(database_url: String) -> Self { + SqliteStore { database_url } + } + + fn db_url(&self) -> String { + if self.database_url.is_empty() { + std::env::var("DATABASE_URL").expect("NO DATABASE URL WAS SET") + } else { + self.database_url.clone() + } + } +} + +fn construct_error(message: &str) -> super::Result<()> { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + message.to_string(), + ))) +} + +impl Store for SqliteStore { + fn create_project(&self, proj_name: &str) -> super::Result<()> { + use crate::db::schema::projects::dsl::{created_at, id, name, projects}; + let mut conn = crate::db::establish_connection(self.db_url()); + + if self.project_exists(proj_name)? { + return construct_error("Project already exists"); + } + + let proj_id = Uuid::new_v4().to_string(); + + match insert_into(projects) + .values(( + id.eq(proj_id), + name.eq(proj_name), + created_at.eq(chrono::Utc::now().naive_utc()), + )) + .execute(&mut conn) + { + Ok(_) => Ok(()), + Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => { + construct_error("Project already exists") + } + Err(err) => Err(Box::new(err)), + } + } + + fn delete_project(&self, proj_name: &str) -> super::Result<()> { + use crate::db::schema::projects::dsl::{name, projects}; + let mut conn = crate::db::establish_connection(self.db_url()); + + if !self.project_exists(proj_name)? { + return construct_error("Project does not exist"); + } + + diesel::delete(projects.filter(name.eq(proj_name))).execute(&mut conn)?; + + Ok(()) + } + fn delete_secret( + &self, + proj_id: &str, + secret_name: &str, + environment: crate::Environment, + ) -> super::Result<()> { + use crate::db::schema::secrets::dsl::{config, id, name, project_id, secrets}; + let mut conn = crate::db::establish_connection(self.db_url()); + + let existing_secret = secrets + .filter(project_id.eq(proj_id)) + .filter(name.eq(secret_name)) + .filter(config.eq(environment)) + .select(id) + .first::(&mut conn) + .optional()?; + + if existing_secret.is_none() { + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Secret does not exist", + ))); + } + + diesel::delete( + secrets + .filter(project_id.eq(proj_id)) + .filter(name.eq(secret_name)) + .filter(config.eq(environment)), + ) + .execute(&mut conn)?; + + Ok(()) + } + fn get_project_id(&self, proj_name: &str) -> super::Result { + use crate::db::schema::projects::dsl::{id, name, projects}; + let mut conn = crate::db::establish_connection(self.db_url()); + + let project_id = projects + .filter(name.eq(proj_name)) + .select(id) + .first::(&mut conn) + .optional()?; + + match project_id { + Some(pid) => Ok(pid), + None => Err(Box::new(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Project not found", + ))), + } + } + fn get_project_secrets( + &self, + proj_id: &str, + environment: crate::Environment, + ) -> super::Result, Vec)>> { + use crate::db::schema::secrets::dsl::{config, name, nonce, project_id, secret, secrets}; + let mut conn = crate::db::establish_connection(self.db_url()); + + let results = secrets + .filter(project_id.eq(proj_id)) + .filter(config.eq(environment)) + .select((name, secret, nonce)) + .load::<(String, Vec, Vec)>(&mut conn)?; + + Ok(results) + } + fn get_projects(&self) -> super::Result> { + use crate::db::schema::projects::dsl::projects; + let mut conn = crate::db::establish_connection(self.db_url()); + + let results = projects + .select(Project::as_select()) + .load::(&mut conn)?; + + Ok(results) + } + + fn project_exists(&self, proj_name: &str) -> super::Result { + use crate::db::schema::projects::dsl::{name, projects}; + let mut conn = crate::db::establish_connection(self.db_url()); + + let existing_project = projects + .filter(name.eq(proj_name)) + .select(Project::as_select()) + .first::(&mut conn) + .optional()?; + + Ok(existing_project.is_some()) + } + fn secret_exists( + &self, + proj_id: &str, + secret_name: &str, + environment: crate::Environment, + ) -> super::Result { + use crate::db::schema::secrets::dsl::{config, id, name, project_id, secrets}; + let mut conn = crate::db::establish_connection(self.db_url()); + + let existing_secret = secrets + .filter(project_id.eq(proj_id)) + .filter(name.eq(secret_name)) + .filter(config.eq(environment)) + .select(id) + .first::(&mut conn) + .optional()?; + + Ok(existing_secret.is_some()) + } + fn set_secret( + &self, + proj_id: &str, + secret_name: &str, + secret_value: Vec, + conf: crate::Environment, + non: crypto::Nonce, + ) -> super::Result<()> { + use crate::db::schema::secrets::dsl::{ + config, created_at, id, name, nonce, project_id, secret, secrets, + }; + let mut conn = crate::db::establish_connection(self.db_url()); + + if self.secret_exists(proj_id, secret_name, conf)? { + update( + secrets + .filter(project_id.eq(proj_id)) + .filter(name.eq(secret_name)) + .filter(config.eq(conf)), + ) + .set((secret.eq(secret_value), nonce.eq(non.to_vec()))) + .execute(&mut conn)?; + } else { + insert_into(secrets) + .values(( + id.eq(Uuid::new_v4().to_string()), + name.eq(secret_name), + secret.eq(secret_value), + project_id.eq(proj_id), + config.eq(conf), + nonce.eq(non.to_vec()), + created_at.eq(chrono::Utc::now().naive_utc()), + )) + .execute(&mut conn)?; + } + + Ok(()) + } +} diff --git a/crates/cli/tests/lib_tests.rs b/crates/cli/tests/lib_tests.rs index 26c4401..bcc71aa 100644 --- a/crates/cli/tests/lib_tests.rs +++ b/crates/cli/tests/lib_tests.rs @@ -1,10 +1,8 @@ use cli::Environment; use cli::config::Config; -use cli::interactions::{ - create_project, delete_project, delete_secret, get_project_id, get_project_secrets, - get_projects, project_exists, secret_exists, set_secret, -}; -use cli::{format_doppler_set_command, parse_secret_pairs}; +use cli::parse_secret_pairs; +use cli::store::Store; +use cli::store::sqlite::SqliteStore; use diesel::connection::SimpleConnection; use diesel::{Connection, SqliteConnection}; use std::path::PathBuf; @@ -91,22 +89,6 @@ fn parse_secret_pairs_handles_valid_and_invalid_input() { assert_eq!(err.to_string(), "Secret key cannot be empty"); } -#[test] -fn format_doppler_set_command_outputs_expected_format() { - let empty = format_doppler_set_command(&[]); - assert_eq!(empty, "doppler secrets set"); - - let one = format_doppler_set_command(&[("API_KEY".to_string(), "abc".to_string())]); - assert_eq!(one, "doppler secrets set \\\n API_KEY=\"abc\""); - - let many = format_doppler_set_command(&[ - ("A".to_string(), "1".to_string()), - ("B".to_string(), "2".to_string()), - ]); - - assert_eq!(many, "doppler secrets set \\\n A=\"1\" \\\n B=\"2\""); -} - #[test] fn config_reads_valid_file_and_reports_errors() { let temp_root = std::env::temp_dir().join(format!("harbor_config_{}", Uuid::new_v4())); @@ -140,55 +122,76 @@ fn config_reads_valid_file_and_reports_errors() { fn project_and_secret_lifecycle() { let _guard = test_lock().lock().expect("lock tests"); let db_path = setup_test_db(); + let store = SqliteStore::new(String::new()); - assert!(!project_exists("alpha").expect("project exists")); + assert!(!store.project_exists("alpha").expect("project exists")); - create_project("alpha").expect("create project"); - assert!(project_exists("alpha").expect("project exists")); + store.create_project("alpha").expect("create project"); + assert!(store.project_exists("alpha").expect("project exists")); - let project_id = get_project_id("alpha").expect("get project id"); - let projects = get_projects().expect("get projects"); + let project_id = store.get_project_id("alpha").expect("get project id"); + let projects = store.get_projects().expect("get projects"); assert_eq!(projects.len(), 1); assert_eq!(projects[0].name, "alpha"); - assert!(!secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists")); + assert!( + !store + .secret_exists(&project_id, "API_KEY", Environment::Dev) + .expect("secret exists") + ); let nonce = crypto::helper::gen_nonce(); - set_secret( - &project_id, - "API_KEY", - b"first".to_vec(), - Environment::Dev, - nonce, - ) - .expect("set secret"); + store + .set_secret( + &project_id, + "API_KEY", + b"first".to_vec(), + Environment::Dev, + nonce, + ) + .expect("set secret"); - assert!(secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists")); + assert!( + store + .secret_exists(&project_id, "API_KEY", Environment::Dev) + .expect("secret exists") + ); - let secrets = get_project_secrets(&project_id, Environment::Dev).expect("get secrets"); + let secrets = store + .get_project_secrets(&project_id, Environment::Dev) + .expect("get secrets"); assert_eq!(secrets.len(), 1); assert_eq!(secrets[0].0, "API_KEY"); assert_eq!(secrets[0].1, b"first".to_vec()); let nonce = crypto::helper::gen_nonce(); - set_secret( - &project_id, - "API_KEY", - b"second".to_vec(), - Environment::Dev, - nonce, - ) - .expect("update secret"); + store + .set_secret( + &project_id, + "API_KEY", + b"second".to_vec(), + Environment::Dev, + nonce, + ) + .expect("update secret"); - let updated = get_project_secrets(&project_id, Environment::Dev).expect("get secrets"); + let updated = store + .get_project_secrets(&project_id, Environment::Dev) + .expect("get secrets"); assert_eq!(updated.len(), 1); assert_eq!(updated[0].1, b"second".to_vec()); - delete_secret(&project_id, "API_KEY", Environment::Dev).expect("delete secret"); - assert!(!secret_exists(&project_id, "API_KEY", Environment::Dev).expect("secret exists")); + store + .delete_secret(&project_id, "API_KEY", Environment::Dev) + .expect("delete secret"); + assert!( + !store + .secret_exists(&project_id, "API_KEY", Environment::Dev) + .expect("secret exists") + ); - delete_project("alpha").expect("delete project"); - assert!(!project_exists("alpha").expect("project exists")); + store.delete_project("alpha").expect("delete project"); + assert!(!store.project_exists("alpha").expect("project exists")); cleanup_test_db(db_path); } @@ -197,15 +200,18 @@ fn project_and_secret_lifecycle() { fn errors_for_missing_project_or_secret() { let _guard = test_lock().lock().expect("lock tests"); let db_path = setup_test_db(); + let store = SqliteStore::new(String::new()); - let err = delete_project("missing").unwrap_err(); + let err = store.delete_project("missing").unwrap_err(); assert_eq!(err.to_string(), "Project does not exist"); - let err = get_project_id("missing").unwrap_err(); + let err = store.get_project_id("missing").unwrap_err(); assert_eq!(err.to_string(), "Project not found"); let missing_project_id = Uuid::new_v4().to_string(); - let err = delete_secret(&missing_project_id, "missing", Environment::Dev).unwrap_err(); + let err = store + .delete_secret(&missing_project_id, "missing", Environment::Dev) + .unwrap_err(); assert_eq!(err.to_string(), "Secret does not exist"); cleanup_test_db(db_path);