use crate::error::{ErrorData, Result};
use alien_error::{AlienError, Context, IntoAlienError};
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug)]
pub struct LocalVault {
vault_name: String,
vault_dir: PathBuf,
}
impl LocalVault {
pub fn new(vault_name: String, vault_dir: PathBuf) -> Self {
Self {
vault_name,
vault_dir,
}
}
fn secrets_file_path(&self) -> PathBuf {
self.vault_dir.join("secrets.json")
}
async fn load_secrets(&self) -> Result<HashMap<String, String>> {
let secrets_file = self.secrets_file_path();
if !secrets_file.exists() {
return Ok(HashMap::new());
}
let content = tokio::fs::read_to_string(&secrets_file)
.await
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: format!(
"Failed to read vault secrets file: {}",
secrets_file.display()
),
resource_id: None,
})?;
serde_json::from_str(&content)
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: format!(
"Failed to parse vault secrets file: {}",
secrets_file.display()
),
resource_id: None,
})
}
async fn save_secrets(&self, secrets: &HashMap<String, String>) -> Result<()> {
let secrets_file = self.secrets_file_path();
if let Some(parent) = secrets_file.parent() {
tokio::fs::create_dir_all(parent)
.await
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: format!("Failed to create vault directory: {}", parent.display()),
resource_id: None,
})?;
}
let json = serde_json::to_string_pretty(secrets)
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: "Failed to serialize vault secrets".to_string(),
resource_id: None,
})?;
let path = secrets_file.clone();
let data = json.into_bytes();
tokio::task::spawn_blocking(move || alien_core::file_utils::write_secret_file(&path, &data))
.await
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: "Failed to spawn blocking write task".to_string(),
resource_id: None,
})?
.into_alien_error()
.context(ErrorData::CloudPlatformError {
message: format!(
"Failed to write vault secrets file: {}",
secrets_file.display()
),
resource_id: None,
})
}
}
#[async_trait]
impl crate::traits::Binding for LocalVault {}
#[async_trait]
impl crate::traits::Vault for LocalVault {
async fn get_secret(&self, secret_name: &str) -> Result<String> {
let secrets = self.load_secrets().await?;
secrets.get(secret_name).cloned().ok_or_else(|| {
AlienError::new(ErrorData::CloudPlatformError {
message: format!(
"Secret '{}' not found in vault '{}'",
secret_name, self.vault_name
),
resource_id: None,
})
})
}
async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()> {
let mut secrets = self.load_secrets().await?;
secrets.insert(secret_name.to_string(), value.to_string());
self.save_secrets(&secrets).await
}
async fn delete_secret(&self, secret_name: &str) -> Result<()> {
let mut secrets = self.load_secrets().await?;
secrets.remove(secret_name);
self.save_secrets(&secrets).await
}
async fn list_secrets(&self) -> Result<Vec<String>> {
let secrets = self.load_secrets().await?;
Ok(secrets.into_keys().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Vault as _;
use std::collections::BTreeSet;
use tempfile::TempDir;
fn test_vault() -> (LocalVault, TempDir) {
let temp_dir = TempDir::new().expect("tempdir");
let vault = LocalVault::new("secrets".to_string(), temp_dir.path().to_path_buf());
(vault, temp_dir)
}
#[tokio::test]
async fn list_secrets_empty_when_no_file() {
let (vault, _temp_dir) = test_vault();
let names = vault.list_secrets().await.expect("list should succeed");
assert!(names.is_empty(), "fresh vault must list no secrets");
}
#[tokio::test]
async fn list_secrets_returns_all_stored_names() {
let (vault, _temp_dir) = test_vault();
vault.set_secret("api-key", "a").await.expect("set api-key");
vault.set_secret("db-url", "b").await.expect("set db-url");
let names: BTreeSet<String> = vault
.list_secrets()
.await
.expect("list should succeed")
.into_iter()
.collect();
assert_eq!(
names,
BTreeSet::from(["api-key".to_string(), "db-url".to_string()]),
"list must return exactly the stored secret names"
);
assert_eq!(vault.get_secret("api-key").await.expect("get"), "a");
}
#[tokio::test]
async fn list_secrets_reflects_deletion() {
let (vault, _temp_dir) = test_vault();
vault.set_secret("keep", "1").await.expect("set keep");
vault.set_secret("drop", "2").await.expect("set drop");
vault.delete_secret("drop").await.expect("delete drop");
vault
.delete_secret("drop")
.await
.expect("repeated delete is idempotent");
let names = vault.list_secrets().await.expect("list should succeed");
assert_eq!(
names,
vec!["keep".to_string()],
"deleted secret must be gone"
);
}
}