1use crate::{
52 cache::{CacheStats, CachedSecret},
53 config::ClientConfig,
54 endpoints::Endpoints,
55 errors::{Error, ErrorResponse, Result},
56 models::*,
57 util::{generate_request_id, header_str},
58};
59
60#[cfg(feature = "metrics")]
61use crate::telemetry;
62use backoff::{future::retry_notify, ExponentialBackoff};
63use moka::future::Cache;
64use reqwest::{Client as HttpClient, Method, Response, StatusCode};
65use secrecy::SecretString;
66use std::time::Duration;
67use tracing::{debug, trace, warn};
68
69const USER_AGENT_PREFIX: &str = "xjp-secret-store-sdk-rust";
70
71#[derive(Clone)]
77pub struct Client {
78 pub(crate) config: ClientConfig,
79 http: HttpClient,
80 endpoints: Endpoints,
81 cache: Option<Cache<String, CachedSecret>>,
82 stats: CacheStats,
83 #[cfg(feature = "metrics")]
84 metrics: std::sync::Arc<telemetry::Metrics>,
85}
86
87impl std::fmt::Debug for Client {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("Client")
90 .field("base_url", &self.config.base_url)
91 .field("timeout", &self.config.timeout)
92 .field("retries", &self.config.retries)
93 .field("cache_enabled", &self.config.cache_config.enabled)
94 .finish()
95 }
96}
97
98impl Client {
99 pub(crate) fn new(config: ClientConfig) -> Result<Self> {
101 let user_agent = if let Some(suffix) = &config.user_agent_suffix {
103 format!("{}/{} {}", USER_AGENT_PREFIX, crate::VERSION, suffix)
104 } else {
105 format!("{}/{}", USER_AGENT_PREFIX, crate::VERSION)
106 };
107
108 let mut http_builder = HttpClient::builder()
110 .user_agent(user_agent)
111 .timeout(config.timeout)
112 .pool_idle_timeout(Duration::from_secs(90))
113 .pool_max_idle_per_host(10)
114 .http2_prior_knowledge();
115
116 #[cfg(not(feature = "danger-insecure-http"))]
118 {
119 http_builder = http_builder.https_only(true);
120 }
121
122 #[cfg(feature = "danger-insecure-http")]
123 {
124 if config.allow_insecure_http {
125 http_builder = http_builder.danger_accept_invalid_certs(true);
126 }
127 }
128
129 let http = http_builder
130 .build()
131 .map_err(|e| Error::Config(format!("Failed to build HTTP client: {}", e)))?;
132
133 let cache = if config.cache_config.enabled {
135 Some(
136 Cache::builder()
137 .max_capacity(config.cache_config.max_entries)
138 .time_to_live(Duration::from_secs(config.cache_config.default_ttl_secs))
139 .build(),
140 )
141 } else {
142 None
143 };
144
145 #[cfg(feature = "metrics")]
147 let metrics = if config.telemetry_config.enabled {
148 telemetry::init_telemetry(config.telemetry_config.clone())
149 } else {
150 std::sync::Arc::new(telemetry::Metrics::new(&config.telemetry_config))
151 };
152
153 Ok(Self {
154 endpoints: Endpoints::new(&config.base_url),
155 http,
156 cache,
157 stats: CacheStats::new(),
158 #[cfg(feature = "metrics")]
159 metrics,
160 config,
161 })
162 }
163
164 pub fn cache_stats(&self) -> &CacheStats {
180 &self.stats
181 }
182
183 pub fn clear_cache(&self) {
198 if let Some(cache) = &self.cache {
199 cache.invalidate_all();
200 self.stats.reset();
201 }
202 }
203
204 pub async fn invalidate_cache(&self, namespace: &str, key: &str) {
225 if let Some(cache) = &self.cache {
226 let cache_key = format!("{}/{}", namespace, key);
227 cache.invalidate(&cache_key).await;
228 }
229 }
230
231 pub async fn get_secret(&self, namespace: &str, key: &str, opts: GetOpts) -> Result<Secret> {
282 let cache_key = format!("{}/{}", namespace, key);
283
284 if opts.use_cache {
286 if let Some(cached) = self.get_from_cache(&cache_key).await {
287 return Ok(cached);
288 }
289 }
290
291 let url = self.endpoints.get_secret(namespace, key);
293 let mut request = self.build_request(Method::GET, &url)?;
294
295 if let Some(etag) = &opts.if_none_match {
297 request = request.header(reqwest::header::IF_NONE_MATCH, etag);
298 }
299 if let Some(modified) = &opts.if_modified_since {
300 request = request.header(reqwest::header::IF_MODIFIED_SINCE, modified);
301 }
302
303 let response = self.execute_with_retry(request).await?;
305
306 if response.status() == StatusCode::NOT_MODIFIED {
308 if let Some(cached) = self.get_from_cache(&cache_key).await {
310 return Ok(cached);
311 }
312 return Err(Error::Other(
314 "Server returned 304 but no cached entry found".to_string(),
315 ));
316 }
317
318 let secret = self.parse_get_response(response, namespace, key).await?;
320
321 if self.config.cache_config.enabled && opts.use_cache {
323 self.cache_secret(&cache_key, &secret).await;
324 }
325
326 Ok(secret)
327 }
328
329 pub async fn put_secret(
374 &self,
375 namespace: &str,
376 key: &str,
377 value: impl Into<String>,
378 opts: PutOpts,
379 ) -> Result<PutResult> {
380 if let Some(cache) = &self.cache {
382 let cache_key = format!("{}/{}", namespace, key);
383 cache.invalidate(&cache_key).await;
384 }
385
386 let mut body = serde_json::json!({
388 "value": value.into(),
389 });
390
391 if let Some(ttl) = opts.ttl_seconds {
392 body["ttl_seconds"] = serde_json::json!(ttl);
393 }
394 if let Some(metadata) = opts.metadata {
395 body["metadata"] = metadata;
396 }
397
398 let url = self.endpoints.put_secret(namespace, key);
400 let mut request = self.build_request(Method::PUT, &url)?;
401 request = request.json(&body);
402
403 if let Some(idempotency_key) = &opts.idempotency_key {
405 request = request.header("X-Idempotency-Key", idempotency_key);
406 }
407
408 let response = self.execute_with_retry(request).await?;
410
411 self.parse_json_response(response).await
413 }
414
415 pub async fn delete_secret(&self, namespace: &str, key: &str) -> Result<DeleteResult> {
417 if let Some(cache) = &self.cache {
419 let cache_key = format!("{}/{}", namespace, key);
420 cache.invalidate(&cache_key).await;
421 }
422
423 let url = self.endpoints.delete_secret(namespace, key);
425 let request = self.build_request(Method::DELETE, &url)?;
426
427 let response = self.execute_with_retry(request).await?;
429 let request_id = header_str(response.headers(), "x-request-id");
430
431 let deleted = response.status() == StatusCode::NO_CONTENT;
433
434 Ok(DeleteResult {
435 deleted,
436 request_id,
437 })
438 }
439
440 pub async fn list_secrets(&self, namespace: &str, opts: ListOpts) -> Result<ListSecretsResult> {
442 let mut url = self.endpoints.list_secrets(namespace);
444
445 let mut query_parts = Vec::new();
446 if let Some(prefix) = &opts.prefix {
447 query_parts.push(format!(
448 "prefix={}",
449 percent_encoding::utf8_percent_encode(prefix, percent_encoding::NON_ALPHANUMERIC)
450 ));
451 }
452 if let Some(limit) = opts.limit {
453 query_parts.push(format!("limit={}", limit));
454 }
455
456 if !query_parts.is_empty() {
457 url.push('?');
458 url.push_str(&query_parts.join("&"));
459 }
460
461 let request = self.build_request(Method::GET, &url)?;
463 let response = self.execute_with_retry(request).await?;
464
465 self.parse_json_response(response).await
467 }
468
469 pub async fn batch_get(
471 &self,
472 namespace: &str,
473 keys: BatchKeys,
474 format: ExportFormat,
475 ) -> Result<BatchGetResult> {
476 let mut url = self.endpoints.batch_get(namespace);
477
478 match &keys {
480 BatchKeys::Keys(key_list) => {
481 let keys_param = key_list.join(",");
482 url.push_str(&format!(
483 "?keys={}",
484 percent_encoding::utf8_percent_encode(
485 &keys_param,
486 percent_encoding::NON_ALPHANUMERIC
487 )
488 ));
489 }
490 BatchKeys::All => {
491 url.push_str("?wildcard=true");
492 }
493 }
494
495 let separator = if url.contains('?') { '&' } else { '?' };
497 url.push_str(&format!("{}format={}", separator, format.as_str()));
498
499 let request = self.build_request(Method::GET, &url)?;
501 let response = self.execute_with_retry(request).await?;
502
503 if !response.status().is_success() {
505 return Err(self.parse_error_response(response).await);
506 }
507
508 match format {
510 ExportFormat::Json => {
511 let json_result: BatchGetJsonResult = response.json().await.map_err(Error::from)?;
512 Ok(BatchGetResult::Json(json_result))
513 }
514 _ => {
515 let text = response.text().await.map_err(Error::from)?;
516 Ok(BatchGetResult::Text(text))
517 }
518 }
519 }
520
521 pub async fn batch_operate(
523 &self,
524 namespace: &str,
525 operations: Vec<BatchOp>,
526 transactional: bool,
527 idempotency_key: Option<String>,
528 ) -> Result<BatchOperateResult> {
529 if let Some(cache) = &self.cache {
531 for op in &operations {
532 let cache_key = format!("{}/{}", namespace, &op.key);
533 cache.invalidate(&cache_key).await;
534 }
535 }
536
537 let body = serde_json::json!({
539 "operations": operations,
540 "transactional": transactional,
541 });
542
543 let url = self.endpoints.batch_operate(namespace);
545 let mut request = self.build_request(Method::POST, &url)?;
546 request = request.json(&body);
547
548 if let Some(key) = idempotency_key {
550 request = request.header("X-Idempotency-Key", key);
551 }
552
553 let response = self.execute_with_retry(request).await?;
555
556 self.parse_json_response(response).await
558 }
559
560 pub async fn export_env(&self, namespace: &str, opts: ExportEnvOpts) -> Result<EnvExport> {
606 let mut url = self.endpoints.export_env(namespace);
607 url.push_str(&format!("?format={}", opts.format.as_str()));
608
609 let mut request = self.build_request(Method::GET, &url)?;
611
612 if let Some(etag) = &opts.if_none_match {
614 request = request.header(reqwest::header::IF_NONE_MATCH, etag);
615 }
616
617 let response = self.execute_with_retry(request).await?;
618
619 if response.status() == StatusCode::NOT_MODIFIED {
621 return Err(Error::Http {
622 status: 304,
623 category: "not_modified".to_string(),
624 message: "Environment export not modified".to_string(),
625 request_id: header_str(response.headers(), "x-request-id"),
626 });
627 }
628
629 if !response.status().is_success() {
631 return Err(self.parse_error_response(response).await);
632 }
633
634 match opts.format {
641 ExportFormat::Json => {
642 let json_result: EnvJsonExport = response.json().await.map_err(Error::from)?;
643 Ok(EnvExport::Json(json_result))
644 }
645 _ => {
646 let text = response.text().await.map_err(Error::from)?;
647 Ok(EnvExport::Text(text))
648 }
649 }
650 }
651
652 pub async fn list_namespaces(&self) -> Result<ListNamespacesResult> {
654 let url = self.endpoints.list_namespaces();
655 let request = self.build_request(Method::GET, &url)?;
656 let response = self.execute_with_retry(request).await?;
657
658 if !response.status().is_success() {
659 return Err(self.parse_error_response(response).await);
660 }
661
662 self.parse_json_response(response).await
663 }
664
665 pub async fn create_namespace(
700 &self,
701 name: &str,
702 description: Option<String>,
703 idempotency_key: Option<String>,
704 ) -> Result<CreateNamespaceResult> {
705 let url = self.endpoints.create_namespace();
706 let mut request = self.build_request(Method::POST, &url)?;
707
708 let body = CreateNamespaceRequest {
709 name: name.to_string(),
710 description,
711 };
712 request = request.json(&body);
713
714 if let Some(key) = idempotency_key {
716 request = request.header("X-Idempotency-Key", key);
717 }
718
719 let response = self.execute_with_retry(request).await?;
720
721 if !response.status().is_success() {
722 return Err(self.parse_error_response(response).await);
723 }
724
725 self.parse_json_response(response).await
726 }
727
728 pub async fn get_namespace(&self, namespace: &str) -> Result<NamespaceInfo> {
730 let url = self.endpoints.get_namespace(namespace);
731 let request = self.build_request(Method::GET, &url)?;
732 let response = self.execute_with_retry(request).await?;
733
734 if !response.status().is_success() {
735 return Err(self.parse_error_response(response).await);
736 }
737
738 self.parse_json_response(response).await
739 }
740
741 pub async fn init_namespace(
776 &self,
777 namespace: &str,
778 template: NamespaceTemplate,
779 idempotency_key: Option<String>,
780 ) -> Result<InitNamespaceResult> {
781 let url = self.endpoints.init_namespace(namespace);
782 let mut request = self.build_request(Method::POST, &url)?;
783 request = request.json(&template);
784
785 if let Some(key) = idempotency_key {
787 request = request.header("X-Idempotency-Key", key);
788 }
789
790 let response = self.execute_with_retry(request).await?;
791
792 if !response.status().is_success() {
793 return Err(self.parse_error_response(response).await);
794 }
795
796 self.parse_json_response(response).await
797 }
798
799 pub async fn delete_namespace(&self, namespace: &str) -> Result<DeleteNamespaceResult> {
835 if let Some(cache) = &self.cache {
837 cache.invalidate_all();
840 debug!(
841 "Cleared all cache entries due to namespace deletion: {}",
842 namespace
843 );
844 }
845
846 let url = self.endpoints.delete_namespace(namespace);
848 let request = self.build_request(Method::DELETE, &url)?;
849
850 let response = self.execute_with_retry(request).await?;
852
853 if !response.status().is_success() {
855 return Err(self.parse_error_response(response).await);
856 }
857
858 let request_id = header_str(response.headers(), "x-request-id");
860
861 let mut result: DeleteNamespaceResult = self.parse_json_response(response).await?;
863
864 if result.request_id.is_none() {
866 result.request_id = request_id;
867 }
868
869 Ok(result)
870 }
871
872 pub async fn delete_namespace_idempotent(
895 &self,
896 namespace: &str,
897 idempotency_key: Option<String>,
898 ) -> Result<DeleteNamespaceResult> {
899 if let Some(cache) = &self.cache {
901 cache.invalidate_all();
902 debug!(
903 "Cleared all cache entries due to namespace deletion: {}",
904 namespace
905 );
906 }
907
908 let url = self.endpoints.delete_namespace(namespace);
910 let mut request = self.build_request(Method::DELETE, &url)?;
911
912 if let Some(key) = idempotency_key {
914 request = request.header("X-Idempotency-Key", key);
915 }
916
917 let response = self.execute_with_retry(request).await?;
919
920 if !response.status().is_success() {
922 return Err(self.parse_error_response(response).await);
923 }
924
925 let request_id = header_str(response.headers(), "x-request-id");
927
928 let mut result: DeleteNamespaceResult = self.parse_json_response(response).await?;
930
931 if result.request_id.is_none() {
933 result.request_id = request_id;
934 }
935
936 Ok(result)
937 }
938
939 pub async fn list_versions(&self, namespace: &str, key: &str) -> Result<VersionList> {
941 let url = self.endpoints.list_versions(namespace, key);
943 let request = self.build_request(Method::GET, &url)?;
944 let response = self.execute_with_retry(request).await?;
945
946 self.parse_json_response(response).await
948 }
949
950 pub async fn get_version(&self, namespace: &str, key: &str, version: i32) -> Result<Secret> {
952 let url = self.endpoints.get_version(namespace, key, version);
954 let request = self.build_request(Method::GET, &url)?;
955 let response = self.execute_with_retry(request).await?;
956
957 self.parse_get_response(response, namespace, key).await
959 }
960
961 pub async fn rollback(
963 &self,
964 namespace: &str,
965 key: &str,
966 version: i32,
967 ) -> Result<RollbackResult> {
968 if let Some(cache) = &self.cache {
970 let cache_key = format!("{}/{}", namespace, key);
971 cache.invalidate(&cache_key).await;
972 }
973
974 let url = self.endpoints.rollback(namespace, key, version);
976 let mut request = self.build_request(Method::POST, &url)?;
977 request = request.json(&serde_json::json!({}));
978
979 let response = self.execute_with_retry(request).await?;
981
982 self.parse_json_response(response).await
984 }
985
986 pub async fn audit(&self, query: AuditQuery) -> Result<AuditResult> {
988 let mut url = self.endpoints.audit();
990 let mut params = Vec::new();
991
992 if let Some(namespace) = &query.namespace {
994 params.push(format!(
995 "namespace={}",
996 percent_encoding::utf8_percent_encode(
997 namespace,
998 percent_encoding::NON_ALPHANUMERIC
999 )
1000 ));
1001 }
1002 if let Some(actor) = &query.actor {
1003 params.push(format!(
1004 "actor={}",
1005 percent_encoding::utf8_percent_encode(actor, percent_encoding::NON_ALPHANUMERIC)
1006 ));
1007 }
1008 if let Some(action) = &query.action {
1009 params.push(format!(
1010 "action={}",
1011 percent_encoding::utf8_percent_encode(action, percent_encoding::NON_ALPHANUMERIC)
1012 ));
1013 }
1014 if let Some(from) = &query.from {
1015 params.push(format!(
1016 "from={}",
1017 percent_encoding::utf8_percent_encode(from, percent_encoding::NON_ALPHANUMERIC)
1018 ));
1019 }
1020 if let Some(to) = &query.to {
1021 params.push(format!(
1022 "to={}",
1023 percent_encoding::utf8_percent_encode(to, percent_encoding::NON_ALPHANUMERIC)
1024 ));
1025 }
1026 if let Some(success) = query.success {
1027 params.push(format!("success={}", success));
1028 }
1029 if let Some(limit) = query.limit {
1030 params.push(format!("limit={}", limit));
1031 }
1032 if let Some(offset) = query.offset {
1033 params.push(format!("offset={}", offset));
1034 }
1035
1036 if !params.is_empty() {
1037 url.push('?');
1038 url.push_str(¶ms.join("&"));
1039 }
1040
1041 let request = self.build_request(Method::GET, &url)?;
1043 let response = self.execute_with_retry(request).await?;
1044
1045 self.parse_json_response(response).await
1047 }
1048
1049 pub async fn list_api_keys(&self) -> Result<ListApiKeysResult> {
1075 let url = self.endpoints.list_api_keys();
1076 let request = self.build_request(Method::GET, &url)?;
1077 let response = self.execute_with_retry(request).await?;
1078
1079 if !response.status().is_success() {
1080 return Err(self.parse_error_response(response).await);
1081 }
1082
1083 let request_id = header_str(response.headers(), "x-request-id");
1084 let mut result: ListApiKeysResult = self.parse_json_response(response).await?;
1085
1086 if result.request_id.is_none() {
1087 result.request_id = request_id;
1088 }
1089
1090 Ok(result)
1091 }
1092
1093 pub async fn create_api_key(
1140 &self,
1141 request: CreateApiKeyRequest,
1142 idempotency_key: Option<String>,
1143 ) -> Result<ApiKeyInfo> {
1144 let url = self.endpoints.create_api_key();
1145 let mut req = self.build_request(Method::POST, &url)?;
1146 req = req.json(&request);
1147
1148 if let Some(key) = idempotency_key {
1150 req = req.header("X-Idempotency-Key", key);
1151 }
1152
1153 let response = self.execute_with_retry(req).await?;
1154
1155 if !response.status().is_success() {
1156 return Err(self.parse_error_response(response).await);
1157 }
1158
1159 self.parse_json_response(response).await
1160 }
1161
1162 pub async fn get_api_key(&self, key_id: &str) -> Result<ApiKeyInfo> {
1191 let url = self.endpoints.get_api_key(key_id);
1192 let request = self.build_request(Method::GET, &url)?;
1193 let response = self.execute_with_retry(request).await?;
1194
1195 if !response.status().is_success() {
1196 return Err(self.parse_error_response(response).await);
1197 }
1198
1199 self.parse_json_response(response).await
1200 }
1201
1202 pub async fn revoke_api_key(&self, key_id: &str) -> Result<RevokeApiKeyResult> {
1231 let url = self.endpoints.revoke_api_key(key_id);
1232 let request = self.build_request(Method::DELETE, &url)?;
1233 let response = self.execute_with_retry(request).await?;
1234
1235 if !response.status().is_success() {
1236 return Err(self.parse_error_response(response).await);
1237 }
1238
1239 let request_id = header_str(response.headers(), "x-request-id");
1240 let mut result: RevokeApiKeyResult = self.parse_json_response(response).await?;
1241
1242 if result.request_id.is_none() {
1243 result.request_id = request_id;
1244 }
1245
1246 Ok(result)
1247 }
1248
1249 pub async fn search_secrets(&self, opts: SearchSecretsOpts) -> Result<SearchSecretsResult> {
1296 let mut url = self.endpoints.search_secrets();
1297
1298 let mut params = vec![format!(
1300 "pattern={}",
1301 percent_encoding::utf8_percent_encode(&opts.pattern, percent_encoding::NON_ALPHANUMERIC)
1302 )];
1303
1304 params.push(format!("mode={}", opts.mode.as_str()));
1305
1306 if !opts.namespaces.is_empty() {
1307 params.push(format!(
1308 "namespaces={}",
1309 percent_encoding::utf8_percent_encode(
1310 &opts.namespaces.join(","),
1311 percent_encoding::NON_ALPHANUMERIC
1312 )
1313 ));
1314 }
1315
1316 if opts.include_values {
1317 params.push("include_values=true".to_string());
1318 }
1319
1320 url.push('?');
1321 url.push_str(¶ms.join("&"));
1322
1323 let request = self.build_request(Method::GET, &url)?;
1325 let response = self.execute_with_retry(request).await?;
1326
1327 if !response.status().is_success() {
1328 return Err(self.parse_error_response(response).await);
1329 }
1330
1331 self.parse_json_response(response).await
1332 }
1333
1334 pub async fn bulk_delete_secrets(&self, opts: BulkDeleteOpts) -> Result<BulkDeleteResult> {
1378 let url = self.endpoints.bulk_delete();
1379
1380 let body = if opts.namespaces.is_empty() {
1382 serde_json::json!({
1383 "key": opts.key,
1384 })
1385 } else {
1386 serde_json::json!({
1387 "key": opts.key,
1388 "namespaces": opts.namespaces,
1389 })
1390 };
1391
1392 let mut request = self.build_request(Method::POST, &url)?;
1394 request = request.json(&body);
1395
1396 let response = self.execute_with_retry(request).await?;
1397
1398 if !response.status().is_success() {
1399 return Err(self.parse_error_response(response).await);
1400 }
1401
1402 self.parse_json_response(response).await
1403 }
1404
1405 pub async fn discovery(&self) -> Result<Discovery> {
1407 let url = self.endpoints.discovery();
1408 let request = self.build_request(Method::GET, &url)?;
1409 let response = self.execute_with_retry(request).await?;
1410
1411 if !response.status().is_success() {
1412 return Err(self.parse_error_response(response).await);
1413 }
1414
1415 self.parse_json_response(response).await
1416 }
1417
1418 pub async fn livez(&self) -> Result<()> {
1444 let url = self.endpoints.livez();
1445 let request = self.build_request(Method::GET, &url)?;
1446
1447 let response = self.execute_without_retry(request).await?;
1449
1450 if response.status().is_success() {
1451 Ok(())
1452 } else {
1453 Err(self.parse_error_response(response).await)
1454 }
1455 }
1456
1457 pub async fn readyz(&self) -> Result<HealthStatus> {
1494 let url = self.endpoints.readyz();
1495 let request = self.build_request(Method::GET, &url)?;
1496
1497 let response = self.execute_without_retry(request).await?;
1499
1500 if response.status().is_success() {
1501 self.parse_json_response(response).await
1502 } else {
1503 Err(self.parse_error_response(response).await)
1504 }
1505 }
1506
1507 pub async fn metrics(&self, metrics_token: Option<&str>) -> Result<String> {
1542 let url = self.endpoints.metrics();
1543 let mut request = self.build_request(Method::GET, &url)?;
1544
1545 if let Some(token) = metrics_token {
1547 request = request.header("X-Metrics-Token", token);
1548 }
1549
1550 let response = self.execute_without_retry(request).await?;
1552
1553 if response.status().is_success() {
1554 response.text().await.map_err(Error::from)
1555 } else {
1556 Err(self.parse_error_response(response).await)
1557 }
1558 }
1559
1560 fn build_request(&self, method: Method, url: &str) -> Result<reqwest::RequestBuilder> {
1564 let mut builder = self.http.request(method, url);
1565
1566 let request_id = generate_request_id();
1568 builder = builder.header("X-Request-ID", &request_id);
1569
1570 builder = builder
1572 .header("X-Trace-ID", &request_id)
1573 .header("X-Span-ID", uuid::Uuid::new_v4().to_string());
1574
1575 Ok(builder)
1576 }
1577
1578 async fn execute_with_retry(
1580 &self,
1581 request_builder: reqwest::RequestBuilder,
1582 ) -> Result<Response> {
1583 let mut token_refresh_count = 0;
1584 let max_retries = self.config.retries;
1585 let auth = &self.config.auth;
1586
1587 #[cfg(feature = "metrics")]
1589 let (method, path) = {
1590 if let Some(cloned_builder) = request_builder.try_clone() {
1592 if let Ok(req) = cloned_builder.build() {
1593 let method = req.method().to_string();
1594 let path = req.url().path().to_string();
1595 (method, path)
1596 } else {
1597 ("UNKNOWN".to_string(), "UNKNOWN".to_string())
1598 }
1599 } else {
1600 ("UNKNOWN".to_string(), "UNKNOWN".to_string())
1601 }
1602 };
1603
1604 loop {
1605 let (auth_header, auth_value) = auth
1607 .get_header()
1608 .await
1609 .map_err(|e| Error::Config(format!("Failed to get auth header: {}", e)))?;
1610
1611 let req_with_auth = request_builder
1613 .try_clone()
1614 .ok_or_else(|| Error::Other("Request cannot be cloned".to_string()))?
1615 .header(auth_header, auth_value);
1616
1617 let mut backoff = ExponentialBackoff {
1619 initial_interval: Duration::from_millis(100),
1620 randomization_factor: 0.3,
1621 multiplier: 2.0,
1622 max_interval: Duration::from_secs(10),
1623 max_elapsed_time: None,
1624 ..Default::default()
1625 };
1626 backoff.max_elapsed_time = if max_retries > 0 {
1629 let timeout_secs = self.config.timeout.as_secs();
1630 Some(Duration::from_secs((max_retries as u64 + 1) * timeout_secs + 30))
1632 } else {
1633 Some(Duration::from_millis(0))
1634 };
1635
1636 let retry_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1637 let retry_count_clone = retry_count.clone();
1638
1639 let result = retry_notify(
1641 backoff,
1642 || async {
1643 let current_retry = retry_count.load(std::sync::atomic::Ordering::Relaxed);
1644 let req = req_with_auth
1646 .try_clone()
1647 .ok_or_else(|| {
1648 backoff::Error::Permanent(Error::Other(
1649 "Request cannot be cloned".to_string(),
1650 ))
1651 })?
1652 .build()
1653 .map_err(|e| {
1654 backoff::Error::Permanent(Error::Other(format!(
1655 "Failed to build request: {}",
1656 e
1657 )))
1658 })?;
1659
1660 #[cfg(feature = "metrics")]
1662 self.metrics.inc_active_connections();
1663
1664 #[cfg(feature = "metrics")]
1666 let start_time = std::time::Instant::now();
1667
1668 let response_result = self.http.execute(req).await;
1669
1670 #[cfg(feature = "metrics")]
1672 self.metrics.dec_active_connections();
1673
1674 match response_result {
1675 Ok(response) => {
1676 let status = response.status();
1677
1678 if status == StatusCode::UNAUTHORIZED
1680 && token_refresh_count == 0
1681 && auth.supports_refresh()
1682 {
1683 return Err(backoff::Error::Permanent(Error::Http {
1685 status: 401,
1686 category: "auth_refresh_needed".to_string(),
1687 message: "Token refresh required".to_string(),
1688 request_id: header_str(response.headers(), "x-request-id"),
1689 }));
1690 }
1691
1692 if status.is_server_error()
1694 || status == StatusCode::TOO_MANY_REQUESTS
1695 || status == StatusCode::REQUEST_TIMEOUT
1696 {
1697 let error = self.parse_error_response(response).await;
1698 if error.is_retryable() && current_retry < max_retries as usize {
1699 debug!("Retrying request due to: {:?}", error);
1700 #[cfg(feature = "metrics")]
1701 self.metrics.record_retry(
1702 (current_retry + 1) as u32,
1703 &status.to_string(),
1704 );
1705 return Err(backoff::Error::transient(error));
1706 } else {
1707 return Err(backoff::Error::Permanent(error));
1708 }
1709 }
1710
1711 if !status.is_success() && status != StatusCode::NOT_MODIFIED {
1713 let error = self.parse_error_response(response).await;
1714 return Err(backoff::Error::Permanent(error));
1715 }
1716
1717 #[cfg(feature = "metrics")]
1719 {
1720 let duration_secs = start_time.elapsed().as_secs_f64();
1721 self.metrics.record_request(
1722 &method,
1723 &path,
1724 status.as_u16(),
1725 duration_secs,
1726 );
1727 }
1728
1729 Ok(response)
1730 }
1731 Err(e) => {
1732 let error = Error::from(e);
1733 if error.is_retryable() && current_retry < max_retries as usize {
1734 debug!("Retrying request due to network error: {:?}", error);
1735 #[cfg(feature = "metrics")]
1736 self.metrics
1737 .record_retry((current_retry + 1) as u32, "network_error");
1738 Err(backoff::Error::transient(error))
1739 } else {
1740 Err(backoff::Error::Permanent(error))
1741 }
1742 }
1743 }
1744 },
1745 |err, dur| {
1746 let count =
1747 retry_count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
1748 debug!("Retry {} after {:?} due to: {:?}", count, dur, err);
1749 },
1750 )
1751 .await;
1752
1753 match result {
1754 Ok(response) => return Ok(response),
1755 Err(Error::Http {
1756 status: 401,
1757 category,
1758 ..
1759 }) if category == "auth_refresh_needed" && token_refresh_count == 0 => {
1760 warn!("Got 401, attempting token refresh");
1762 auth.refresh()
1763 .await
1764 .map_err(|e| Error::Network(format!("Token refresh failed: {}", e)))?;
1765 token_refresh_count += 1;
1766 continue;
1768 }
1769 Err(e) => return Err(e),
1770 }
1771 }
1772 }
1773
1774 async fn execute_without_retry(
1776 &self,
1777 request_builder: reqwest::RequestBuilder,
1778 ) -> Result<Response> {
1779 let (auth_header, auth_value) = self
1781 .config
1782 .auth
1783 .get_header()
1784 .await
1785 .map_err(|e| Error::Config(format!("Failed to get auth header: {}", e)))?;
1786
1787 let request = request_builder
1789 .header(auth_header, auth_value)
1790 .build()
1791 .map_err(|e| Error::Other(format!("Failed to build request: {}", e)))?;
1792
1793 self.http.execute(request).await.map_err(Error::from)
1795 }
1796
1797 async fn parse_error_response(&self, response: Response) -> Error {
1799 let status = response.status().as_u16();
1800 let request_id = header_str(response.headers(), "x-request-id");
1801
1802 match response.json::<ErrorResponse>().await {
1804 Ok(error_resp) => Error::from_response(
1805 error_resp.status,
1806 &error_resp.error,
1807 &error_resp.message,
1808 request_id,
1809 ),
1810 Err(_) => Error::Http {
1811 status,
1812 category: "unknown".to_string(),
1813 message: format!("HTTP error {}", status),
1814 request_id,
1815 },
1816 }
1817 }
1818
1819 async fn parse_json_response<T: serde::de::DeserializeOwned>(
1821 &self,
1822 response: Response,
1823 ) -> Result<T> {
1824 response.json().await.map_err(Error::from)
1825 }
1826
1827 async fn parse_get_response(
1829 &self,
1830 response: Response,
1831 namespace: &str,
1832 key: &str,
1833 ) -> Result<Secret> {
1834 let headers = response.headers().clone();
1835
1836 let etag = header_str(&headers, "etag");
1838 let last_modified = header_str(&headers, "last-modified");
1839 let request_id = header_str(&headers, "x-request-id");
1840
1841 #[derive(serde::Deserialize)]
1843 struct GetResponse {
1844 value: String,
1845 version: i32,
1846 expires_at: Option<String>,
1847 metadata: Option<serde_json::Value>,
1848 updated_at: String,
1849 }
1850
1851 let body: GetResponse = response.json().await.map_err(Error::from)?;
1852
1853 let updated_at = time::OffsetDateTime::parse(
1855 &body.updated_at,
1856 &time::format_description::well_known::Rfc3339,
1857 )
1858 .map_err(|e| Error::Deserialize(format!("Invalid updated_at timestamp: {}", e)))?;
1859
1860 let expires_at = body
1861 .expires_at
1862 .as_ref()
1863 .map(|s| {
1864 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
1865 .map_err(|e| Error::Deserialize(format!("Invalid expires_at timestamp: {}", e)))
1866 })
1867 .transpose()?;
1868
1869 Ok(Secret {
1870 namespace: namespace.to_string(),
1871 key: key.to_string(),
1872 value: SecretString::new(body.value),
1873 version: body.version,
1874 expires_at,
1875 metadata: body.metadata.unwrap_or(serde_json::Value::Null),
1876 updated_at,
1877 etag,
1878 last_modified,
1879 request_id,
1880 })
1881 }
1882
1883 async fn get_from_cache(&self, cache_key: &str) -> Option<Secret> {
1885 let cache = self.cache.as_ref()?;
1886
1887 match cache.get(cache_key).await {
1888 Some(cached) => {
1889 if cached.is_expired() {
1891 trace!("Cache entry expired for key: {}", cache_key);
1892 cache.invalidate(cache_key).await;
1893 self.stats.record_expiration();
1894 self.stats.record_miss();
1895 None
1896 } else {
1897 debug!("Cache hit for key: {}", cache_key);
1898 self.stats.record_hit();
1899
1900 #[cfg(feature = "metrics")]
1902 {
1903 let (namespace, _) = cache_key.split_once('/').unwrap_or(("", cache_key));
1904 self.metrics.record_cache_hit(namespace);
1905 }
1906
1907 let (namespace, key) = cache_key.split_once('/').unwrap_or(("", cache_key));
1908 Some(cached.into_secret(namespace.to_string(), key.to_string()))
1909 }
1910 }
1911 None => {
1912 trace!("Cache miss for key: {}", cache_key);
1913 self.stats.record_miss();
1914
1915 #[cfg(feature = "metrics")]
1917 {
1918 let (namespace, _) = cache_key.split_once('/').unwrap_or(("", cache_key));
1919 self.metrics.record_cache_miss(namespace);
1920 }
1921
1922 None
1923 }
1924 }
1925 }
1926
1927 async fn cache_secret(&self, cache_key: &str, secret: &Secret) {
1929 let Some(cache) = &self.cache else { return };
1930
1931 let ttl = if let Some(_etag) = &secret.etag {
1933 Duration::from_secs(self.config.cache_config.default_ttl_secs * 2)
1935 } else {
1936 Duration::from_secs(self.config.cache_config.default_ttl_secs)
1937 };
1938
1939 let cache_expires_at = time::OffsetDateTime::now_utc() + ttl;
1940
1941 let cached = CachedSecret {
1942 value: secret.value.clone(),
1943 version: secret.version,
1944 expires_at: secret.expires_at,
1945 metadata: secret.metadata.clone(),
1946 updated_at: secret.updated_at,
1947 etag: secret.etag.clone(),
1948 last_modified: secret.last_modified.clone(),
1949 cache_expires_at,
1950 };
1951
1952 cache.insert(cache_key.to_string(), cached).await;
1953 self.stats.record_insertion();
1954 debug!("Cached secret for key: {} with TTL: {:?}", cache_key, ttl);
1955 }
1956}
1957
1958#[cfg(test)]
1959mod tests {
1960 use super::*;
1961 use crate::{auth::Auth, ClientBuilder};
1962 use secrecy::ExposeSecret;
1963 use wiremock::matchers::{header, method, path};
1964 use wiremock::{Mock, MockServer, ResponseTemplate};
1965
1966 fn create_test_client(base_url: &str) -> Client {
1968 #[cfg(feature = "danger-insecure-http")]
1969 {
1970 ClientBuilder::new(base_url)
1971 .auth(Auth::bearer("test-token"))
1972 .allow_insecure_http()
1973 .build()
1974 .unwrap()
1975 }
1976 #[cfg(not(feature = "danger-insecure-http"))]
1977 {
1978 ClientBuilder::new(&base_url.replace("http://", "https://"))
1981 .auth(Auth::bearer("test-token"))
1982 .build()
1983 .unwrap()
1984 }
1985 }
1986
1987 #[test]
1988 fn test_client_creation() {
1989 let client = ClientBuilder::new("https://example.com")
1990 .auth(Auth::bearer("test-token"))
1991 .build();
1992 assert!(client.is_ok());
1993 }
1994
1995 #[test]
1996 fn test_cache_key_format() {
1997 let cache_key = format!("{}/{}", "namespace", "key");
1998 assert_eq!(cache_key, "namespace/key");
1999 }
2000
2001 #[tokio::test]
2002 async fn test_get_secret_success() {
2003 let mock_server = MockServer::start().await;
2004
2005 let response_body = serde_json::json!({
2007 "value": "secret-value",
2008 "version": 1,
2009 "expires_at": null,
2010 "metadata": {"env": "prod"},
2011 "updated_at": "2024-01-01T00:00:00Z"
2012 });
2013
2014 Mock::given(method("GET"))
2015 .and(path("/api/v2/secrets/test-namespace/test-key"))
2016 .and(header("Authorization", "Bearer test-token"))
2017 .respond_with(
2018 ResponseTemplate::new(200)
2019 .set_body_json(&response_body)
2020 .insert_header("etag", "\"abc123\"")
2021 .insert_header("x-request-id", "req-123"),
2022 )
2023 .mount(&mock_server)
2024 .await;
2025
2026 let client = create_test_client(&mock_server.uri());
2027
2028 let result = client
2029 .get_secret("test-namespace", "test-key", GetOpts::default())
2030 .await;
2031 if let Err(ref e) = result {
2032 eprintln!("Error: {:?}", e);
2033 }
2034 assert!(result.is_ok());
2035
2036 let secret = result.unwrap();
2037 assert_eq!(secret.namespace, "test-namespace");
2038 assert_eq!(secret.key, "test-key");
2039 assert_eq!(secret.version, 1);
2040 assert_eq!(secret.etag, Some("\"abc123\"".to_string()));
2041 }
2042
2043 #[tokio::test]
2044 async fn test_get_secret_404() {
2045 let mock_server = MockServer::start().await;
2046
2047 let error_body = serde_json::json!({
2048 "error": "not_found",
2049 "message": "Secret not found",
2050 "timestamp": "2024-01-01T00:00:00Z",
2051 "status": 404
2052 });
2053
2054 Mock::given(method("GET"))
2055 .and(path("/api/v2/secrets/test-namespace/missing-key"))
2056 .respond_with(
2057 ResponseTemplate::new(404)
2058 .set_body_json(&error_body)
2059 .insert_header("x-request-id", "req-456"),
2060 )
2061 .mount(&mock_server)
2062 .await;
2063
2064 let client = create_test_client(&mock_server.uri());
2065
2066 let result = client
2067 .get_secret("test-namespace", "missing-key", GetOpts::default())
2068 .await;
2069 assert!(result.is_err());
2070
2071 let err = result.unwrap_err();
2072 assert_eq!(err.status_code(), Some(404));
2073 assert_eq!(err.request_id(), Some("req-456"));
2074 }
2075
2076 #[tokio::test]
2077 async fn test_get_secret_with_cache() {
2078 let mock_server = MockServer::start().await;
2079
2080 let response_body = serde_json::json!({
2081 "value": "cached-value",
2082 "version": 2,
2083 "expires_at": null,
2084 "metadata": null,
2085 "updated_at": "2024-01-01T00:00:00Z"
2086 });
2087
2088 Mock::given(method("GET"))
2090 .and(path("/api/v2/secrets/cache-ns/cache-key"))
2091 .respond_with(
2092 ResponseTemplate::new(200)
2093 .set_body_json(&response_body)
2094 .insert_header("etag", "\"etag123\""),
2095 )
2096 .expect(1) .mount(&mock_server)
2098 .await;
2099
2100 #[cfg(feature = "danger-insecure-http")]
2101 let client = ClientBuilder::new(mock_server.uri())
2102 .auth(Auth::bearer("test-token"))
2103 .enable_cache(true)
2104 .allow_insecure_http()
2105 .build()
2106 .unwrap();
2107
2108 #[cfg(not(feature = "danger-insecure-http"))]
2109 let client = ClientBuilder::new(&mock_server.uri().replace("http://", "https://"))
2110 .auth(Auth::bearer("test-token"))
2111 .enable_cache(true)
2112 .build()
2113 .unwrap();
2114
2115 let secret1 = client
2117 .get_secret("cache-ns", "cache-key", GetOpts::default())
2118 .await
2119 .unwrap();
2120 assert_eq!(secret1.version, 2);
2121
2122 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2124
2125 let secret2 = client
2127 .get_secret("cache-ns", "cache-key", GetOpts::default())
2128 .await
2129 .unwrap();
2130 assert_eq!(secret2.version, 2);
2131
2132 let stats = client.cache_stats();
2134 assert_eq!(stats.hits(), 1);
2135 assert_eq!(stats.misses(), 1);
2136 }
2137
2138 #[tokio::test]
2139 async fn test_get_secret_304_not_modified() {
2140 let mock_server = MockServer::start().await;
2141
2142 let response_body = serde_json::json!({
2143 "value": "initial-value",
2144 "version": 1,
2145 "expires_at": null,
2146 "metadata": null,
2147 "updated_at": "2024-01-01T00:00:00Z"
2148 });
2149
2150 Mock::given(method("GET"))
2153 .and(path("/api/v2/secrets/test-ns/test-key"))
2154 .and(header("Authorization", "Bearer test-token"))
2155 .and(header("if-none-match", "etag-v1"))
2156 .respond_with(ResponseTemplate::new(304))
2157 .expect(1)
2158 .mount(&mock_server)
2159 .await;
2160
2161 Mock::given(method("GET"))
2163 .and(path("/api/v2/secrets/test-ns/test-key"))
2164 .and(header("Authorization", "Bearer test-token"))
2165 .respond_with(
2166 ResponseTemplate::new(200)
2167 .set_body_json(&response_body)
2168 .insert_header("etag", "\"etag-v1\""),
2169 )
2170 .expect(1)
2171 .mount(&mock_server)
2172 .await;
2173
2174 #[cfg(feature = "danger-insecure-http")]
2175 let client = ClientBuilder::new(mock_server.uri())
2176 .auth(Auth::bearer("test-token"))
2177 .enable_cache(true)
2178 .allow_insecure_http()
2179 .build()
2180 .unwrap();
2181
2182 #[cfg(not(feature = "danger-insecure-http"))]
2183 let client = ClientBuilder::new(&mock_server.uri().replace("http://", "https://"))
2184 .auth(Auth::bearer("test-token"))
2185 .enable_cache(true)
2186 .build()
2187 .unwrap();
2188
2189 let secret1 = client
2191 .get_secret("test-ns", "test-key", GetOpts::default())
2192 .await
2193 .unwrap();
2194 assert_eq!(secret1.etag, Some("\"etag-v1\"".to_string()));
2195
2196 client.clear_cache();
2198
2199 let opts = GetOpts {
2201 use_cache: false, if_none_match: Some("etag-v1".to_string()), if_modified_since: None,
2204 };
2205 let result = client.get_secret("test-ns", "test-key", opts).await;
2207 assert!(result.is_err());
2208
2209 if let Err(e) = result {
2211 match &e {
2212 Error::Other(msg) => {
2213 assert!(msg.contains("304"));
2214 assert!(msg.contains("no cached entry"));
2215 }
2216 _ => panic!("Expected Error::Other, got {:?}", e),
2217 }
2218 }
2219 }
2220
2221 #[tokio::test]
2222 async fn test_put_secret_success() {
2223 let mock_server = MockServer::start().await;
2224
2225 let response_body = serde_json::json!({
2226 "message": "Secret created",
2227 "namespace": "test-ns",
2228 "key": "new-key",
2229 "created_at": "2024-01-01T00:00:00Z",
2230 "request_id": "req-789"
2231 });
2232
2233 Mock::given(method("PUT"))
2234 .and(path("/api/v2/secrets/test-ns/new-key"))
2235 .respond_with(ResponseTemplate::new(201).set_body_json(&response_body))
2236 .mount(&mock_server)
2237 .await;
2238
2239 let client = create_test_client(&mock_server.uri());
2240
2241 let opts = PutOpts {
2242 ttl_seconds: Some(3600),
2243 metadata: Some(serde_json::json!({"env": "test"})),
2244 idempotency_key: None,
2245 };
2246
2247 let result = client
2248 .put_secret("test-ns", "new-key", "new-value", opts)
2249 .await;
2250 assert!(result.is_ok());
2251
2252 let put_result = result.unwrap();
2253 assert_eq!(put_result.namespace, "test-ns");
2254 assert_eq!(put_result.key, "new-key");
2255 }
2256
2257 #[tokio::test]
2258 async fn test_delete_secret_success() {
2259 let mock_server = MockServer::start().await;
2260
2261 Mock::given(method("DELETE"))
2262 .and(path("/api/v2/secrets/test-ns/delete-key"))
2263 .respond_with(ResponseTemplate::new(204).insert_header("x-request-id", "req-delete"))
2264 .mount(&mock_server)
2265 .await;
2266
2267 let client = create_test_client(&mock_server.uri());
2268
2269 let result = client.delete_secret("test-ns", "delete-key").await;
2270 assert!(result.is_ok());
2271
2272 let delete_result = result.unwrap();
2273 assert!(delete_result.deleted);
2274 assert_eq!(delete_result.request_id, Some("req-delete".to_string()));
2275 }
2276
2277 #[tokio::test]
2278 async fn test_retry_on_server_error() {
2279 let mock_server = MockServer::start().await;
2280
2281 let error_body = serde_json::json!({
2282 "error": "internal",
2283 "message": "Internal server error",
2284 "timestamp": "2024-01-01T00:00:00Z",
2285 "status": 500
2286 });
2287
2288 Mock::given(method("GET"))
2290 .and(path("/api/v2/secrets/test-ns/retry-key"))
2291 .respond_with(ResponseTemplate::new(500).set_body_json(&error_body))
2292 .up_to_n_times(2)
2293 .mount(&mock_server)
2294 .await;
2295
2296 let success_body = serde_json::json!({
2297 "value": "success-after-retry",
2298 "version": 1,
2299 "expires_at": null,
2300 "metadata": null,
2301 "updated_at": "2024-01-01T00:00:00Z"
2302 });
2303
2304 Mock::given(method("GET"))
2305 .and(path("/api/v2/secrets/test-ns/retry-key"))
2306 .respond_with(ResponseTemplate::new(200).set_body_json(&success_body))
2307 .mount(&mock_server)
2308 .await;
2309
2310 #[cfg(feature = "danger-insecure-http")]
2311 let client = ClientBuilder::new(mock_server.uri())
2312 .auth(Auth::bearer("test-token"))
2313 .retries(3)
2314 .allow_insecure_http()
2315 .build()
2316 .unwrap();
2317
2318 #[cfg(not(feature = "danger-insecure-http"))]
2319 let client = ClientBuilder::new(&mock_server.uri().replace("http://", "https://"))
2320 .auth(Auth::bearer("test-token"))
2321 .retries(3)
2322 .build()
2323 .unwrap();
2324
2325 let result = client
2326 .get_secret("test-ns", "retry-key", GetOpts::default())
2327 .await;
2328 assert!(result.is_ok()); }
2330
2331 #[tokio::test]
2332 async fn test_list_secrets() {
2333 let mock_server = MockServer::start().await;
2334
2335 let response_body = serde_json::json!({
2336 "namespace": "test-ns",
2337 "secrets": [
2338 {"key": "key1", "version": 1, "updated_at": "2024-01-01T00:00:00Z", "kid": null},
2339 {"key": "key2", "version": 2, "updated_at": "2024-01-01T00:00:00Z", "kid": "kid123"}
2340 ],
2341 "total": 2,
2342 "limit": 10,
2343 "has_more": false,
2344 "request_id": "req-list"
2345 });
2346
2347 Mock::given(method("GET"))
2348 .and(path("/api/v2/secrets/test-ns"))
2349 .and(wiremock::matchers::query_param("prefix", "key"))
2350 .and(wiremock::matchers::query_param("limit", "10"))
2351 .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
2352 .mount(&mock_server)
2353 .await;
2354
2355 let client = create_test_client(&mock_server.uri());
2356
2357 let opts = ListOpts {
2358 prefix: Some("key".to_string()),
2359 limit: Some(10),
2360 };
2361
2362 let result = client.list_secrets("test-ns", opts).await;
2363 assert!(result.is_ok());
2364
2365 let list_result = result.unwrap();
2366 assert_eq!(list_result.namespace, "test-ns");
2367 assert_eq!(list_result.secrets.len(), 2);
2368 assert_eq!(list_result.total, 2);
2369 }
2370
2371 #[tokio::test]
2372 async fn test_list_versions() {
2373 let mock_server = MockServer::start().await;
2374
2375 let response_body = serde_json::json!({
2376 "namespace": "test-ns",
2377 "key": "versioned-key",
2378 "versions": [
2379 {
2380 "version": 3,
2381 "created_at": "2024-01-03T00:00:00Z",
2382 "created_by": "user1",
2383 "is_current": true
2384 },
2385 {
2386 "version": 2,
2387 "created_at": "2024-01-02T00:00:00Z",
2388 "created_by": "user1",
2389 "is_current": false
2390 },
2391 {
2392 "version": 1,
2393 "created_at": "2024-01-01T00:00:00Z",
2394 "created_by": "user1",
2395 "is_current": false
2396 }
2397 ],
2398 "total": 3,
2399 "request_id": "req-versions"
2400 });
2401
2402 Mock::given(method("GET"))
2403 .and(path("/api/v2/secrets/test-ns/versioned-key/versions"))
2404 .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
2405 .mount(&mock_server)
2406 .await;
2407
2408 let client = create_test_client(&mock_server.uri());
2409
2410 let result = client.list_versions("test-ns", "versioned-key").await;
2411 assert!(result.is_ok());
2412
2413 let version_list = result.unwrap();
2414 assert_eq!(version_list.namespace, "test-ns");
2415 assert_eq!(version_list.key, "versioned-key");
2416 assert_eq!(version_list.versions.len(), 3);
2417 assert_eq!(version_list.total, 3);
2418 assert!(version_list.versions[0].is_current);
2419 }
2420
2421 #[tokio::test]
2422 async fn test_get_version() {
2423 let mock_server = MockServer::start().await;
2424
2425 let response_body = serde_json::json!({
2426 "value": "version-2-value",
2427 "version": 2,
2428 "expires_at": null,
2429 "metadata": {"note": "version 2"},
2430 "updated_at": "2024-01-02T00:00:00Z"
2431 });
2432
2433 Mock::given(method("GET"))
2434 .and(path("/api/v2/secrets/test-ns/versioned-key/versions/2"))
2435 .respond_with(
2436 ResponseTemplate::new(200)
2437 .set_body_json(&response_body)
2438 .insert_header("etag", "\"etag-v2\""),
2439 )
2440 .mount(&mock_server)
2441 .await;
2442
2443 let client = create_test_client(&mock_server.uri());
2444
2445 let result = client.get_version("test-ns", "versioned-key", 2).await;
2446 assert!(result.is_ok());
2447
2448 let secret = result.unwrap();
2449 assert_eq!(secret.namespace, "test-ns");
2450 assert_eq!(secret.key, "versioned-key");
2451 assert_eq!(secret.version, 2);
2452 assert_eq!(secret.value.expose_secret(), "version-2-value");
2453 }
2454
2455 #[tokio::test]
2456 async fn test_rollback() {
2457 let mock_server = MockServer::start().await;
2458
2459 let response_body = serde_json::json!({
2460 "message": "Secret successfully rolled back to version 2",
2461 "namespace": "test-ns",
2462 "key": "versioned-key",
2463 "from_version": 4,
2464 "to_version": 2,
2465 "request_id": "req-rollback"
2466 });
2467
2468 Mock::given(method("POST"))
2469 .and(path("/api/v2/secrets/test-ns/versioned-key/rollback/2"))
2470 .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
2471 .mount(&mock_server)
2472 .await;
2473
2474 let client = create_test_client(&mock_server.uri());
2475
2476 let result = client.rollback("test-ns", "versioned-key", 2).await;
2477 assert!(result.is_ok());
2478
2479 let rollback_result = result.unwrap();
2480 assert_eq!(rollback_result.namespace, "test-ns");
2481 assert_eq!(rollback_result.key, "versioned-key");
2482 assert_eq!(rollback_result.from_version, 4);
2483 assert_eq!(rollback_result.to_version, 2);
2484 }
2485
2486 #[tokio::test]
2487 async fn test_audit_logs() {
2488 let mock_server = MockServer::start().await;
2489
2490 let response_body = serde_json::json!({
2491 "logs": [
2492 {
2493 "id": 123,
2494 "timestamp": "2024-01-01T12:00:00Z",
2495 "actor": "user1",
2496 "action": "put",
2497 "namespace": "production",
2498 "key_name": "api-key",
2499 "success": true,
2500 "ip_address": "192.168.1.1",
2501 "user_agent": "SDK/1.0"
2502 },
2503 {
2504 "id": 124,
2505 "timestamp": "2024-01-01T12:05:00Z",
2506 "actor": "user2",
2507 "action": "get",
2508 "namespace": "production",
2509 "key_name": "db-pass",
2510 "success": false,
2511 "error": "not found"
2512 }
2513 ],
2514 "total": 2,
2515 "limit": 10,
2516 "offset": 0,
2517 "has_more": false,
2518 "request_id": "req-audit"
2519 });
2520
2521 Mock::given(method("GET"))
2522 .and(path("/api/v2/audit"))
2523 .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
2524 .mount(&mock_server)
2525 .await;
2526
2527 let client = create_test_client(&mock_server.uri());
2528
2529 let query = AuditQuery::default();
2530 let result = client.audit(query).await;
2531 assert!(result.is_ok());
2532
2533 let audit_result = result.unwrap();
2534 assert_eq!(audit_result.entries.len(), 2);
2535 assert_eq!(audit_result.total, 2);
2536 assert!(!audit_result.has_more);
2537
2538 let first = &audit_result.entries[0];
2540 assert_eq!(first.id, 123);
2541 assert_eq!(first.action, "put");
2542 assert!(first.success);
2543 assert_eq!(first.namespace, Some("production".to_string()));
2544 }
2545
2546 #[tokio::test]
2547 async fn test_audit_logs_with_filters() {
2548 let mock_server = MockServer::start().await;
2549
2550 let response_body = serde_json::json!({
2551 "logs": [
2552 {
2553 "id": 200,
2554 "timestamp": "2024-01-02T10:00:00Z",
2555 "actor": "admin",
2556 "action": "delete",
2557 "namespace": "test",
2558 "key_name": "temp-key",
2559 "success": false,
2560 "error": "permission denied"
2561 }
2562 ],
2563 "total": 1,
2564 "limit": 5,
2565 "offset": 0,
2566 "has_more": false,
2567 "request_id": "req-audit-filtered"
2568 });
2569
2570 Mock::given(method("GET"))
2571 .and(path("/api/v2/audit"))
2572 .and(wiremock::matchers::query_param("namespace", "test"))
2573 .and(wiremock::matchers::query_param("success", "false"))
2574 .and(wiremock::matchers::query_param("limit", "5"))
2575 .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
2576 .mount(&mock_server)
2577 .await;
2578
2579 let client = create_test_client(&mock_server.uri());
2580
2581 let query = AuditQuery {
2582 namespace: Some("test".to_string()),
2583 success: Some(false),
2584 limit: Some(5),
2585 ..Default::default()
2586 };
2587
2588 let result = client.audit(query).await;
2589 assert!(result.is_ok());
2590
2591 let audit_result = result.unwrap();
2592 assert_eq!(audit_result.entries.len(), 1);
2593 assert_eq!(audit_result.entries[0].action, "delete");
2594 assert!(!audit_result.entries[0].success);
2595 assert_eq!(
2596 audit_result.entries[0].error,
2597 Some("permission denied".to_string())
2598 );
2599 }
2600}