1use oz_keystore::HashicorpCloudClient;
14use serde::{Deserialize, Serialize};
15use std::{env, fmt, sync::Arc};
16use tokio::sync::OnceCell;
17use zeroize::{Zeroize, ZeroizeOnDrop};
18
19use crate::{
20 impl_case_insensitive_enum,
21 models::security::{
22 error::{SecurityError, SecurityResult},
23 get_env_var,
24 },
25};
26
27#[async_trait::async_trait]
29pub trait VaultClient: Send + Sync {
30 async fn get_secret(&self, name: &str) -> SecurityResult<SecretString>;
31}
32
33#[derive(Clone)]
35pub struct CloudVaultClient {
36 client: Arc<HashicorpCloudClient>,
37}
38
39impl CloudVaultClient {
40 pub fn from_env() -> SecurityResult<Self> {
42 let client_id = get_env_var("HCP_CLIENT_ID")?;
43 let client_secret = get_env_var("HCP_CLIENT_SECRET")?;
44 let org_id = get_env_var("HCP_ORG_ID")?;
45 let project_id = get_env_var("HCP_PROJECT_ID")?;
46 let app_name = get_env_var("HCP_APP_NAME")?;
47 let client =
48 HashicorpCloudClient::new(client_id, client_secret, org_id, project_id, app_name);
49 Ok(Self {
50 client: Arc::new(client),
51 })
52 }
53}
54
55#[async_trait::async_trait]
56impl VaultClient for CloudVaultClient {
57 async fn get_secret(&self, name: &str) -> SecurityResult<SecretString> {
58 let secret = self.client.get_secret(name).await.map_err(|e| {
59 SecurityError::network_error(
60 "Failed to get secret from Hashicorp Cloud Vault",
61 Some(e.into()),
62 None,
63 )
64 })?;
65 Ok(SecretString::new(secret.secret.static_version.value))
66 }
67}
68
69#[derive(Clone)]
71pub enum VaultType {
72 Cloud(CloudVaultClient),
73}
74
75impl VaultType {
76 pub fn from_env() -> SecurityResult<Self> {
78 Ok(Self::Cloud(CloudVaultClient::from_env()?))
80 }
81}
82
83#[async_trait::async_trait]
84impl VaultClient for VaultType {
85 async fn get_secret(&self, name: &str) -> SecurityResult<SecretString> {
86 match self {
87 Self::Cloud(client) => client.get_secret(name).await,
88 }
89 }
90}
91
92static VAULT_CLIENT: OnceCell<VaultType> = OnceCell::const_new();
94
95pub async fn get_vault_client() -> SecurityResult<&'static VaultType> {
97 VAULT_CLIENT
98 .get_or_try_init(|| async { VaultType::from_env() })
99 .await
100 .map_err(|e| {
101 Box::new(SecurityError::parse_error(
102 "Failed to get vault client",
103 Some(e.into()),
104 None,
105 ))
106 })
107}
108
109#[derive(Debug, Clone, Serialize, ZeroizeOnDrop)]
119#[serde(tag = "type", content = "value")]
120#[serde(deny_unknown_fields)]
121pub enum SecretValue {
122 Plain(SecretString),
124 Environment(String),
126 HashicorpCloudVault(String),
128}
129
130impl_case_insensitive_enum!(SecretValue, {
131 "plain" => Plain,
132 "environment" => Environment,
133 "hashicorpcloudvault" => HashicorpCloudVault,
134});
135
136impl PartialEq for SecretValue {
137 fn eq(&self, other: &Self) -> bool {
138 match (self, other) {
139 (Self::Plain(l0), Self::Plain(r0)) => l0.as_str() == r0.as_str(),
140 (Self::Environment(l0), Self::Environment(r0)) => l0 == r0,
141 (Self::HashicorpCloudVault(l0), Self::HashicorpCloudVault(r0)) => l0 == r0,
142 _ => false,
143 }
144 }
145}
146
147#[derive(Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
160pub struct SecretString(String);
161
162impl PartialEq for SecretString {
163 fn eq(&self, other: &Self) -> bool {
164 self.0 == other.0
165 }
166}
167
168impl SecretValue {
169 pub async fn resolve(&self) -> SecurityResult<SecretString> {
183 match self {
184 SecretValue::Plain(secret) => Ok(secret.clone()),
185 SecretValue::Environment(env_var) => {
186 env::var(env_var).map(SecretString::new).map_err(|e| {
187 Box::new(SecurityError::parse_error(
188 format!("Failed to get environment variable {}", env_var),
189 Some(e.into()),
190 None,
191 ))
192 })
193 }
194 SecretValue::HashicorpCloudVault(name) => {
195 let client = get_vault_client().await?;
196 client.get_secret(name).await.map_err(|e| {
197 Box::new(SecurityError::parse_error(
198 format!("Failed to get secret from Hashicorp Cloud Vault {}", name),
199 Some(e.into()),
200 None,
201 ))
202 })
203 }
204 }
205 }
206
207 pub fn starts_with(&self, prefix: &str) -> bool {
209 match self {
210 SecretValue::Plain(secret) => secret.as_str().starts_with(prefix),
211 SecretValue::Environment(env_var) => env_var.starts_with(prefix),
212 SecretValue::HashicorpCloudVault(name) => name.starts_with(prefix),
213 }
214 }
215
216 pub fn is_empty(&self) -> bool {
218 match self {
219 SecretValue::Plain(secret) => secret.as_str().is_empty(),
220 SecretValue::Environment(env_var) => env_var.is_empty(),
221 SecretValue::HashicorpCloudVault(name) => name.is_empty(),
222 }
223 }
224
225 pub fn trim(&self) -> &str {
227 match self {
228 SecretValue::Plain(secret) => secret.as_str().trim(),
229 SecretValue::Environment(env_var) => env_var.trim(),
230 SecretValue::HashicorpCloudVault(name) => name.trim(),
231 }
232 }
233
234 pub fn as_str(&self) -> &str {
236 match self {
237 SecretValue::Plain(secret) => secret.as_str(),
238 SecretValue::Environment(env_var) => env_var,
239 SecretValue::HashicorpCloudVault(name) => name,
240 }
241 }
242}
243
244impl Zeroize for SecretValue {
245 fn zeroize(&mut self) {
252 match self {
253 SecretValue::Plain(secret) => secret.zeroize(),
254 SecretValue::Environment(env_var) => {
255 env_var.clear();
257 }
258 SecretValue::HashicorpCloudVault(name) => {
259 name.clear();
260 }
261 }
262 }
263}
264
265impl SecretString {
266 pub fn new(value: String) -> Self {
270 Self(value)
271 }
272
273 pub fn as_str(&self) -> &str {
280 &self.0
281 }
282}
283
284impl From<String> for SecretString {
285 fn from(value: String) -> Self {
286 Self::new(value)
287 }
288}
289
290impl AsRef<str> for SecretString {
291 fn as_ref(&self) -> &str {
292 self.as_str()
293 }
294}
295
296impl fmt::Display for SecretValue {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 match self {
299 SecretValue::Plain(_) => write!(f, "<secret string>"),
300 SecretValue::Environment(env_var) => write!(f, "{}", env_var),
301 SecretValue::HashicorpCloudVault(name) => write!(f, "{}", name),
302 }
303 }
304}
305
306impl fmt::Debug for SecretString {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 write!(f, "<secret string>")
309 }
310}
311
312impl fmt::Display for SecretString {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(f, "<secret string>")
315 }
316}
317
318impl AsRef<str> for SecretValue {
319 fn as_ref(&self) -> &str {
320 match self {
321 SecretValue::Plain(secret) => secret.as_ref(),
322 SecretValue::Environment(env_var) => env_var,
323 SecretValue::HashicorpCloudVault(name) => name,
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use lazy_static::lazy_static;
332 use std::sync::atomic::{AtomicBool, Ordering};
333 use std::sync::Mutex;
334 use zeroize::Zeroize;
335
336 lazy_static! {
338 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
339 }
340
341 #[allow(clippy::await_holding_lock)]
343 async fn with_test_env<F, Fut>(f: F)
344 where
345 F: FnOnce() -> Fut,
346 Fut: std::future::Future<Output = ()>,
347 {
348 let _lock = ENV_MUTEX.lock().unwrap();
350
351 let env_vars = [
352 ("HCP_CLIENT_ID", "test-client-id"),
353 ("HCP_CLIENT_SECRET", "test-client-secret"),
354 ("HCP_ORG_ID", "test-org"),
355 ("HCP_PROJECT_ID", "test-project"),
356 ("HCP_APP_NAME", "test-app"),
357 ];
358
359 let original_values: Vec<_> = env_vars
361 .iter()
362 .map(|(key, _)| (*key, std::env::var(key).ok()))
363 .collect();
364
365 for (key, value) in env_vars.iter() {
367 std::env::set_var(key, value);
368 }
369
370 f().await;
372
373 for (key, value) in original_values {
375 match value {
376 Some(val) => std::env::set_var(key, val),
377 None => std::env::remove_var(key),
378 }
379 }
380 }
381
382 struct TrackedSecret<T: Zeroize> {
384 inner: T,
385 was_zeroized: Arc<AtomicBool>,
386 }
387
388 impl<T: Zeroize> TrackedSecret<T> {
389 fn new(value: T, was_zeroized: Arc<AtomicBool>) -> Self {
390 Self {
391 inner: value,
392 was_zeroized,
393 }
394 }
395 }
396
397 impl<T: Zeroize> Zeroize for TrackedSecret<T> {
398 fn zeroize(&mut self) {
399 self.was_zeroized.store(true, Ordering::SeqCst);
400 self.inner.zeroize();
401 }
402 }
403
404 impl<T: Zeroize> Drop for TrackedSecret<T> {
405 fn drop(&mut self) {
406 self.zeroize();
407 }
408 }
409
410 #[test]
412 fn test_secret_string_zeroize_on_drop() {
413 let was_zeroized = Arc::new(AtomicBool::new(false));
414 let secret = "sensitive_data".to_string();
415 let secret_string =
416 TrackedSecret::new(SecretString::new(secret.clone()), was_zeroized.clone());
417
418 assert_eq!(secret_string.inner.as_str(), secret);
420 assert!(!was_zeroized.load(Ordering::SeqCst));
421
422 {
424 let _secret_string = secret_string;
425 assert_eq!(_secret_string.inner.as_str(), secret);
427 assert!(!was_zeroized.load(Ordering::SeqCst));
428 }
429
430 assert!(was_zeroized.load(Ordering::SeqCst));
432 }
433
434 #[test]
436 fn test_secret_value_zeroize_on_drop() {
437 let was_zeroized = Arc::new(AtomicBool::new(false));
438 let secret = "sensitive_data".to_string();
439 let secret_value = TrackedSecret::new(
440 SecretValue::Plain(SecretString::new(secret.clone())),
441 was_zeroized.clone(),
442 );
443
444 assert_eq!(secret_value.inner.as_str(), secret);
446 assert!(!was_zeroized.load(Ordering::SeqCst));
447
448 {
450 let _secret_value = secret_value;
451 assert_eq!(_secret_value.inner.as_str(), secret);
453 assert!(!was_zeroized.load(Ordering::SeqCst));
454 }
455
456 assert!(was_zeroized.load(Ordering::SeqCst));
458 }
459
460 #[tokio::test]
462 async fn test_environment_secret() {
463 const TEST_ENV_VAR: &str = "TEST_SECRET_ENV_VAR";
464 const TEST_SECRET: &str = "test_secret_value";
465
466 env::set_var(TEST_ENV_VAR, TEST_SECRET);
467
468 let secret = SecretValue::Environment(TEST_ENV_VAR.to_string());
469 let resolved = secret.resolve().await.unwrap();
470
471 assert_eq!(resolved.as_str(), TEST_SECRET);
472
473 env::remove_var(TEST_ENV_VAR);
474 }
475
476 #[test]
478 fn test_secret_string_zeroize() {
479 let secret = "sensitive_data".to_string();
480 let mut secret_string = SecretString::new(secret.clone());
481
482 assert_eq!(secret_string.as_str(), secret);
483
484 secret_string.zeroize();
486 assert_eq!(secret_string.as_str(), "");
487 }
488
489 #[test]
491 fn test_secret_value_zeroize() {
492 let mut plain_secret = SecretValue::Plain(SecretString::new("plain_secret".to_string()));
493 let mut env_secret = SecretValue::Environment("ENV_VAR".to_string());
494 let mut cloud_vault_secret = SecretValue::HashicorpCloudVault("secret_name".to_string());
495
496 plain_secret.zeroize();
497 env_secret.zeroize();
498 cloud_vault_secret.zeroize();
499
500 if let SecretValue::Plain(ref secret) = plain_secret {
502 assert_eq!(secret.as_str(), "");
503 }
504
505 if let SecretValue::Environment(ref env_var) = env_secret {
506 assert_eq!(env_var, "");
507 }
508 if let SecretValue::HashicorpCloudVault(ref name) = cloud_vault_secret {
509 assert_eq!(name, "");
510 }
511 }
512
513 #[tokio::test]
514 async fn test_cloud_vault_client_from_env_success() {
515 with_test_env(|| async {
516 let result = CloudVaultClient::from_env();
517 assert!(result.is_ok());
518 })
519 .await;
520 }
521
522 #[tokio::test]
523 async fn test_cloud_vault_client_from_env_missing_vars() {
524 with_test_env(|| async {
525 std::env::remove_var("HCP_CLIENT_ID");
527 let result = CloudVaultClient::from_env();
528 assert!(result.is_err());
529 assert!(result.err().unwrap().to_string().contains("HCP_CLIENT_ID"));
530 })
531 .await;
532
533 with_test_env(|| async {
534 std::env::remove_var("HCP_CLIENT_SECRET");
536 let result = CloudVaultClient::from_env();
537 assert!(result.is_err());
538 assert!(result
539 .err()
540 .unwrap()
541 .to_string()
542 .contains("HCP_CLIENT_SECRET"));
543 })
544 .await;
545 }
546
547 #[tokio::test]
548 async fn test_vault_type_from_env() {
549 with_test_env(|| async {
550 let result = VaultType::from_env();
551 assert!(result.is_ok());
552 match result.unwrap() {
553 VaultType::Cloud(_) => (), }
555 })
556 .await;
557 }
558
559 #[tokio::test]
560 async fn test_get_vault_client() {
561 with_test_env(|| async {
562 std::env::remove_var("HCP_CLIENT_ID");
566 let result = get_vault_client().await;
567 assert!(result.is_err());
568 assert!(result
569 .err()
570 .unwrap()
571 .to_string()
572 .contains("Failed to get vault client"));
573
574 std::env::set_var("HCP_CLIENT_ID", "test-client-id");
576
577 let result = get_vault_client().await;
579 assert!(result.is_ok());
580 let client = result.unwrap();
581 match client {
582 VaultType::Cloud(_) => (), }
584
585 let result2 = get_vault_client().await;
587 assert!(result2.is_ok());
588 assert!(std::ptr::eq(client, result2.unwrap()));
589 })
590 .await;
591 }
592
593 #[tokio::test]
594 async fn test_vault_client_get_secret() {
595 let mut server = mockito::Server::new_async().await;
596 let token_mock = server
598 .mock("POST", "/oauth2/token")
599 .with_status(200)
600 .with_header("content-type", "application/json")
601 .with_body(
602 r#"{"access_token": "test-token", "token_type": "Bearer", "expires_in": 3600}"#,
603 )
604 .create_async()
605 .await;
606
607 let secret_mock = server
609 .mock(
610 "GET",
611 "/secrets/2023-11-28/organizations/test-org/projects/test-project/apps/test-app/secrets/test-secret:open",
612 )
613 .with_status(200)
614 .with_header("content-type", "application/json")
615 .with_body(r#"{"secret": {"static_version": {"value": "test-secret-value"}}}"#)
616 .create_async()
617 .await;
618
619 let hashicorp_client = HashicorpCloudClient::new(
621 "test-client-id".to_string(),
622 "test-client-secret".to_string(),
623 "test-org".to_string(),
624 "test-project".to_string(),
625 "test-app".to_string(),
626 )
627 .with_api_base_url(server.url())
628 .with_auth_base_url(server.url());
629
630 let vault_client = CloudVaultClient {
631 client: Arc::new(hashicorp_client),
632 };
633
634 let result = vault_client.get_secret("test-secret").await;
636
637 token_mock.assert_async().await;
639 secret_mock.assert_async().await;
640
641 assert!(result.is_ok());
643 assert_eq!(result.unwrap().as_str(), "test-secret-value");
644 }
645
646 #[tokio::test]
647 async fn test_vault_client_get_secret_error() {
648 with_test_env(|| async {
649 let mut server = mockito::Server::new_async().await;
651 let token_mock = server
652 .mock("POST", "/oauth2/token")
653 .with_status(500)
654 .with_header("content-type", "application/json")
655 .with_body(r#"{"error": "internal server error"}"#)
656 .create_async()
657 .await;
658
659 let hashicorp_client = HashicorpCloudClient::new(
661 "test-client-id".to_string(),
662 "test-client-secret".to_string(),
663 "test-org".to_string(),
664 "test-project".to_string(),
665 "test-app".to_string(),
666 )
667 .with_api_base_url(server.url())
668 .with_auth_base_url(server.url());
669
670 let vault_client = CloudVaultClient {
671 client: Arc::new(hashicorp_client),
672 };
673
674 let result = vault_client.get_secret("test-secret").await;
675
676 token_mock.assert_async().await;
678
679 assert!(result.is_err());
681 assert!(result
682 .err()
683 .unwrap()
684 .to_string()
685 .contains("Failed to get secret from Hashicorp Cloud Vault"));
686 })
687 .await;
688 }
689
690 #[tokio::test]
691 async fn test_vault_type_clone() {
692 with_test_env(|| async {
693 let vault_type = VaultType::from_env().unwrap();
694 let cloned = vault_type.clone();
695
696 match (vault_type, cloned) {
697 (VaultType::Cloud(_), VaultType::Cloud(_)) => (), }
699 })
700 .await;
701 }
702
703 #[test]
704 fn test_cloud_vault_client_new_wraps_arc() {
705 let dummy = HashicorpCloudClient::new(
706 "id".to_string(),
707 "secret".to_string(),
708 "org".to_string(),
709 "proj".to_string(),
710 "app".to_string(),
711 );
712 let client = CloudVaultClient {
713 client: Arc::new(dummy),
714 };
715 assert!(Arc::strong_count(&client.client) >= 1);
717 }
718
719 #[tokio::test]
720 async fn test_cloud_vault_client_from_env_missing_org_id() {
721 with_test_env(|| async {
722 std::env::remove_var("HCP_ORG_ID");
723 let result = CloudVaultClient::from_env();
724 assert!(result.is_err());
725 assert!(result.err().unwrap().to_string().contains("HCP_ORG_ID"));
726 })
727 .await;
728 }
729
730 #[tokio::test]
731 async fn test_cloud_vault_client_from_env_missing_project_id() {
732 with_test_env(|| async {
733 std::env::remove_var("HCP_PROJECT_ID");
734 let result = CloudVaultClient::from_env();
735 assert!(result.is_err());
736 assert!(result.err().unwrap().to_string().contains("HCP_PROJECT_ID"));
737 })
738 .await;
739 }
740
741 #[tokio::test]
742 async fn test_cloud_vault_client_from_env_missing_app_name() {
743 with_test_env(|| async {
744 std::env::remove_var("HCP_APP_NAME");
745 let result = CloudVaultClient::from_env();
746 assert!(result.is_err());
747 assert!(result.err().unwrap().to_string().contains("HCP_APP_NAME"));
748 })
749 .await;
750 }
751
752 #[tokio::test]
753 async fn test_cloud_vault_client_from_env_missing_client_id() {
754 with_test_env(|| async {
755 std::env::remove_var("HCP_CLIENT_ID");
756 let result = CloudVaultClient::from_env();
757 assert!(result.is_err());
758 assert!(result.err().unwrap().to_string().contains("HCP_CLIENT_ID"));
759 })
760 .await;
761 }
762
763 #[tokio::test]
764 async fn test_cloud_vault_client_from_env_missing_client_secret() {
765 with_test_env(|| async {
766 std::env::remove_var("HCP_CLIENT_SECRET");
767 let result = CloudVaultClient::from_env();
768 assert!(result.is_err());
769 assert!(result
770 .err()
771 .unwrap()
772 .to_string()
773 .contains("HCP_CLIENT_SECRET"));
774 })
775 .await;
776 }
777
778 #[tokio::test]
779 async fn test_vault_type_get_secret_delegates() {
780 with_test_env(|| async {
781 let vault = VaultType::from_env().unwrap();
782 let result = vault.get_secret("nonexistent").await;
783 assert!(
784 result.is_err(),
785 "Expected error for nonexistent secret, got: {:?}",
786 result
787 );
788 })
789 .await;
790 }
791
792 #[test]
793 fn test_secret_value_partial_eq_false_for_different_variants() {
794 let a = SecretValue::Plain(SecretString::new("a".to_string()));
795 let b = SecretValue::Environment("a".to_string());
796 let c = SecretValue::HashicorpCloudVault("a".to_string());
797 assert_ne!(a, b);
798 assert_ne!(a, c);
799 assert_ne!(b, c);
800 }
801
802 #[test]
803 fn test_secret_string_partial_eq() {
804 let a = SecretString::new("foo".to_string());
805 let b = SecretString::new("foo".to_string());
806 let c = SecretString::new("bar".to_string());
807 assert_eq!(a, b);
808 assert_ne!(a, c);
809 }
810
811 #[tokio::test]
812 async fn test_secret_value_resolve_env_error() {
813 let secret = SecretValue::Environment("NON_EXISTENT_ENV_VAR".to_string());
814 let result = secret.resolve().await;
815 assert!(result.is_err());
816 assert!(result
817 .err()
818 .unwrap()
819 .to_string()
820 .contains("Failed to get environment variable"));
821 }
822
823 #[tokio::test]
824 async fn test_secret_value_resolve_hashicorp_cloud_vault_error() {
825 with_test_env(|| async {
826 let secret = SecretValue::HashicorpCloudVault("NON_EXISTENT_VAULT_SECRET".to_string());
827 let result = secret.resolve().await;
828 assert!(result.is_err());
829 assert!(result
830 .err()
831 .unwrap()
832 .to_string()
833 .contains("Failed to get secret from Hashicorp Cloud Vault"));
834 })
835 .await;
836 }
837
838 #[test]
839 fn test_secret_string_debug() {
840 let secret = SecretString::new("test".to_string());
841 assert_eq!(format!("{:?}", secret), "<secret string>");
842
843 let secret = SecretValue::Plain(SecretString::new("test".to_string()));
844 assert_eq!(format!("{:?}", secret), "Plain(<secret string>)");
845
846 let secret = SecretValue::Environment("test".to_string());
847 assert_eq!(format!("{:?}", secret), "Environment(\"test\")");
848
849 let secret = SecretValue::HashicorpCloudVault("test".to_string());
850 assert_eq!(format!("{:?}", secret), "HashicorpCloudVault(\"test\")");
851 }
852
853 #[test]
854 fn test_secret_string_display() {
855 let secret = SecretString::new("test".to_string());
856 assert_eq!(format!("{}", secret), "<secret string>");
857
858 let secret = SecretValue::Plain(SecretString::new("test".to_string()));
859 assert_eq!(format!("{}", secret), "<secret string>");
860
861 let secret = SecretValue::Environment("test".to_string());
862 assert_eq!(format!("{}", secret), "test");
863
864 let secret = SecretValue::HashicorpCloudVault("test".to_string());
865 assert_eq!(format!("{}", secret), "test");
866 }
867
868 #[test]
869 fn test_secret_value_starts_with() {
870 let plain = SecretValue::Plain(SecretString::new("PREFIX_value".to_string()));
871 let env = SecretValue::Environment("PREFIX_value".to_string());
872 let vault = SecretValue::HashicorpCloudVault("PREFIX_secret".to_string());
873 assert!(plain.starts_with("PREFIX"));
874 assert!(env.starts_with("PREFIX"));
875 assert!(vault.starts_with("PREFIX"));
876 assert!(!plain.starts_with("NOPE"));
877 assert!(!env.starts_with("NOPE"));
878 assert!(!vault.starts_with("NOPE"));
879 }
880
881 #[test]
882 fn test_secret_value_is_empty() {
883 let plain = SecretValue::Plain(SecretString::new("".to_string()));
884 let env = SecretValue::Environment("".to_string());
885 let vault = SecretValue::HashicorpCloudVault("".to_string());
886 assert!(plain.is_empty());
887 assert!(env.is_empty());
888 assert!(vault.is_empty());
889
890 let plain2 = SecretValue::Plain(SecretString::new("notempty".to_string()));
891 let env2 = SecretValue::Environment("notempty".to_string());
892 let vault2 = SecretValue::HashicorpCloudVault("notempty".to_string());
893 assert!(!plain2.is_empty());
894 assert!(!env2.is_empty());
895 assert!(!vault2.is_empty());
896 }
897
898 #[test]
899 fn test_secret_value_trim() {
900 let plain = SecretValue::Plain(SecretString::new(" plainval ".to_string()));
901 let env = SecretValue::Environment(" foo ".to_string());
902 let vault = SecretValue::HashicorpCloudVault(" bar ".to_string());
903 assert_eq!(plain.trim(), "plainval");
904 assert_eq!(env.trim(), "foo");
905 assert_eq!(vault.trim(), "bar");
906 }
907
908 #[test]
909 fn test_secret_value_as_str() {
910 let plain = SecretValue::Plain(SecretString::new("plainval".to_string()));
911 let env = SecretValue::Environment("envval".to_string());
912 let vault = SecretValue::HashicorpCloudVault("vaultval".to_string());
913 assert_eq!(plain.as_str(), "plainval");
914 assert_eq!(env.as_str(), "envval");
915 assert_eq!(vault.as_str(), "vaultval");
916 }
917
918 #[test]
919 fn test_secret_string_from_string() {
920 let s: SecretString = String::from("foo").into();
921 assert_eq!(s.as_str(), "foo");
922 }
923
924 #[test]
925 fn test_secret_value_display() {
926 let plain = SecretValue::Plain(SecretString::new("plainval".to_string()));
927 let env = SecretValue::Environment("envval".to_string());
928 let vault = SecretValue::HashicorpCloudVault("vaultval".to_string());
929 assert_eq!(format!("{}", plain), "<secret string>");
930 assert_eq!(format!("{}", env), "envval");
931 assert_eq!(format!("{}", vault), "vaultval");
932 }
933
934 #[test]
935 fn test_secret_value_as_ref() {
936 let plain = SecretValue::Plain(SecretString::new("plainval".to_string()));
937 let env = SecretValue::Environment("envval".to_string());
938 let vault = SecretValue::HashicorpCloudVault("vaultval".to_string());
939 assert_eq!(plain.as_ref(), "plainval");
940 assert_eq!(env.as_ref(), "envval");
941 assert_eq!(vault.as_ref(), "vaultval");
942 }
943
944 #[test]
945 fn test_case_insensitive_deserialization() {
946 let uppercase_json = r#"{"type":"PLAIN","value":"test_secret"}"#;
948 let uppercase_result: Result<SecretValue, _> = serde_json::from_str(uppercase_json);
949 assert!(
950 uppercase_result.is_ok(),
951 "Failed to deserialize uppercase variant: {:?}",
952 uppercase_result.err()
953 );
954
955 if let Ok(ref secret_value) = uppercase_result {
956 match secret_value {
957 SecretValue::Plain(secret) => assert_eq!(secret.as_str(), "test_secret"),
958 _ => panic!("Expected Plain variant"),
959 }
960 }
961
962 let lowercase_json = r#"{"type":"plain","value":"test_secret"}"#;
964 let lowercase_result: Result<SecretValue, _> = serde_json::from_str(lowercase_json);
965 assert!(
966 lowercase_result.is_ok(),
967 "Failed to deserialize lowercase variant: {:?}",
968 lowercase_result.err()
969 );
970
971 if let Ok(ref secret_value) = lowercase_result {
972 match secret_value {
973 SecretValue::Plain(secret) => assert_eq!(secret.as_str(), "test_secret"),
974 _ => panic!("Expected Plain variant"),
975 }
976 }
977
978 let mixedcase_json = r#"{"type":"pLaIn","value":"test_secret"}"#;
980 let mixedcase_result: Result<SecretValue, _> = serde_json::from_str(mixedcase_json);
981 assert!(
982 mixedcase_result.is_ok(),
983 "Failed to deserialize mixed case variant: {:?}",
984 mixedcase_result.err()
985 );
986
987 if let Ok(ref secret_value) = mixedcase_result {
988 match secret_value {
989 SecretValue::Plain(secret) => assert_eq!(secret.as_str(), "test_secret"),
990 _ => panic!("Expected Plain variant"),
991 }
992 }
993
994 let env_json = r#"{"type":"environment","value":"ENV_VAR"}"#;
996 let env_result: Result<SecretValue, _> = serde_json::from_str(env_json);
997 assert!(env_result.is_ok());
998
999 if let Ok(ref secret_value) = env_result {
1000 match secret_value {
1001 SecretValue::Environment(env_var) => assert_eq!(env_var, "ENV_VAR"),
1002 _ => panic!("Expected Environment variant"),
1003 }
1004 }
1005
1006 let vault_json = r#"{"type":"hashicorpcloudvault","value":"secret_name"}"#;
1008 let vault_result: Result<SecretValue, _> = serde_json::from_str(vault_json);
1009 assert!(vault_result.is_ok());
1010
1011 if let Ok(ref secret_value) = vault_result {
1012 match secret_value {
1013 SecretValue::HashicorpCloudVault(name) => assert_eq!(name, "secret_name"),
1014 _ => panic!("Expected HashicorpCloudVault variant"),
1015 }
1016 }
1017 }
1018}