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(Debug, 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(secret) => write!(f, "{}", secret.as_str()),
300 SecretValue::Environment(env_var) => write!(f, "{}", env_var),
301 SecretValue::HashicorpCloudVault(name) => write!(f, "{}", name),
302 }
303 }
304}
305
306impl AsRef<str> for SecretValue {
307 fn as_ref(&self) -> &str {
308 match self {
309 SecretValue::Plain(secret) => secret.as_ref(),
310 SecretValue::Environment(env_var) => env_var,
311 SecretValue::HashicorpCloudVault(name) => name,
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use lazy_static::lazy_static;
320 use std::sync::atomic::{AtomicBool, Ordering};
321 use std::sync::Mutex;
322 use zeroize::Zeroize;
323
324 lazy_static! {
326 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
327 }
328
329 #[allow(clippy::await_holding_lock)]
331 async fn with_test_env<F, Fut>(f: F)
332 where
333 F: FnOnce() -> Fut,
334 Fut: std::future::Future<Output = ()>,
335 {
336 let _lock = ENV_MUTEX.lock().unwrap();
338
339 let env_vars = [
340 ("HCP_CLIENT_ID", "test-client-id"),
341 ("HCP_CLIENT_SECRET", "test-client-secret"),
342 ("HCP_ORG_ID", "test-org"),
343 ("HCP_PROJECT_ID", "test-project"),
344 ("HCP_APP_NAME", "test-app"),
345 ];
346
347 let original_values: Vec<_> = env_vars
349 .iter()
350 .map(|(key, _)| (*key, std::env::var(key).ok()))
351 .collect();
352
353 for (key, value) in env_vars.iter() {
355 std::env::set_var(key, value);
356 }
357
358 f().await;
360
361 for (key, value) in original_values {
363 match value {
364 Some(val) => std::env::set_var(key, val),
365 None => std::env::remove_var(key),
366 }
367 }
368 }
369
370 struct TrackedSecret<T: Zeroize> {
372 inner: T,
373 was_zeroized: Arc<AtomicBool>,
374 }
375
376 impl<T: Zeroize> TrackedSecret<T> {
377 fn new(value: T, was_zeroized: Arc<AtomicBool>) -> Self {
378 Self {
379 inner: value,
380 was_zeroized,
381 }
382 }
383 }
384
385 impl<T: Zeroize> Zeroize for TrackedSecret<T> {
386 fn zeroize(&mut self) {
387 self.was_zeroized.store(true, Ordering::SeqCst);
388 self.inner.zeroize();
389 }
390 }
391
392 impl<T: Zeroize> Drop for TrackedSecret<T> {
393 fn drop(&mut self) {
394 self.zeroize();
395 }
396 }
397
398 #[test]
400 fn test_secret_string_zeroize_on_drop() {
401 let was_zeroized = Arc::new(AtomicBool::new(false));
402 let secret = "sensitive_data".to_string();
403 let secret_string =
404 TrackedSecret::new(SecretString::new(secret.clone()), was_zeroized.clone());
405
406 assert_eq!(secret_string.inner.as_str(), secret);
408 assert!(!was_zeroized.load(Ordering::SeqCst));
409
410 {
412 let _secret_string = secret_string;
413 assert_eq!(_secret_string.inner.as_str(), secret);
415 assert!(!was_zeroized.load(Ordering::SeqCst));
416 }
417
418 assert!(was_zeroized.load(Ordering::SeqCst));
420 }
421
422 #[test]
424 fn test_secret_value_zeroize_on_drop() {
425 let was_zeroized = Arc::new(AtomicBool::new(false));
426 let secret = "sensitive_data".to_string();
427 let secret_value = TrackedSecret::new(
428 SecretValue::Plain(SecretString::new(secret.clone())),
429 was_zeroized.clone(),
430 );
431
432 assert_eq!(secret_value.inner.as_str(), secret);
434 assert!(!was_zeroized.load(Ordering::SeqCst));
435
436 {
438 let _secret_value = secret_value;
439 assert_eq!(_secret_value.inner.as_str(), secret);
441 assert!(!was_zeroized.load(Ordering::SeqCst));
442 }
443
444 assert!(was_zeroized.load(Ordering::SeqCst));
446 }
447
448 #[tokio::test]
450 async fn test_environment_secret() {
451 const TEST_ENV_VAR: &str = "TEST_SECRET_ENV_VAR";
452 const TEST_SECRET: &str = "test_secret_value";
453
454 env::set_var(TEST_ENV_VAR, TEST_SECRET);
455
456 let secret = SecretValue::Environment(TEST_ENV_VAR.to_string());
457 let resolved = secret.resolve().await.unwrap();
458
459 assert_eq!(resolved.as_str(), TEST_SECRET);
460
461 env::remove_var(TEST_ENV_VAR);
462 }
463
464 #[test]
466 fn test_secret_string_zeroize() {
467 let secret = "sensitive_data".to_string();
468 let mut secret_string = SecretString::new(secret.clone());
469
470 assert_eq!(secret_string.as_str(), secret);
471
472 secret_string.zeroize();
474 assert_eq!(secret_string.as_str(), "");
475 }
476
477 #[test]
479 fn test_secret_value_zeroize() {
480 let mut plain_secret = SecretValue::Plain(SecretString::new("plain_secret".to_string()));
481 let mut env_secret = SecretValue::Environment("ENV_VAR".to_string());
482 let mut cloud_vault_secret = SecretValue::HashicorpCloudVault("secret_name".to_string());
483
484 plain_secret.zeroize();
485 env_secret.zeroize();
486 cloud_vault_secret.zeroize();
487
488 if let SecretValue::Plain(ref secret) = plain_secret {
490 assert_eq!(secret.as_str(), "");
491 }
492
493 if let SecretValue::Environment(ref env_var) = env_secret {
494 assert_eq!(env_var, "");
495 }
496 if let SecretValue::HashicorpCloudVault(ref name) = cloud_vault_secret {
497 assert_eq!(name, "");
498 }
499 }
500
501 #[tokio::test]
502 async fn test_cloud_vault_client_from_env_success() {
503 with_test_env(|| async {
504 let result = CloudVaultClient::from_env();
505 assert!(result.is_ok());
506 })
507 .await;
508 }
509
510 #[tokio::test]
511 async fn test_cloud_vault_client_from_env_missing_vars() {
512 with_test_env(|| async {
513 std::env::remove_var("HCP_CLIENT_ID");
515 let result = CloudVaultClient::from_env();
516 assert!(result.is_err());
517 assert!(result.err().unwrap().to_string().contains("HCP_CLIENT_ID"));
518 })
519 .await;
520
521 with_test_env(|| async {
522 std::env::remove_var("HCP_CLIENT_SECRET");
524 let result = CloudVaultClient::from_env();
525 assert!(result.is_err());
526 assert!(result
527 .err()
528 .unwrap()
529 .to_string()
530 .contains("HCP_CLIENT_SECRET"));
531 })
532 .await;
533 }
534
535 #[tokio::test]
536 async fn test_vault_type_from_env() {
537 with_test_env(|| async {
538 let result = VaultType::from_env();
539 assert!(result.is_ok());
540 match result.unwrap() {
541 VaultType::Cloud(_) => (), }
543 })
544 .await;
545 }
546
547 #[tokio::test]
548 async fn test_get_vault_client() {
549 with_test_env(|| async {
550 std::env::remove_var("HCP_CLIENT_ID");
554 let result = get_vault_client().await;
555 assert!(result.is_err());
556 assert!(result
557 .err()
558 .unwrap()
559 .to_string()
560 .contains("Failed to get vault client"));
561
562 std::env::set_var("HCP_CLIENT_ID", "test-client-id");
564
565 let result = get_vault_client().await;
567 assert!(result.is_ok());
568 let client = result.unwrap();
569 match client {
570 VaultType::Cloud(_) => (), }
572
573 let result2 = get_vault_client().await;
575 assert!(result2.is_ok());
576 assert!(std::ptr::eq(client, result2.unwrap()));
577 })
578 .await;
579 }
580
581 #[tokio::test]
582 async fn test_vault_client_get_secret() {
583 let mut server = mockito::Server::new_async().await;
584 let token_mock = server
586 .mock("POST", "/oauth2/token")
587 .with_status(200)
588 .with_header("content-type", "application/json")
589 .with_body(
590 r#"{"access_token": "test-token", "token_type": "Bearer", "expires_in": 3600}"#,
591 )
592 .create_async()
593 .await;
594
595 let secret_mock = server
597 .mock(
598 "GET",
599 "/secrets/2023-11-28/organizations/test-org/projects/test-project/apps/test-app/secrets/test-secret:open",
600 )
601 .with_status(200)
602 .with_header("content-type", "application/json")
603 .with_body(r#"{"secret": {"static_version": {"value": "test-secret-value"}}}"#)
604 .create_async()
605 .await;
606
607 let hashicorp_client = HashicorpCloudClient::new(
609 "test-client-id".to_string(),
610 "test-client-secret".to_string(),
611 "test-org".to_string(),
612 "test-project".to_string(),
613 "test-app".to_string(),
614 )
615 .with_api_base_url(server.url())
616 .with_auth_base_url(server.url());
617
618 let vault_client = CloudVaultClient {
619 client: Arc::new(hashicorp_client),
620 };
621
622 let result = vault_client.get_secret("test-secret").await;
624
625 token_mock.assert_async().await;
627 secret_mock.assert_async().await;
628
629 assert!(result.is_ok());
631 assert_eq!(result.unwrap().as_str(), "test-secret-value");
632 }
633
634 #[tokio::test]
635 async fn test_vault_client_get_secret_error() {
636 with_test_env(|| async {
637 let mut server = mockito::Server::new_async().await;
639 let token_mock = server
640 .mock("POST", "/oauth2/token")
641 .with_status(500)
642 .with_header("content-type", "application/json")
643 .with_body(r#"{"error": "internal server error"}"#)
644 .create_async()
645 .await;
646
647 let hashicorp_client = HashicorpCloudClient::new(
649 "test-client-id".to_string(),
650 "test-client-secret".to_string(),
651 "test-org".to_string(),
652 "test-project".to_string(),
653 "test-app".to_string(),
654 )
655 .with_api_base_url(server.url())
656 .with_auth_base_url(server.url());
657
658 let vault_client = CloudVaultClient {
659 client: Arc::new(hashicorp_client),
660 };
661
662 let result = vault_client.get_secret("test-secret").await;
663
664 token_mock.assert_async().await;
666
667 assert!(result.is_err());
669 assert!(result
670 .err()
671 .unwrap()
672 .to_string()
673 .contains("Failed to get secret from Hashicorp Cloud Vault"));
674 })
675 .await;
676 }
677
678 #[tokio::test]
679 async fn test_vault_type_clone() {
680 with_test_env(|| async {
681 let vault_type = VaultType::from_env().unwrap();
682 let cloned = vault_type.clone();
683
684 match (vault_type, cloned) {
685 (VaultType::Cloud(_), VaultType::Cloud(_)) => (), }
687 })
688 .await;
689 }
690
691 #[test]
692 fn test_cloud_vault_client_new_wraps_arc() {
693 let dummy = HashicorpCloudClient::new(
694 "id".to_string(),
695 "secret".to_string(),
696 "org".to_string(),
697 "proj".to_string(),
698 "app".to_string(),
699 );
700 let client = CloudVaultClient {
701 client: Arc::new(dummy),
702 };
703 assert!(Arc::strong_count(&client.client) >= 1);
705 }
706
707 #[tokio::test]
708 async fn test_cloud_vault_client_from_env_missing_org_id() {
709 with_test_env(|| async {
710 std::env::remove_var("HCP_ORG_ID");
711 let result = CloudVaultClient::from_env();
712 assert!(result.is_err());
713 assert!(result.err().unwrap().to_string().contains("HCP_ORG_ID"));
714 })
715 .await;
716 }
717
718 #[tokio::test]
719 async fn test_cloud_vault_client_from_env_missing_project_id() {
720 with_test_env(|| async {
721 std::env::remove_var("HCP_PROJECT_ID");
722 let result = CloudVaultClient::from_env();
723 assert!(result.is_err());
724 assert!(result.err().unwrap().to_string().contains("HCP_PROJECT_ID"));
725 })
726 .await;
727 }
728
729 #[tokio::test]
730 async fn test_cloud_vault_client_from_env_missing_app_name() {
731 with_test_env(|| async {
732 std::env::remove_var("HCP_APP_NAME");
733 let result = CloudVaultClient::from_env();
734 assert!(result.is_err());
735 assert!(result.err().unwrap().to_string().contains("HCP_APP_NAME"));
736 })
737 .await;
738 }
739
740 #[tokio::test]
741 async fn test_cloud_vault_client_from_env_missing_client_id() {
742 with_test_env(|| async {
743 std::env::remove_var("HCP_CLIENT_ID");
744 let result = CloudVaultClient::from_env();
745 assert!(result.is_err());
746 assert!(result.err().unwrap().to_string().contains("HCP_CLIENT_ID"));
747 })
748 .await;
749 }
750
751 #[tokio::test]
752 async fn test_cloud_vault_client_from_env_missing_client_secret() {
753 with_test_env(|| async {
754 std::env::remove_var("HCP_CLIENT_SECRET");
755 let result = CloudVaultClient::from_env();
756 assert!(result.is_err());
757 assert!(result
758 .err()
759 .unwrap()
760 .to_string()
761 .contains("HCP_CLIENT_SECRET"));
762 })
763 .await;
764 }
765
766 #[tokio::test]
767 async fn test_vault_type_get_secret_delegates() {
768 with_test_env(|| async {
769 let vault = VaultType::from_env().unwrap();
770 let result = vault.get_secret("nonexistent").await;
771 assert!(
772 result.is_err(),
773 "Expected error for nonexistent secret, got: {:?}",
774 result
775 );
776 })
777 .await;
778 }
779
780 #[test]
781 fn test_secret_value_partial_eq_false_for_different_variants() {
782 let a = SecretValue::Plain(SecretString::new("a".to_string()));
783 let b = SecretValue::Environment("a".to_string());
784 let c = SecretValue::HashicorpCloudVault("a".to_string());
785 assert_ne!(a, b);
786 assert_ne!(a, c);
787 assert_ne!(b, c);
788 }
789
790 #[test]
791 fn test_secret_string_partial_eq() {
792 let a = SecretString::new("foo".to_string());
793 let b = SecretString::new("foo".to_string());
794 let c = SecretString::new("bar".to_string());
795 assert_eq!(a, b);
796 assert_ne!(a, c);
797 }
798
799 #[tokio::test]
800 async fn test_secret_value_resolve_env_error() {
801 let secret = SecretValue::Environment("NON_EXISTENT_ENV_VAR".to_string());
802 let result = secret.resolve().await;
803 assert!(result.is_err());
804 assert!(result
805 .err()
806 .unwrap()
807 .to_string()
808 .contains("Failed to get environment variable"));
809 }
810
811 #[tokio::test]
812 async fn test_secret_value_resolve_hashicorp_cloud_vault_error() {
813 with_test_env(|| async {
814 let secret = SecretValue::HashicorpCloudVault("NON_EXISTENT_VAULT_SECRET".to_string());
815 let result = secret.resolve().await;
816 assert!(result.is_err());
817 assert!(result
818 .err()
819 .unwrap()
820 .to_string()
821 .contains("Failed to get secret from Hashicorp Cloud Vault"));
822 })
823 .await;
824 }
825
826 #[test]
827 fn test_secret_value_starts_with() {
828 let plain = SecretValue::Plain(SecretString::new("PREFIX_value".to_string()));
829 let env = SecretValue::Environment("PREFIX_value".to_string());
830 let vault = SecretValue::HashicorpCloudVault("PREFIX_secret".to_string());
831 assert!(plain.starts_with("PREFIX"));
832 assert!(env.starts_with("PREFIX"));
833 assert!(vault.starts_with("PREFIX"));
834 assert!(!plain.starts_with("NOPE"));
835 assert!(!env.starts_with("NOPE"));
836 assert!(!vault.starts_with("NOPE"));
837 }
838
839 #[test]
840 fn test_secret_value_is_empty() {
841 let plain = SecretValue::Plain(SecretString::new("".to_string()));
842 let env = SecretValue::Environment("".to_string());
843 let vault = SecretValue::HashicorpCloudVault("".to_string());
844 assert!(plain.is_empty());
845 assert!(env.is_empty());
846 assert!(vault.is_empty());
847
848 let plain2 = SecretValue::Plain(SecretString::new("notempty".to_string()));
849 let env2 = SecretValue::Environment("notempty".to_string());
850 let vault2 = SecretValue::HashicorpCloudVault("notempty".to_string());
851 assert!(!plain2.is_empty());
852 assert!(!env2.is_empty());
853 assert!(!vault2.is_empty());
854 }
855
856 #[test]
857 fn test_secret_value_trim() {
858 let plain = SecretValue::Plain(SecretString::new(" plainval ".to_string()));
859 let env = SecretValue::Environment(" foo ".to_string());
860 let vault = SecretValue::HashicorpCloudVault(" bar ".to_string());
861 assert_eq!(plain.trim(), "plainval");
862 assert_eq!(env.trim(), "foo");
863 assert_eq!(vault.trim(), "bar");
864 }
865
866 #[test]
867 fn test_secret_value_as_str() {
868 let plain = SecretValue::Plain(SecretString::new("plainval".to_string()));
869 let env = SecretValue::Environment("envval".to_string());
870 let vault = SecretValue::HashicorpCloudVault("vaultval".to_string());
871 assert_eq!(plain.as_str(), "plainval");
872 assert_eq!(env.as_str(), "envval");
873 assert_eq!(vault.as_str(), "vaultval");
874 }
875
876 #[test]
877 fn test_secret_string_from_string() {
878 let s: SecretString = String::from("foo").into();
879 assert_eq!(s.as_str(), "foo");
880 }
881
882 #[test]
883 fn test_secret_value_display() {
884 let plain = SecretValue::Plain(SecretString::new("plainval".to_string()));
885 let env = SecretValue::Environment("envval".to_string());
886 let vault = SecretValue::HashicorpCloudVault("vaultval".to_string());
887 assert_eq!(format!("{}", plain), "plainval");
888 assert_eq!(format!("{}", env), "envval");
889 assert_eq!(format!("{}", vault), "vaultval");
890 }
891
892 #[test]
893 fn test_secret_value_as_ref() {
894 let plain = SecretValue::Plain(SecretString::new("plainval".to_string()));
895 let env = SecretValue::Environment("envval".to_string());
896 let vault = SecretValue::HashicorpCloudVault("vaultval".to_string());
897 assert_eq!(plain.as_ref(), "plainval");
898 assert_eq!(env.as_ref(), "envval");
899 assert_eq!(vault.as_ref(), "vaultval");
900 }
901
902 #[test]
903 fn test_case_insensitive_deserialization() {
904 let uppercase_json = r#"{"type":"PLAIN","value":"test_secret"}"#;
906 let uppercase_result: Result<SecretValue, _> = serde_json::from_str(uppercase_json);
907 assert!(
908 uppercase_result.is_ok(),
909 "Failed to deserialize uppercase variant: {:?}",
910 uppercase_result.err()
911 );
912
913 if let Ok(ref secret_value) = uppercase_result {
914 match secret_value {
915 SecretValue::Plain(secret) => assert_eq!(secret.as_str(), "test_secret"),
916 _ => panic!("Expected Plain variant"),
917 }
918 }
919
920 let lowercase_json = r#"{"type":"plain","value":"test_secret"}"#;
922 let lowercase_result: Result<SecretValue, _> = serde_json::from_str(lowercase_json);
923 assert!(
924 lowercase_result.is_ok(),
925 "Failed to deserialize lowercase variant: {:?}",
926 lowercase_result.err()
927 );
928
929 if let Ok(ref secret_value) = lowercase_result {
930 match secret_value {
931 SecretValue::Plain(secret) => assert_eq!(secret.as_str(), "test_secret"),
932 _ => panic!("Expected Plain variant"),
933 }
934 }
935
936 let mixedcase_json = r#"{"type":"pLaIn","value":"test_secret"}"#;
938 let mixedcase_result: Result<SecretValue, _> = serde_json::from_str(mixedcase_json);
939 assert!(
940 mixedcase_result.is_ok(),
941 "Failed to deserialize mixed case variant: {:?}",
942 mixedcase_result.err()
943 );
944
945 if let Ok(ref secret_value) = mixedcase_result {
946 match secret_value {
947 SecretValue::Plain(secret) => assert_eq!(secret.as_str(), "test_secret"),
948 _ => panic!("Expected Plain variant"),
949 }
950 }
951
952 let env_json = r#"{"type":"environment","value":"ENV_VAR"}"#;
954 let env_result: Result<SecretValue, _> = serde_json::from_str(env_json);
955 assert!(env_result.is_ok());
956
957 if let Ok(ref secret_value) = env_result {
958 match secret_value {
959 SecretValue::Environment(env_var) => assert_eq!(env_var, "ENV_VAR"),
960 _ => panic!("Expected Environment variant"),
961 }
962 }
963
964 let vault_json = r#"{"type":"hashicorpcloudvault","value":"secret_name"}"#;
966 let vault_result: Result<SecretValue, _> = serde_json::from_str(vault_json);
967 assert!(vault_result.is_ok());
968
969 if let Ok(ref secret_value) = vault_result {
970 match secret_value {
971 SecretValue::HashicorpCloudVault(name) => assert_eq!(name, "secret_name"),
972 _ => panic!("Expected HashicorpCloudVault variant"),
973 }
974 }
975 }
976}