secret_store_sdk/
client.rs

1//! XJP Secret Store Client Implementation
2//!
3//! This module contains the main `Client` struct that provides the core functionality
4//! for interacting with the XJP Secret Store service.
5//!
6//! # Architecture
7//!
8//! The client is designed with the following key components:
9//! - **HTTP Layer**: Built on `reqwest` for async HTTP operations
10//! - **Caching Layer**: Uses `moka` for high-performance async caching with TTL support
11//! - **Retry Logic**: Implements exponential backoff with jitter for transient failures
12//! - **Authentication**: Supports multiple auth methods with automatic token refresh
13//! - **Telemetry**: Optional OpenTelemetry integration for observability
14//!
15//! # Examples
16//!
17//! ## Basic Usage
18//!
19//! ```no_run
20//! use secret_store_sdk::{Client, ClientBuilder, Auth};
21//!
22//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23//! let client = ClientBuilder::new("https://secret.example.com")
24//!     .auth(Auth::bearer("your-token"))
25//!     .build()?;
26//!
27//! // Get a secret
28//! let secret = client.get_secret("prod", "api-key", Default::default()).await?;
29//! println!("Secret version: {}", secret.version);
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## With Caching and Retries
35//!
36//! ```no_run
37//! use secret_store_sdk::{ClientBuilder, Auth};
38//!
39//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
40//! let client = ClientBuilder::new("https://secret.example.com")
41//!     .auth(Auth::bearer("your-token"))
42//!     .enable_cache(true)
43//!     .cache_ttl_secs(600) // 10 minute cache
44//!     .retries(5) // Up to 5 retries
45//!     .timeout_ms(30000) // 30 second timeout
46//!     .build()?;
47//! # Ok(())
48//! # }
49//! ```
50
51use 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/// XJP Secret Store client
72///
73/// The main client for interacting with the XJP Secret Store API.
74/// Provides methods for managing secrets, including get, put, delete,
75/// and batch operations. Supports caching, retries, and conditional requests.
76#[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    /// Create a new client with the given configuration
100    pub(crate) fn new(config: ClientConfig) -> Result<Self> {
101        // Build user agent
102        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        // Create HTTP client
109        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        // Configure TLS
117        #[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        // Create cache if enabled
134        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        // Initialize telemetry if enabled
146        #[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    /// Get cache statistics
165    ///
166    /// Returns statistics about the cache including hit rate, number of hits/misses,
167    /// and evictions. Useful for monitoring cache performance.
168    ///
169    /// # Example
170    ///
171    /// ```no_run
172    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
173    /// # async fn example(client: &Client) {
174    /// let stats = client.cache_stats();
175    /// println!("Cache hit rate: {:.2}%", stats.hit_rate());
176    /// println!("Total hits: {}, misses: {}", stats.hits(), stats.misses());
177    /// # }
178    /// ```
179    pub fn cache_stats(&self) -> &CacheStats {
180        &self.stats
181    }
182
183    /// Clear the cache
184    ///
185    /// Removes all entries from the cache and resets cache statistics.
186    /// This is useful when you need to force fresh data retrieval.
187    ///
188    /// # Example
189    ///
190    /// ```no_run
191    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
192    /// # async fn example(client: &Client) {
193    /// // Clear all cached secrets
194    /// client.clear_cache();
195    /// # }
196    /// ```
197    pub fn clear_cache(&self) {
198        if let Some(cache) = &self.cache {
199            cache.invalidate_all();
200            self.stats.reset();
201        }
202    }
203
204    /// Invalidate a specific cache entry
205    ///
206    /// Removes a single secret from the cache, forcing the next retrieval
207    /// to fetch fresh data from the server.
208    ///
209    /// # Arguments
210    ///
211    /// * `namespace` - The namespace of the secret
212    /// * `key` - The key of the secret
213    ///
214    /// # Example
215    ///
216    /// ```no_run
217    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
218    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
219    /// // Invalidate a specific secret from cache
220    /// client.invalidate_cache("production", "api-key").await;
221    /// # Ok(())
222    /// # }
223    /// ```
224    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    /// Get a secret from the store
232    ///
233    /// Retrieves a secret value from the specified namespace and key.
234    /// Supports caching and conditional requests via ETags.
235    ///
236    /// # Arguments
237    ///
238    /// * `namespace` - The namespace containing the secret
239    /// * `key` - The key identifying the secret
240    /// * `opts` - Options controlling cache usage and conditional requests
241    ///
242    /// # Returns
243    ///
244    /// The secret value with metadata on success, or an error if the secret
245    /// doesn't exist or access is denied.
246    ///
247    /// # Errors
248    ///
249    /// * `Error::Http` with status 404 if the secret doesn't exist
250    /// * `Error::Http` with status 403 if access is denied
251    /// * `Error::Http` with status 401 if authentication fails
252    /// * `Error::Network` for connection issues
253    /// * `Error::Timeout` if the request times out
254    ///
255    /// # Example
256    ///
257    /// ```no_run
258    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, GetOpts};
259    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
260    /// // Simple get with default options (cache enabled)
261    /// let secret = client.get_secret("production", "database-url", GetOpts::default()).await?;
262    /// println!("Secret version: {}", secret.version);
263    ///
264    /// // Get without cache
265    /// let opts = GetOpts { use_cache: false, ..Default::default() };
266    /// let fresh_secret = client.get_secret("production", "api-key", opts).await?;
267    ///
268    /// // Conditional get with ETag
269    /// let opts = GetOpts {
270    ///     if_none_match: Some(secret.etag.unwrap()),
271    ///     ..Default::default()
272    /// };
273    /// match client.get_secret("production", "database-url", opts).await {
274    ///     Ok(updated) => println!("Secret was updated"),
275    ///     Err(e) if e.status_code() == Some(304) => println!("Not modified"),
276    ///     Err(e) => return Err(e.into()),
277    /// }
278    /// # Ok(())
279    /// # }
280    /// ```
281    pub async fn get_secret(&self, namespace: &str, key: &str, opts: GetOpts) -> Result<Secret> {
282        let cache_key = format!("{}/{}", namespace, key);
283
284        // Check cache if enabled and requested
285        if opts.use_cache {
286            if let Some(cached) = self.get_from_cache(&cache_key).await {
287                return Ok(cached);
288            }
289        }
290
291        // Build request
292        let url = self.endpoints.get_secret(namespace, key);
293        let mut request = self.build_request(Method::GET, &url)?;
294
295        // Add conditional headers
296        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        // Execute with retry
304        let response = self.execute_with_retry(request).await?;
305
306        // Handle 304 Not Modified
307        if response.status() == StatusCode::NOT_MODIFIED {
308            // Try to return from cache if available
309            if let Some(cached) = self.get_from_cache(&cache_key).await {
310                return Ok(cached);
311            }
312            // If not in cache, this is an error
313            return Err(Error::Other(
314                "Server returned 304 but no cached entry found".to_string(),
315            ));
316        }
317
318        // Parse response
319        let secret = self.parse_get_response(response, namespace, key).await?;
320
321        // Cache the secret if caching is enabled AND use_cache is true
322        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    /// Put a secret into the store
330    ///
331    /// Creates or updates a secret in the specified namespace.
332    /// Automatically invalidates any cached value for this key.
333    ///
334    /// # Arguments
335    ///
336    /// * `namespace` - The namespace to store the secret in
337    /// * `key` - The key for the secret
338    /// * `value` - The secret value (will be securely stored)
339    /// * `opts` - Options including TTL, metadata, and idempotency key
340    ///
341    /// # Returns
342    ///
343    /// A `PutResult` containing the operation details and timestamp.
344    ///
345    /// # Security
346    ///
347    /// The secret value is transmitted over HTTPS and stored encrypted.
348    /// The SDK uses the `secrecy` crate to prevent accidental exposure
349    /// of secret values in logs or debug output.
350    ///
351    /// # Example
352    ///
353    /// ```no_run
354    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, PutOpts};
355    /// # use serde_json::json;
356    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
357    /// // Simple put
358    /// client.put_secret("production", "new-key", "secret-value", PutOpts::default()).await?;
359    ///
360    /// // Put with TTL and metadata
361    /// let opts = PutOpts {
362    ///     ttl_seconds: Some(3600), // Expires in 1 hour
363    ///     metadata: Some(json!({
364    ///         "owner": "backend-team",
365    ///         "rotation_date": "2024-12-01"
366    ///     })),
367    ///     idempotency_key: Some("deploy-12345".to_string()),
368    /// };
369    /// client.put_secret("production", "api-key", "new-api-key", opts).await?;
370    /// # Ok(())
371    /// # }
372    /// ```
373    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        // Invalidate cache for this key
381        if let Some(cache) = &self.cache {
382            let cache_key = format!("{}/{}", namespace, key);
383            cache.invalidate(&cache_key).await;
384        }
385
386        // Build request body
387        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        // Build request
399        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        // Add idempotency key if provided
404        if let Some(idempotency_key) = &opts.idempotency_key {
405            request = request.header("X-Idempotency-Key", idempotency_key);
406        }
407
408        // Execute with retry
409        let response = self.execute_with_retry(request).await?;
410
411        // Parse response
412        self.parse_json_response(response).await
413    }
414
415    /// Delete a secret from the store
416    pub async fn delete_secret(&self, namespace: &str, key: &str) -> Result<DeleteResult> {
417        // Invalidate cache for this key
418        if let Some(cache) = &self.cache {
419            let cache_key = format!("{}/{}", namespace, key);
420            cache.invalidate(&cache_key).await;
421        }
422
423        // Build request
424        let url = self.endpoints.delete_secret(namespace, key);
425        let request = self.build_request(Method::DELETE, &url)?;
426
427        // Execute with retry
428        let response = self.execute_with_retry(request).await?;
429        let request_id = header_str(response.headers(), "x-request-id");
430
431        // Check status
432        let deleted = response.status() == StatusCode::NO_CONTENT;
433
434        Ok(DeleteResult {
435            deleted,
436            request_id,
437        })
438    }
439
440    /// List secrets in a namespace
441    pub async fn list_secrets(&self, namespace: &str, opts: ListOpts) -> Result<ListSecretsResult> {
442        // Build URL with query parameters
443        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        // Build and execute request
462        let request = self.build_request(Method::GET, &url)?;
463        let response = self.execute_with_retry(request).await?;
464
465        // Parse response
466        self.parse_json_response(response).await
467    }
468
469    /// Batch get secrets
470    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        // Build query parameters
479        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        // Add format parameter
496        let separator = if url.contains('?') { '&' } else { '?' };
497        url.push_str(&format!("{}format={}", separator, format.as_str()));
498
499        // Build and execute request
500        let request = self.build_request(Method::GET, &url)?;
501        let response = self.execute_with_retry(request).await?;
502
503        // Check status
504        if !response.status().is_success() {
505            return Err(self.parse_error_response(response).await);
506        }
507
508        // Parse response based on format
509        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    /// Batch operate on secrets
522    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        // Invalidate cache for all affected keys
530        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        // Build request body
538        let body = serde_json::json!({
539            "operations": operations,
540            "transactional": transactional,
541        });
542
543        // Build request
544        let url = self.endpoints.batch_operate(namespace);
545        let mut request = self.build_request(Method::POST, &url)?;
546        request = request.json(&body);
547
548        // Add idempotency key if provided
549        if let Some(key) = idempotency_key {
550            request = request.header("X-Idempotency-Key", key);
551        }
552
553        // Execute with retry
554        let response = self.execute_with_retry(request).await?;
555
556        // Parse response
557        self.parse_json_response(response).await
558    }
559
560    /// Export secrets as environment variables
561    ///
562    /// Exports all secrets from a namespace in the specified format.
563    /// Supports conditional requests using ETag for efficient caching.
564    ///
565    /// # Arguments
566    ///
567    /// * `namespace` - The namespace to export
568    /// * `opts` - Export options including format and conditional request headers
569    ///
570    /// # Returns
571    ///
572    /// Returns `EnvExport::Json` for JSON format or `EnvExport::Text` for other formats.
573    ///
574    /// # Errors
575    ///
576    /// * Returns `Error::Http` with status 304 if content hasn't changed (when using if_none_match)
577    /// * Returns other errors for authentication, network, or server issues
578    ///
579    /// # Example
580    ///
581    /// ```no_run
582    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, ExportEnvOpts, ExportFormat};
583    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
584    /// // Simple export
585    /// let opts = ExportEnvOpts {
586    ///     format: ExportFormat::Dotenv,
587    ///     ..Default::default()
588    /// };
589    /// let export = client.export_env("production", opts).await?;
590    ///
591    /// // Conditional export with ETag
592    /// let opts = ExportEnvOpts {
593    ///     format: ExportFormat::Json,
594    ///     use_cache: true,
595    ///     if_none_match: Some("previous-etag".to_string()),
596    /// };
597    /// match client.export_env("production", opts).await {
598    ///     Ok(export) => println!("Content updated"),
599    ///     Err(e) if e.status_code() == Some(304) => println!("Not modified"),
600    ///     Err(e) => return Err(e.into()),
601    /// }
602    /// # Ok(())
603    /// # }
604    /// ```
605    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        // Build request
610        let mut request = self.build_request(Method::GET, &url)?;
611
612        // Add conditional header if provided
613        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        // Handle 304 Not Modified
620        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        // Check other error statuses
630        if !response.status().is_success() {
631            return Err(self.parse_error_response(response).await);
632        }
633
634        // NOTE: opts.use_cache is currently not implemented.
635        // Future implementation will cache responses with namespace/env/{format} as key
636        // and use ETag headers for cache validation (304 responses).
637        // For now, this flag has no effect.
638
639        // Parse response based on format
640        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    /// List all namespaces
653    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    /// Create a new namespace
666    ///
667    /// Creates a new namespace in the secret store. Requires global admin permissions.
668    ///
669    /// # Arguments
670    ///
671    /// * `name` - The name of the namespace to create
672    /// * `description` - Optional description for the namespace
673    /// * `idempotency_key` - Optional idempotency key to prevent duplicate creation
674    ///
675    /// # Returns
676    ///
677    /// A `CreateNamespaceResult` containing the creation details.
678    ///
679    /// # Errors
680    ///
681    /// * `Error::Http` with status 403 if not authorized to create namespaces
682    /// * `Error::Http` with status 409 if the namespace already exists
683    /// * `Error::Http` with status 400 for invalid namespace names
684    ///
685    /// # Example
686    ///
687    /// ```no_run
688    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
689    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
690    /// let result = client.create_namespace(
691    ///     "production",
692    ///     Some("Production environment secrets".to_string()),
693    ///     None
694    /// ).await?;
695    /// println!("Created namespace: {}", result.namespace);
696    /// # Ok(())
697    /// # }
698    /// ```
699    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        // Add idempotency key if provided
715        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    /// Get namespace information
729    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    /// Initialize a namespace with a template
742    ///
743    /// Initializes a new namespace using a predefined template to create
744    /// a set of initial secrets.
745    ///
746    /// # Arguments
747    ///
748    /// * `namespace` - The namespace to initialize
749    /// * `template` - The template configuration
750    /// * `idempotency_key` - Optional idempotency key to prevent duplicate initialization
751    ///
752    /// # Example
753    ///
754    /// ```no_run
755    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, NamespaceTemplate};
756    /// # use serde_json::json;
757    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
758    /// let template = NamespaceTemplate {
759    ///     template: "web-app".to_string(),
760    ///     params: json!({
761    ///         "environment": "staging",
762    ///         "region": "us-west-2"
763    ///     }),
764    /// };
765    ///
766    /// let result = client.init_namespace(
767    ///     "staging-app",
768    ///     template,
769    ///     Some("init-staging-12345".to_string())
770    /// ).await?;
771    /// println!("Created {} secrets", result.secrets_created);
772    /// # Ok(())
773    /// # }
774    /// ```
775    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        // Add idempotency key if provided
786        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    /// Delete a namespace and all its secrets
800    ///
801    /// **Warning**: This operation is irreversible and will delete all secrets
802    /// in the namespace. Use with extreme caution.
803    ///
804    /// This operation may take some time for namespaces with many secrets.
805    /// The response includes the number of secrets that were deleted.
806    ///
807    /// # Arguments
808    ///
809    /// * `namespace` - The namespace to delete
810    ///
811    /// # Returns
812    ///
813    /// A `DeleteNamespaceResult` containing deletion details.
814    ///
815    /// # Errors
816    ///
817    /// * `Error::Http` with status 404 if the namespace doesn't exist
818    /// * `Error::Http` with status 403 if deletion is forbidden
819    /// * `Error::Http` with status 409 if namespace has protection enabled
820    ///
821    /// # Example
822    ///
823    /// ```no_run
824    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
825    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
826    /// let result = client.delete_namespace("test-namespace").await?;
827    /// println!("Deleted {} secrets from namespace {}",
828    ///     result.secrets_deleted,
829    ///     result.namespace
830    /// );
831    /// # Ok(())
832    /// # }
833    /// ```
834    pub async fn delete_namespace(&self, namespace: &str) -> Result<DeleteNamespaceResult> {
835        // Clear all cached entries for this namespace
836        if let Some(cache) = &self.cache {
837            // TODO: Optimize to only clear entries for this specific namespace
838            // For now, we'll invalidate all cache to ensure consistency
839            cache.invalidate_all();
840            debug!(
841                "Cleared all cache entries due to namespace deletion: {}",
842                namespace
843            );
844        }
845
846        // Build request
847        let url = self.endpoints.delete_namespace(namespace);
848        let request = self.build_request(Method::DELETE, &url)?;
849
850        // Execute with retry
851        let response = self.execute_with_retry(request).await?;
852
853        // Check status
854        if !response.status().is_success() {
855            return Err(self.parse_error_response(response).await);
856        }
857
858        // Extract request ID from headers
859        let request_id = header_str(response.headers(), "x-request-id");
860
861        // Parse response
862        let mut result: DeleteNamespaceResult = self.parse_json_response(response).await?;
863
864        // Set request_id if not already in the response body
865        if result.request_id.is_none() {
866            result.request_id = request_id;
867        }
868
869        Ok(result)
870    }
871
872    /// Delete a namespace and all its secrets with idempotency support
873    ///
874    /// Same as `delete_namespace` but with idempotency key support for safe retries.
875    ///
876    /// # Arguments
877    ///
878    /// * `namespace` - The namespace to delete
879    /// * `idempotency_key` - Optional idempotency key to prevent duplicate deletion
880    ///
881    /// # Example
882    ///
883    /// ```no_run
884    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
885    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
886    /// let result = client.delete_namespace_idempotent(
887    ///     "test-namespace",
888    ///     Some("delete-ns-12345".to_string())
889    /// ).await?;
890    /// println!("Deleted {} secrets", result.secrets_deleted);
891    /// # Ok(())
892    /// # }
893    /// ```
894    pub async fn delete_namespace_idempotent(
895        &self,
896        namespace: &str,
897        idempotency_key: Option<String>,
898    ) -> Result<DeleteNamespaceResult> {
899        // Clear all cached entries for this namespace
900        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        // Build request
909        let url = self.endpoints.delete_namespace(namespace);
910        let mut request = self.build_request(Method::DELETE, &url)?;
911
912        // Add idempotency key if provided
913        if let Some(key) = idempotency_key {
914            request = request.header("X-Idempotency-Key", key);
915        }
916
917        // Execute with retry
918        let response = self.execute_with_retry(request).await?;
919
920        // Check status
921        if !response.status().is_success() {
922            return Err(self.parse_error_response(response).await);
923        }
924
925        // Extract request ID from headers
926        let request_id = header_str(response.headers(), "x-request-id");
927
928        // Parse response
929        let mut result: DeleteNamespaceResult = self.parse_json_response(response).await?;
930
931        // Set request_id if not already in the response body
932        if result.request_id.is_none() {
933            result.request_id = request_id;
934        }
935
936        Ok(result)
937    }
938
939    /// List versions of a secret
940    pub async fn list_versions(&self, namespace: &str, key: &str) -> Result<VersionList> {
941        // Build and execute request
942        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        // Parse response
947        self.parse_json_response(response).await
948    }
949
950    /// Get a specific version of a secret
951    pub async fn get_version(&self, namespace: &str, key: &str, version: i32) -> Result<Secret> {
952        // Build and execute request
953        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        // Parse response (similar to get_secret)
958        self.parse_get_response(response, namespace, key).await
959    }
960
961    /// Rollback a secret to a previous version
962    pub async fn rollback(
963        &self,
964        namespace: &str,
965        key: &str,
966        version: i32,
967    ) -> Result<RollbackResult> {
968        // Invalidate cache for this key since we're changing it
969        if let Some(cache) = &self.cache {
970            let cache_key = format!("{}/{}", namespace, key);
971            cache.invalidate(&cache_key).await;
972        }
973
974        // Build request with empty body (comment is optional)
975        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        // Execute with retry
980        let response = self.execute_with_retry(request).await?;
981
982        // Parse response
983        self.parse_json_response(response).await
984    }
985
986    /// Query audit logs
987    pub async fn audit(&self, query: AuditQuery) -> Result<AuditResult> {
988        // Build URL with query parameters
989        let mut url = self.endpoints.audit();
990        let mut params = Vec::new();
991
992        // Add query parameters
993        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(&params.join("&"));
1039        }
1040
1041        // Build and execute request
1042        let request = self.build_request(Method::GET, &url)?;
1043        let response = self.execute_with_retry(request).await?;
1044
1045        // Parse response
1046        self.parse_json_response(response).await
1047    }
1048
1049    /// List all API keys
1050    ///
1051    /// Retrieves a list of all API keys associated with the current account.
1052    /// The response includes metadata about each key but not the key values themselves.
1053    ///
1054    /// # Returns
1055    ///
1056    /// A `ListApiKeysResult` containing the list of API keys and total count.
1057    ///
1058    /// # Errors
1059    ///
1060    /// * `Error::Http` with status 403 if not authorized to list keys
1061    ///
1062    /// # Example
1063    ///
1064    /// ```no_run
1065    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
1066    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1067    /// let keys = client.list_api_keys().await?;
1068    /// for key in &keys.keys {
1069    ///     println!("Key {}: {} (active: {})", key.id, key.name, key.active);
1070    /// }
1071    /// # Ok(())
1072    /// # }
1073    /// ```
1074    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    /// Create a new API key
1094    ///
1095    /// Creates a new API key with the specified permissions and restrictions.
1096    /// The key value is only returned in the creation response and cannot be retrieved later.
1097    ///
1098    /// # Arguments
1099    ///
1100    /// * `request` - The API key creation request containing name, permissions, etc.
1101    /// * `idempotency_key` - Optional idempotency key to prevent duplicate creation
1102    ///
1103    /// # Returns
1104    ///
1105    /// An `ApiKeyInfo` containing the newly created key details including the key value.
1106    ///
1107    /// # Security
1108    ///
1109    /// The returned API key value should be stored securely. It cannot be retrieved
1110    /// again after this call.
1111    ///
1112    /// # Errors
1113    ///
1114    /// * `Error::Http` with status 403 if not authorized to create keys
1115    /// * `Error::Http` with status 400 for invalid permissions or parameters
1116    ///
1117    /// # Example
1118    ///
1119    /// ```no_run
1120    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, CreateApiKeyRequest};
1121    /// # use secrecy::ExposeSecret;
1122    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1123    /// let request = CreateApiKeyRequest {
1124    ///     name: "CI/CD Pipeline Key".to_string(),
1125    ///     expires_at: Some("2024-12-31T23:59:59Z".to_string()),
1126    ///     namespaces: vec!["production".to_string()],
1127    ///     permissions: vec!["read".to_string()],
1128    ///     metadata: None,
1129    /// };
1130    ///
1131    /// let key_info = client.create_api_key(request, Some("unique-key-123".to_string())).await?;
1132    /// if let Some(key) = &key_info.key {
1133    ///     println!("New API key: {}", key.expose_secret());
1134    ///     // Store this securely - it won't be available again!
1135    /// }
1136    /// # Ok(())
1137    /// # }
1138    /// ```
1139    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        // Add idempotency key if provided
1149        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    /// Get API key details
1163    ///
1164    /// Retrieves detailed information about a specific API key.
1165    /// Note that the key value itself is never returned for security reasons.
1166    ///
1167    /// # Arguments
1168    ///
1169    /// * `key_id` - The ID of the API key to retrieve
1170    ///
1171    /// # Returns
1172    ///
1173    /// An `ApiKeyInfo` with the key's metadata (without the key value).
1174    ///
1175    /// # Errors
1176    ///
1177    /// * `Error::Http` with status 404 if the key doesn't exist
1178    /// * `Error::Http` with status 403 if not authorized to view the key
1179    ///
1180    /// # Example
1181    ///
1182    /// ```no_run
1183    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
1184    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1185    /// let key_info = client.get_api_key("key_123abc").await?;
1186    /// println!("Key {} last used: {:?}", key_info.name, key_info.last_used_at);
1187    /// # Ok(())
1188    /// # }
1189    /// ```
1190    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    /// Revoke an API key
1203    ///
1204    /// Revokes an API key, immediately invalidating it for future use.
1205    /// This operation is irreversible.
1206    ///
1207    /// # Arguments
1208    ///
1209    /// * `key_id` - The ID of the API key to revoke
1210    ///
1211    /// # Returns
1212    ///
1213    /// A `RevokeApiKeyResult` confirming the revocation.
1214    ///
1215    /// # Errors
1216    ///
1217    /// * `Error::Http` with status 404 if the key doesn't exist
1218    /// * `Error::Http` with status 403 if not authorized to revoke the key
1219    ///
1220    /// # Example
1221    ///
1222    /// ```no_run
1223    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
1224    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1225    /// let result = client.revoke_api_key("key_123abc").await?;
1226    /// println!("Revoked key: {}", result.key_id);
1227    /// # Ok(())
1228    /// # }
1229    /// ```
1230    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    /// Search secrets across namespaces
1250    ///
1251    /// Searches for secrets matching a pattern across multiple namespaces.
1252    /// Supports exact, prefix, and contains matching modes.
1253    ///
1254    /// # Arguments
1255    ///
1256    /// * `opts` - Search options including pattern, mode, and namespace filters
1257    ///
1258    /// # Returns
1259    ///
1260    /// A `SearchSecretsResult` containing matching secrets.
1261    ///
1262    /// # Errors
1263    ///
1264    /// * `Error::Http` with status 400 if the pattern is empty
1265    /// * `Error::Http` with status 403 if no accessible namespaces
1266    ///
1267    /// # Example
1268    ///
1269    /// ```no_run
1270    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, SearchSecretsOpts, SearchMode};
1271    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1272    /// // Search for all secrets containing "api" in their key
1273    /// let opts = SearchSecretsOpts {
1274    ///     pattern: "api".to_string(),
1275    ///     mode: SearchMode::Contains,
1276    ///     ..Default::default()
1277    /// };
1278    /// let result = client.search_secrets(opts).await?;
1279    /// for m in &result.matches {
1280    ///     println!("{}/{}: v{}", m.namespace, m.key, m.version);
1281    /// }
1282    ///
1283    /// // Search with prefix in specific namespaces
1284    /// let opts = SearchSecretsOpts {
1285    ///     pattern: "db-".to_string(),
1286    ///     mode: SearchMode::Prefix,
1287    ///     namespaces: vec!["production".to_string(), "staging".to_string()],
1288    ///     include_values: false,
1289    /// };
1290    /// let result = client.search_secrets(opts).await?;
1291    /// println!("Found {} secrets", result.total);
1292    /// # Ok(())
1293    /// # }
1294    /// ```
1295    pub async fn search_secrets(&self, opts: SearchSecretsOpts) -> Result<SearchSecretsResult> {
1296        let mut url = self.endpoints.search_secrets();
1297
1298        // Build query parameters
1299        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(&params.join("&"));
1322
1323        // Build and execute request
1324        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    /// Bulk delete a secret across namespaces
1335    ///
1336    /// Deletes a secret with the given key from multiple namespaces in a single operation.
1337    /// Requires admin permission on each namespace.
1338    ///
1339    /// # Arguments
1340    ///
1341    /// * `opts` - Bulk delete options including the key and target namespaces
1342    ///
1343    /// # Returns
1344    ///
1345    /// A `BulkDeleteResult` containing lists of successful and failed deletions.
1346    ///
1347    /// # Errors
1348    ///
1349    /// * `Error::Http` with status 400 if the key is empty
1350    /// * `Error::Http` with status 403 if no accessible namespaces
1351    ///
1352    /// # Example
1353    ///
1354    /// ```no_run
1355    /// # use secret_store_sdk::{Client, ClientBuilder, Auth, BulkDeleteOpts};
1356    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1357    /// // Delete a secret from all accessible namespaces
1358    /// let opts = BulkDeleteOpts {
1359    ///     key: "deprecated-api-key".to_string(),
1360    ///     namespaces: vec![], // empty = all accessible
1361    /// };
1362    /// let result = client.bulk_delete_secrets(opts).await?;
1363    /// println!("Deleted from: {:?}", result.deleted);
1364    /// if !result.failed.is_empty() {
1365    ///     println!("Failed in: {:?}", result.failed);
1366    /// }
1367    ///
1368    /// // Delete from specific namespaces only
1369    /// let opts = BulkDeleteOpts {
1370    ///     key: "old-secret".to_string(),
1371    ///     namespaces: vec!["staging".to_string(), "dev".to_string()],
1372    /// };
1373    /// let result = client.bulk_delete_secrets(opts).await?;
1374    /// # Ok(())
1375    /// # }
1376    /// ```
1377    pub async fn bulk_delete_secrets(&self, opts: BulkDeleteOpts) -> Result<BulkDeleteResult> {
1378        let url = self.endpoints.bulk_delete();
1379
1380        // Build request body
1381        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        // Build and execute request
1393        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    /// Get API discovery information
1406    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    /// Check liveness
1419    ///
1420    /// Performs a simple liveness check against the service.
1421    /// Returns `Ok(())` if the service is alive and responding.
1422    ///
1423    /// This endpoint is typically used by Kubernetes liveness probes.
1424    /// It does not check dependencies and should respond quickly.
1425    ///
1426    /// # Errors
1427    ///
1428    /// Returns an error if the service is not responding or returns
1429    /// a non-2xx status code.
1430    ///
1431    /// # Example
1432    ///
1433    /// ```no_run
1434    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
1435    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1436    /// match client.livez().await {
1437    ///     Ok(()) => println!("Service is alive"),
1438    ///     Err(e) => eprintln!("Service is down: {}", e),
1439    /// }
1440    /// # Ok(())
1441    /// # }
1442    /// ```
1443    pub async fn livez(&self) -> Result<()> {
1444        let url = self.endpoints.livez();
1445        let request = self.build_request(Method::GET, &url)?;
1446
1447        // Execute without retry for health checks
1448        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    /// Check readiness with detailed status
1458    ///
1459    /// Performs a comprehensive readiness check that may include
1460    /// checking dependencies (database, cache, etc.).
1461    ///
1462    /// This endpoint is typically used by Kubernetes readiness probes
1463    /// to determine if the service is ready to accept traffic.
1464    ///
1465    /// # Returns
1466    ///
1467    /// Returns a `HealthStatus` with details about the service health
1468    /// including individual component checks.
1469    ///
1470    /// # Errors
1471    ///
1472    /// Returns an error if the service is not ready or if the
1473    /// request fails.
1474    ///
1475    /// # Example
1476    ///
1477    /// ```no_run
1478    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
1479    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1480    /// let health = client.readyz().await?;
1481    /// println!("Service status: {}", health.status);
1482    ///
1483    /// for (check, result) in &health.checks {
1484    ///     println!("  {}: {} ({}ms)",
1485    ///         check,
1486    ///         result.status,
1487    ///         result.duration_ms.unwrap_or(0)
1488    ///     );
1489    /// }
1490    /// # Ok(())
1491    /// # }
1492    /// ```
1493    pub async fn readyz(&self) -> Result<HealthStatus> {
1494        let url = self.endpoints.readyz();
1495        let request = self.build_request(Method::GET, &url)?;
1496
1497        // Execute without retry for health checks
1498        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    /// Get service metrics
1508    ///
1509    /// Retrieves metrics from the service in Prometheus format.
1510    /// This endpoint may require special authentication using a metrics token.
1511    ///
1512    /// # Arguments
1513    ///
1514    /// * `metrics_token` - Optional metrics-specific authentication token.
1515    ///   If not provided, uses the client's default authentication.
1516    ///
1517    /// # Returns
1518    ///
1519    /// Returns the metrics as a raw string in Prometheus exposition format.
1520    ///
1521    /// # Errors
1522    ///
1523    /// * `Error::Http` with status 401 if authentication fails
1524    /// * `Error::Http` with status 403 if not authorized to view metrics
1525    ///
1526    /// # Example
1527    ///
1528    /// ```no_run
1529    /// # use secret_store_sdk::{Client, ClientBuilder, Auth};
1530    /// # async fn example(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
1531    /// // Using default authentication
1532    /// let metrics = client.metrics(None).await?;
1533    /// println!("Metrics:\n{}", metrics);
1534    ///
1535    /// // Using specific metrics token
1536    /// let metrics = client.metrics(Some("metrics-token-xyz")).await?;
1537    /// println!("Metrics with token:\n{}", metrics);
1538    /// # Ok(())
1539    /// # }
1540    /// ```
1541    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        // Add metrics-specific token if provided
1546        if let Some(token) = metrics_token {
1547            request = request.header("X-Metrics-Token", token);
1548        }
1549
1550        // Execute without retry for metrics endpoint
1551        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    // Helper methods
1561
1562    /// Build a request with common headers
1563    fn build_request(&self, method: Method, url: &str) -> Result<reqwest::RequestBuilder> {
1564        let mut builder = self.http.request(method, url);
1565
1566        // Generate and add request ID
1567        let request_id = generate_request_id();
1568        builder = builder.header("X-Request-ID", &request_id);
1569
1570        // Add trace headers
1571        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    /// Execute a request with retry logic
1579    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        // Extract method and URL for metrics
1588        #[cfg(feature = "metrics")]
1589        let (method, path) = {
1590            // Try to build a request to extract metadata
1591            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            // Get current auth header (may be refreshed)
1606            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            // Clone the base request and add current auth header
1612            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            // Create backoff strategy for retries
1618            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            // Calculate max elapsed time based on timeout and retries
1627            // Allow enough time for all retries with their respective timeouts
1628            backoff.max_elapsed_time = if max_retries > 0 {
1629                let timeout_secs = self.config.timeout.as_secs();
1630                // Allow time for all retries plus some buffer for backoff delays
1631                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            // Execute with backoff retry
1640            let result = retry_notify(
1641                backoff,
1642                || async {
1643                    let current_retry = retry_count.load(std::sync::atomic::Ordering::Relaxed);
1644                    // Clone request for this attempt
1645                    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                    // Track active connections
1661                    #[cfg(feature = "metrics")]
1662                    self.metrics.inc_active_connections();
1663
1664                    // Start timing request
1665                    #[cfg(feature = "metrics")]
1666                    let start_time = std::time::Instant::now();
1667
1668                    let response_result = self.http.execute(req).await;
1669
1670                    // Decrement active connections
1671                    #[cfg(feature = "metrics")]
1672                    self.metrics.dec_active_connections();
1673
1674                    match response_result {
1675                        Ok(response) => {
1676                            let status = response.status();
1677
1678                            // Handle 401 - but don't retry within backoff if we can refresh token
1679                            if status == StatusCode::UNAUTHORIZED
1680                                && token_refresh_count == 0
1681                                && auth.supports_refresh()
1682                            {
1683                                // Return a special error that we'll handle outside the backoff retry
1684                                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                            // Check if error is retryable
1693                            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                            // Non-retryable HTTP errors
1712                            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                            // Record successful request metrics
1718                            #[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                    // Try to refresh token once
1761                    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 to retry with new token
1767                    continue;
1768                }
1769                Err(e) => return Err(e),
1770            }
1771        }
1772    }
1773
1774    /// Execute a request without retry logic (for health checks)
1775    async fn execute_without_retry(
1776        &self,
1777        request_builder: reqwest::RequestBuilder,
1778    ) -> Result<Response> {
1779        // Get auth header
1780        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        // Add auth header
1788        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        // Execute request
1794        self.http.execute(request).await.map_err(Error::from)
1795    }
1796
1797    /// Parse error response from server
1798    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        // Try to parse JSON error response
1803        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    /// Parse JSON response
1820    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    /// Parse get secret response
1828    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        // Extract headers
1837        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        // Parse body
1842        #[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        // Parse timestamps
1854        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    /// Get secret from cache
1884    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                // Check if expired
1890                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                    // Record cache hit metric
1901                    #[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                // Record cache miss metric
1916                #[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    /// Cache a secret
1928    async fn cache_secret(&self, cache_key: &str, secret: &Secret) {
1929        let Some(cache) = &self.cache else { return };
1930
1931        // Determine TTL from Cache-Control or use default
1932        let ttl = if let Some(_etag) = &secret.etag {
1933            // If we have an etag, use a longer TTL since we can validate
1934            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    // Helper function to create test client that works with HTTP URLs
1967    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            // In tests without the feature, we'll just use a dummy HTTPS URL
1979            // The actual URL doesn't matter since we're mocking
1980            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        // Mock successful response
2006        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        // First request
2089        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) // Should only be called once
2097            .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        // First request - should hit server
2116        let secret1 = client
2117            .get_secret("cache-ns", "cache-key", GetOpts::default())
2118            .await
2119            .unwrap();
2120        assert_eq!(secret1.version, 2);
2121
2122        // Small delay to ensure cache is populated
2123        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2124
2125        // Second request - should hit cache
2126        let secret2 = client
2127            .get_secret("cache-ns", "cache-key", GetOpts::default())
2128            .await
2129            .unwrap();
2130        assert_eq!(secret2.version, 2);
2131
2132        // Verify cache hit
2133        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        // Mount both mocks at once with more specific one first
2151        // Second request with etag - return 304 (more specific, so should match first)
2152        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        // First request - return data (less specific)
2162        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        // First request
2190        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        // Clear cache to force second request to hit server
2197        client.clear_cache();
2198
2199        // Second request with etag
2200        let opts = GetOpts {
2201            use_cache: false, // Disable cache to ensure we hit the server
2202            if_none_match: Some("etag-v1".to_string()), // Without quotes
2203            if_modified_since: None,
2204        };
2205        // This should return error since cache was cleared and server returns 304
2206        let result = client.get_secret("test-ns", "test-key", opts).await;
2207        assert!(result.is_err());
2208
2209        // The error should indicate that we got 304 but have no cache
2210        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        // First two requests fail, third succeeds
2289        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()); // Should succeed after retries
2329    }
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        // Check first entry
2539        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}