generated from pure_sagacity/rust-starter
Compare commits
2 Commits
920f104c2e
...
a850f70e3b
| Author | SHA1 | Date | |
|---|---|---|---|
| a850f70e3b | |||
|
be860b657a
|
@@ -0,0 +1,218 @@
|
||||
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 diesel::connection::SimpleConnection;
|
||||
use diesel::{Connection, SqliteConnection};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn test_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
fn setup_test_db() -> PathBuf {
|
||||
let db_path = std::env::temp_dir().join(format!("harbor_test_{}.db", Uuid::new_v4()));
|
||||
std::fs::File::create(&db_path).expect("create sqlite file");
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("DATABASE_URL", &db_path);
|
||||
}
|
||||
|
||||
let database_url = db_path.to_str().expect("db path");
|
||||
let mut conn = SqliteConnection::establish(database_url).expect("connect sqlite");
|
||||
conn.batch_execute(
|
||||
"PRAGMA foreign_keys = ON;\
|
||||
CREATE TABLE projects (\
|
||||
id TEXT PRIMARY KEY NOT NULL,\
|
||||
name TEXT NOT NULL UNIQUE,\
|
||||
created_at TIMESTAMP NOT NULL\
|
||||
);\
|
||||
CREATE TABLE secrets (\
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT (lower(hex(randomblob(16)))),\
|
||||
name TEXT NOT NULL,\
|
||||
project_id TEXT NOT NULL,\
|
||||
config TEXT NOT NULL,\
|
||||
secret BLOB NOT NULL,\
|
||||
nonce BLOB NOT NULL,\
|
||||
created_at TIMESTAMP NOT NULL,\
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id)\
|
||||
);",
|
||||
)
|
||||
.expect("create schema");
|
||||
|
||||
db_path
|
||||
}
|
||||
|
||||
fn cleanup_test_db(path: PathBuf) {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_parsing_and_display() {
|
||||
let dev: Environment = "DEV".parse().expect("parse dev");
|
||||
let staging: Environment = " staging ".parse().expect("parse staging");
|
||||
|
||||
assert_eq!(dev, Environment::Dev);
|
||||
assert_eq!(staging, Environment::Staging);
|
||||
assert_eq!(format!("{}", Environment::Prod), "prod");
|
||||
|
||||
let err = "unknown".parse::<Environment>().unwrap_err();
|
||||
assert_eq!(err.to_string(), "Environment must be one of: dev, prod, staging");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_secret_pairs_handles_valid_and_invalid_input() {
|
||||
let input = vec![
|
||||
" FOO=bar ".to_string(),
|
||||
"".to_string(),
|
||||
"BAZ=qux".to_string(),
|
||||
"A=B=C".to_string(),
|
||||
];
|
||||
|
||||
let parsed = parse_secret_pairs(&input).expect("parse secret pairs");
|
||||
assert_eq!(parsed.len(), 3);
|
||||
assert_eq!(parsed[0], ("FOO".to_string(), "bar".to_string()));
|
||||
assert_eq!(parsed[1], ("BAZ".to_string(), "qux".to_string()));
|
||||
assert_eq!(parsed[2], ("A".to_string(), "B=C".to_string()));
|
||||
|
||||
let err = parse_secret_pairs(&["NOPE".to_string()]).unwrap_err();
|
||||
assert_eq!(err.to_string(), "Invalid secret format: NOPE");
|
||||
|
||||
let err = parse_secret_pairs(&["=value".to_string()]).unwrap_err();
|
||||
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()));
|
||||
std::fs::create_dir_all(&temp_root).expect("create temp dir");
|
||||
|
||||
let config_path = temp_root.join(".harbor.toml");
|
||||
std::fs::write(
|
||||
&config_path,
|
||||
"version = \"1\"\nname = \"proj\"\nconfig = \"dev\"\n",
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
let config = Config::from_repo_root(&temp_root).expect("read config");
|
||||
assert_eq!(config.name, "proj");
|
||||
assert_eq!(config.version, "1");
|
||||
assert_eq!(config.default_env, Environment::Dev);
|
||||
|
||||
let invalid_path = temp_root.join("bad.toml");
|
||||
std::fs::write(&invalid_path, "not = [valid").expect("write invalid");
|
||||
let err = Config::read_main_file(&invalid_path).unwrap_err();
|
||||
assert!(err.to_string().contains("TOML error"));
|
||||
|
||||
let missing_path = temp_root.join("missing.toml");
|
||||
let err = Config::read_main_file(&missing_path).unwrap_err();
|
||||
assert!(err.to_string().contains("IO error"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&temp_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_and_secret_lifecycle() {
|
||||
let _guard = test_lock().lock().expect("lock tests");
|
||||
let db_path = setup_test_db();
|
||||
|
||||
assert!(!project_exists("alpha").expect("project exists"));
|
||||
|
||||
create_project("alpha").expect("create project");
|
||||
assert!(project_exists("alpha").expect("project exists"));
|
||||
|
||||
let project_id = get_project_id("alpha").expect("get project id");
|
||||
let projects = get_projects().expect("get projects");
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].name, "alpha");
|
||||
|
||||
assert!(!secret_exists().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");
|
||||
|
||||
assert!(secret_exists().expect("secret exists"));
|
||||
|
||||
let secrets = 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");
|
||||
|
||||
let updated = 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("API_KEY").expect("delete secret");
|
||||
assert!(!secret_exists().expect("secret exists"));
|
||||
|
||||
delete_project("alpha").expect("delete project");
|
||||
assert!(!project_exists("alpha").expect("project exists"));
|
||||
|
||||
cleanup_test_db(db_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_for_missing_project_or_secret() {
|
||||
let _guard = test_lock().lock().expect("lock tests");
|
||||
let db_path = setup_test_db();
|
||||
|
||||
let err = delete_project("missing").unwrap_err();
|
||||
assert_eq!(err.to_string(), "Project does not exist");
|
||||
|
||||
let err = get_project_id("missing").unwrap_err();
|
||||
assert_eq!(err.to_string(), "Project not found");
|
||||
|
||||
let err = delete_secret("missing").unwrap_err();
|
||||
assert_eq!(err.to_string(), "Secret does not exist");
|
||||
|
||||
cleanup_test_db(db_path);
|
||||
}
|
||||
@@ -63,4 +63,39 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn helper_generates_expected_sizes() {
|
||||
let key = helper::gen_key();
|
||||
let nonce = helper::gen_nonce();
|
||||
|
||||
assert_eq!(key.as_slice().len(), 32);
|
||||
assert_eq!(nonce.as_slice().len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_validates_length() {
|
||||
let key = helper::key_from(vec![7u8; 32]).expect("expected valid key");
|
||||
assert_eq!(key.as_slice(), vec![7u8; 32]);
|
||||
|
||||
let err = helper::key_from(vec![1u8; 31]).unwrap_err();
|
||||
assert_eq!(err.to_string(), "key must be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_fails_with_wrong_key_or_tampered_ciphertext() {
|
||||
let key = helper::gen_key();
|
||||
let other_key = helper::gen_key();
|
||||
let plaintext = b"secret".to_vec();
|
||||
|
||||
let (nonce, mut ciphertext) = encrypt(&key, plaintext.clone()).expect("encrypt");
|
||||
|
||||
assert!(decrypt(&other_key, ciphertext.clone(), &nonce).is_err());
|
||||
|
||||
if let Some(byte) = ciphertext.get_mut(0) {
|
||||
*byte ^= 0b1010_1010;
|
||||
}
|
||||
|
||||
assert!(decrypt(&key, ciphertext, &nonce).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user