use super::client::ReqwestHttpClient;
use super::store::HttpSecretStore;
use super::types::ConfigKey;
use crate::common::Result;
#[derive(Debug, Default)]
pub struct HttpSecretStoreBuilder {
base_url: Option<String>,
auth_token: Option<String>,
namespace: Option<String>,
}
impl HttpSecretStoreBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn from_env() -> Self {
Self {
base_url: std::env::var(ConfigKey::BaseUrl.env_var())
.ok()
.filter(|v| !v.is_empty()),
auth_token: std::env::var(ConfigKey::AuthToken.env_var())
.ok()
.filter(|v| !v.is_empty()),
namespace: std::env::var(ConfigKey::Namespace.env_var())
.ok()
.filter(|v| !v.is_empty()),
}
}
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
self.auth_token = Some(token.into());
self
}
pub fn with_namespace(mut self, ns: impl Into<String>) -> Self {
self.namespace = Some(ns.into());
self
}
pub fn build(self) -> Result<HttpSecretStore> {
let base_url = self
.base_url
.ok_or_else(|| crate::common::Error::Configuration {
store: "HttpSecretStore",
message: format!(
"base URL is required — set `{}` or call `.with_base_url()`",
ConfigKey::BaseUrl.env_var()
),
})?;
let base_url = base_url.trim_end_matches('/').to_owned();
let http_client = reqwest::Client::builder().build().map_err(|e| {
crate::common::Error::Configuration {
store: "HttpSecretStore",
message: format!("failed to build HTTP client: {e}"),
}
})?;
Ok(HttpSecretStore::from_reqwest_client(ReqwestHttpClient {
base_url,
auth_token: self.auth_token,
namespace: self.namespace,
http: http_client,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_stores_base_url() {
let b = HttpSecretStoreBuilder::new().with_base_url("http://localhost:8200/v1/secret");
assert_eq!(
b.base_url.as_deref(),
Some("http://localhost:8200/v1/secret")
);
}
#[test]
fn builder_stores_auth_token() {
let b = HttpSecretStoreBuilder::new()
.with_base_url("http://x")
.with_auth_token("my-token");
assert_eq!(b.auth_token.as_deref(), Some("my-token"));
}
#[test]
fn builder_stores_namespace() {
let b = HttpSecretStoreBuilder::new()
.with_base_url("http://x")
.with_namespace("my-ns");
assert_eq!(b.namespace.as_deref(), Some("my-ns"));
}
#[test]
fn from_env_reads_variables() {
unsafe { std::env::set_var("SECRET_STORE_HTTP_URL", "http://env-vault:8200") };
unsafe { std::env::set_var("SECRET_STORE_HTTP_TOKEN", "env-token") };
let b = HttpSecretStoreBuilder::from_env();
assert_eq!(b.base_url.as_deref(), Some("http://env-vault:8200"));
assert_eq!(b.auth_token.as_deref(), Some("env-token"));
unsafe { std::env::remove_var("SECRET_STORE_HTTP_URL") };
unsafe { std::env::remove_var("SECRET_STORE_HTTP_TOKEN") };
}
#[test]
fn from_env_ignores_empty_vars() {
unsafe { std::env::set_var("SECRET_STORE_HTTP_URL", "") };
let b = HttpSecretStoreBuilder::from_env();
assert!(b.base_url.is_none());
unsafe { std::env::remove_var("SECRET_STORE_HTTP_URL") };
}
#[test]
fn build_fails_without_base_url() {
let result = HttpSecretStoreBuilder::new().build();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::common::Error::Configuration { .. }
));
}
#[test]
fn build_strips_trailing_slash_from_url() {
let store = HttpSecretStoreBuilder::new()
.with_base_url("http://localhost:8200/v1/secret/")
.build()
.unwrap();
assert!(
!store.base_url().ends_with('/'),
"url was: {}",
store.base_url()
);
}
#[test]
fn build_succeeds_with_base_url() {
let result = HttpSecretStoreBuilder::new()
.with_base_url("http://localhost:8200/v1/secret")
.build();
assert!(result.is_ok());
}
}