Skip to main content

fakecloud_secretsmanager/
service.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use chrono::Utc;
6use http::StatusCode;
7use serde_json::{json, Value};
8
9use tokio::sync::Mutex as AsyncMutex;
10
11use fakecloud_core::delivery::DeliveryBus;
12use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
13use fakecloud_core::validation::*;
14use fakecloud_persistence::SnapshotStore;
15
16use crate::state::{
17    RotationRules, Secret, SecretVersion, SecretsManagerSnapshot, SecretsManagerState,
18    SharedSecretsManagerState, SECRETSMANAGER_SNAPSHOT_SCHEMA_VERSION,
19};
20
21/// Information needed to invoke the rotation Lambda after releasing state lock.
22struct RotationInvocation {
23    lambda_arn: String,
24    secret_id: String,
25    client_request_token: String,
26}
27
28/// Result of an idempotency check against an existing
29/// `ClientRequestToken` / version id.
30enum VersionIdempotency {
31    /// The version id isn't in the secret yet — this is a fresh write.
32    NotFound,
33    /// The version id exists and stores the exact same payload we're
34    /// about to write — callers should return the existing version as
35    /// a successful no-op response.
36    Match,
37    /// The version id exists but stores a different payload — AWS
38    /// surfaces this as a `ResourceExistsException`.
39    Conflict,
40}
41
42/// Classify whether a proposed write collides with an existing
43/// version. AWS uses `ClientRequestToken` as a client-side idempotency
44/// key, so a repeat write of the exact same payload is a success but a
45/// repeat with a different payload is a `ResourceExistsException`.
46fn check_secret_version_idempotency(
47    versions: &HashMap<String, SecretVersion>,
48    version_id: &str,
49    secret_string: &Option<String>,
50    secret_binary: &Option<Vec<u8>>,
51) -> VersionIdempotency {
52    let Some(existing) = versions.get(version_id) else {
53        return VersionIdempotency::NotFound;
54    };
55    if &existing.secret_string == secret_string && &existing.secret_binary == secret_binary {
56        VersionIdempotency::Match
57    } else {
58        VersionIdempotency::Conflict
59    }
60}
61
62/// Actions that mutate Secrets Manager state.
63fn is_mutating_action(action: &str) -> bool {
64    matches!(
65        action,
66        "CreateSecret"
67            | "PutSecretValue"
68            | "UpdateSecret"
69            | "DeleteSecret"
70            | "RestoreSecret"
71            | "TagResource"
72            | "UntagResource"
73            | "RotateSecret"
74            | "CancelRotateSecret"
75            | "UpdateSecretVersionStage"
76            | "PutResourcePolicy"
77            | "DeleteResourcePolicy"
78            | "ReplicateSecretToRegions"
79            | "RemoveRegionsFromReplication"
80            | "StopReplicationToReplica"
81    )
82}
83
84pub struct SecretsManagerService {
85    state: SharedSecretsManagerState,
86    delivery_bus: Option<Arc<DeliveryBus>>,
87    snapshot_store: Option<Arc<dyn SnapshotStore>>,
88    snapshot_lock: Arc<AsyncMutex<()>>,
89}
90
91impl SecretsManagerService {
92    pub fn new(state: SharedSecretsManagerState) -> Self {
93        Self {
94            state,
95            delivery_bus: None,
96            snapshot_store: None,
97            snapshot_lock: Arc::new(AsyncMutex::new(())),
98        }
99    }
100
101    pub fn with_delivery(mut self, delivery_bus: Arc<DeliveryBus>) -> Self {
102        self.delivery_bus = Some(delivery_bus);
103        self
104    }
105
106    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
107        self.snapshot_store = Some(store);
108        self
109    }
110
111    /// Persist current state as a snapshot. Held across the
112    /// clone-serialize-write sequence to prevent stale-last writes,
113    /// with serde + file I/O offloaded to the blocking pool.
114    async fn save_snapshot(&self) {
115        let Some(store) = self.snapshot_store.clone() else {
116            return;
117        };
118        let _guard = self.snapshot_lock.lock().await;
119        let snapshot = SecretsManagerSnapshot {
120            schema_version: SECRETSMANAGER_SNAPSHOT_SCHEMA_VERSION,
121            state: None,
122            accounts: Some(self.state.read().clone()),
123        };
124        let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
125            let bytes = serde_json::to_vec(&snapshot)
126                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
127            store.save(&bytes)
128        })
129        .await;
130        match join {
131            Ok(Ok(())) => {}
132            Ok(Err(err)) => tracing::error!(%err, "failed to write secretsmanager snapshot"),
133            Err(err) => tracing::error!(%err, "secretsmanager snapshot task panicked"),
134        }
135    }
136
137    fn create_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
138        let input = CreateSecretInput::from_body(&req.json_body())?;
139        let has_value = input.secret_string.is_some() || input.secret_binary.is_some();
140
141        let mut accounts = self.state.write();
142        let state = accounts.get_or_create(&req.account_id);
143
144        if let Some(existing) = state.secrets.get(&input.name) {
145            if let Some(ref token) = input.client_request_token {
146                match check_secret_version_idempotency(
147                    &existing.versions,
148                    token,
149                    &input.secret_string,
150                    &input.secret_binary,
151                ) {
152                    VersionIdempotency::Match => {
153                        let mut response = json!({
154                            "ARN": existing.arn,
155                            "Name": existing.name,
156                            "VersionId": token,
157                        });
158                        if !has_value {
159                            response.as_object_mut().unwrap().remove("VersionId");
160                        }
161                        return Ok(AwsResponse::ok_json(response));
162                    }
163                    VersionIdempotency::Conflict => {
164                        return Err(AwsServiceError::aws_error(
165                            StatusCode::BAD_REQUEST,
166                            "ResourceExistsException",
167                            format!(
168                                "You can't use ClientRequestToken {token} because that value is already in use for a version of secret {}.",
169                                existing.arn
170                            ),
171                        ));
172                    }
173                    VersionIdempotency::NotFound => {}
174                }
175            }
176            return Err(AwsServiceError::aws_error(
177                StatusCode::BAD_REQUEST,
178                "ResourceExistsException",
179                format!(
180                    "The operation failed because the secret {} already exists.",
181                    input.name
182                ),
183            ));
184        }
185
186        let arn = format!(
187            "arn:aws:secretsmanager:{}:{}:secret:{}-{}",
188            req.region,
189            req.account_id,
190            input.name,
191            &uuid::Uuid::new_v4().to_string()[..6]
192        );
193
194        let now = Utc::now();
195
196        let (versions, current_version_id, version_id_for_response) = if has_value {
197            let vid = input
198                .client_request_token
199                .clone()
200                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
201            let version = SecretVersion {
202                version_id: vid.clone(),
203                secret_string: input.secret_string,
204                secret_binary: input.secret_binary,
205                stages: vec!["AWSCURRENT".to_string()],
206                created_at: now,
207            };
208            let mut versions = std::collections::HashMap::new();
209            versions.insert(vid.clone(), version);
210            (versions, Some(vid.clone()), Some(vid))
211        } else {
212            (std::collections::HashMap::new(), None, None)
213        };
214
215        let tags_ever_set = !input.tags.is_empty();
216        let secret = Secret {
217            name: input.name.clone(),
218            arn: arn.clone(),
219            description: input.description,
220            kms_key_id: input.kms_key_id,
221            versions,
222            current_version_id,
223            tags: input.tags,
224            tags_ever_set,
225            deleted: false,
226            deletion_date: None,
227            created_at: now,
228            last_changed_at: now,
229            last_accessed_at: None,
230            rotation_enabled: None,
231            rotation_lambda_arn: None,
232            rotation_rules: None,
233            last_rotated_at: None,
234            resource_policy: None,
235        };
236
237        state.secrets.insert(input.name.clone(), secret);
238
239        let mut response = json!({
240            "ARN": arn,
241            "Name": input.name,
242        });
243        if let Some(vid) = version_id_for_response {
244            response["VersionId"] = json!(vid);
245        }
246
247        Ok(AwsResponse::ok_json(response))
248    }
249
250    fn get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
251        let body = req.json_body();
252        let secret_id = require_secret_id(&body)?;
253        validate_optional_string_length("versionId", body["VersionId"].as_str(), 32, 64)?;
254        validate_optional_string_length("versionStage", body["VersionStage"].as_str(), 1, 256)?;
255
256        let mut accounts = self.state.write();
257        let state = accounts.get_or_create(&req.account_id);
258        let secret = self.find_secret_mut(state, &secret_id)?;
259
260        if secret.deleted {
261            return Err(AwsServiceError::aws_error(
262                StatusCode::BAD_REQUEST,
263                "InvalidRequestException",
264                "You can't perform this operation on the secret because it was marked for deletion.",
265            ));
266        }
267
268        let requested_stage = body["VersionStage"].as_str().unwrap_or("AWSCURRENT");
269
270        // Determine which version to return
271        let version_id = body["VersionId"]
272            .as_str()
273            .map(|s| s.to_string())
274            .or_else(|| {
275                secret
276                    .versions
277                    .iter()
278                    .find(|(_, v)| v.stages.contains(&requested_stage.to_string()))
279                    .map(|(id, _)| id.clone())
280            });
281
282        let version_id = match version_id {
283            Some(vid) => vid,
284            None => {
285                // No versions exist
286                return Err(AwsServiceError::aws_error(
287                    StatusCode::NOT_FOUND,
288                    "ResourceNotFoundException",
289                    format!(
290                        "Secrets Manager can't find the specified secret value for staging label: {requested_stage}"
291                    ),
292                ));
293            }
294        };
295
296        let version = secret.versions.get(&version_id).ok_or_else(|| {
297            AwsServiceError::aws_error(
298                StatusCode::NOT_FOUND,
299                "ResourceNotFoundException",
300                format!(
301                    "Secrets Manager can't find the specified secret value for VersionId: {version_id}"
302                ),
303            )
304        })?;
305
306        // If VersionStage is specified with VersionId, verify they match
307        if body["VersionId"].as_str().is_some() {
308            if let Some(stage) = body["VersionStage"].as_str() {
309                if !version.stages.contains(&stage.to_string()) {
310                    return Err(AwsServiceError::aws_error(
311                        StatusCode::NOT_FOUND,
312                        "ResourceNotFoundException",
313                        "You provided a VersionStage that is not associated to the provided VersionId.",
314                    ));
315                }
316            }
317        }
318
319        // Only set last_accessed_at on successful retrieval
320        secret.last_accessed_at = Some(Utc::now());
321
322        let mut response = json!({
323            "ARN": secret.arn,
324            "Name": secret.name,
325            "VersionId": version.version_id,
326            "VersionStages": version.stages,
327            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
328        });
329
330        if let Some(ref s) = version.secret_string {
331            response["SecretString"] = json!(s);
332        }
333        if let Some(ref b) = version.secret_binary {
334            response["SecretBinary"] = json!(base64_encode(b));
335        }
336
337        Ok(AwsResponse::ok_json(response))
338    }
339
340    fn put_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
341        let body = req.json_body();
342        let secret_id = require_secret_id(&body)?;
343        validate_optional_string_length(
344            "clientRequestToken",
345            body["ClientRequestToken"].as_str(),
346            32,
347            64,
348        )?;
349        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
350
351        let secret_string = body["SecretString"].as_str().map(|s| s.to_string());
352        let secret_binary = body["SecretBinary"].as_str().and_then(base64_decode);
353
354        // Validate that either SecretString or SecretBinary is provided
355        if secret_string.is_none() && secret_binary.is_none() {
356            return Err(AwsServiceError::aws_error(
357                StatusCode::BAD_REQUEST,
358                "InvalidRequestException",
359                "You must provide either SecretString or SecretBinary.",
360            ));
361        }
362
363        let mut accounts = self.state.write();
364        let state = accounts.get_or_create(&req.account_id);
365        let secret = match self.find_secret_mut(state, &secret_id) {
366            Ok(s) => s,
367            Err(_) => {
368                return Err(AwsServiceError::aws_error(
369                    StatusCode::NOT_FOUND,
370                    "ResourceNotFoundException",
371                    "Secrets Manager can't find the specified secret.",
372                ));
373            }
374        };
375
376        if secret.deleted {
377            return Err(AwsServiceError::aws_error(
378                StatusCode::BAD_REQUEST,
379                "InvalidRequestException",
380                "You can't perform this operation on the secret because it was marked for deletion.",
381            ));
382        }
383
384        let now = Utc::now();
385        let version_id = body["ClientRequestToken"]
386            .as_str()
387            .map(|s| s.to_string())
388            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
389
390        match check_secret_version_idempotency(
391            &secret.versions,
392            &version_id,
393            &secret_string,
394            &secret_binary,
395        ) {
396            VersionIdempotency::Match => {
397                let existing_stages = secret.versions[&version_id].stages.clone();
398                return Ok(AwsResponse::ok_json(json!({
399                    "ARN": secret.arn,
400                    "Name": secret.name,
401                    "VersionId": version_id,
402                    "VersionStages": existing_stages,
403                })));
404            }
405            VersionIdempotency::Conflict => {
406                return Err(AwsServiceError::aws_error(
407                    StatusCode::BAD_REQUEST,
408                    "ResourceExistsException",
409                    format!(
410                        "You can't use ClientRequestToken {version_id} because that value is already in use for a version of secret {}.",
411                        secret.arn
412                    ),
413                ));
414            }
415            VersionIdempotency::NotFound => {}
416        }
417
418        let mut version_stages: Vec<String> = body["VersionStages"]
419            .as_array()
420            .map(|arr| {
421                arr.iter()
422                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
423                    .collect()
424            })
425            .unwrap_or_else(|| vec!["AWSCURRENT".to_string()]);
426
427        // If this is the first version with a value, add AWSCURRENT to stages
428        let has_current = secret
429            .versions
430            .values()
431            .any(|v| v.stages.contains(&"AWSCURRENT".to_string()));
432        if !has_current && !version_stages.contains(&"AWSCURRENT".to_string()) {
433            version_stages.push("AWSCURRENT".to_string());
434        }
435
436        // Move AWSCURRENT from old version to AWSPREVIOUS if new version has AWSCURRENT
437        if version_stages.contains(&"AWSCURRENT".to_string()) {
438            if let Some(ref old_vid) = secret.current_version_id.clone() {
439                if let Some(old_version) = secret.versions.get_mut(old_vid) {
440                    old_version.stages.retain(|s| s != "AWSCURRENT");
441                    if !old_version.stages.contains(&"AWSPREVIOUS".to_string()) {
442                        old_version.stages.push("AWSPREVIOUS".to_string());
443                    }
444                }
445                // Remove AWSPREVIOUS from any other version
446                for (id, v) in secret.versions.iter_mut() {
447                    if id != old_vid {
448                        v.stages.retain(|s| s != "AWSPREVIOUS");
449                    }
450                }
451            }
452            secret.current_version_id = Some(version_id.clone());
453        }
454
455        // Remove custom stages from other versions that have them
456        for stage in &version_stages {
457            if stage == "AWSCURRENT" || stage == "AWSPREVIOUS" {
458                continue;
459            }
460            for v in secret.versions.values_mut() {
461                v.stages.retain(|s| s != stage);
462            }
463        }
464
465        // Remove versions with no stages
466        secret.versions.retain(|_, v| !v.stages.is_empty());
467
468        let version = SecretVersion {
469            version_id: version_id.clone(),
470            secret_string,
471            secret_binary,
472            stages: version_stages.clone(),
473            created_at: now,
474        };
475
476        secret.versions.insert(version_id.clone(), version);
477        secret.last_changed_at = now;
478
479        let response = json!({
480            "ARN": secret.arn,
481            "Name": secret.name,
482            "VersionId": version_id,
483            "VersionStages": version_stages,
484        });
485
486        Ok(AwsResponse::ok_json(response))
487    }
488
489    fn update_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
490        let body = req.json_body();
491        let secret_id = require_secret_id(&body)?;
492        validate_optional_string_length(
493            "clientRequestToken",
494            body["ClientRequestToken"].as_str(),
495            32,
496            64,
497        )?;
498        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
499        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
500        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
501
502        let mut accounts = self.state.write();
503        let state = accounts.get_or_create(&req.account_id);
504        let secret = match self.find_secret_mut(state, &secret_id) {
505            Ok(s) => s,
506            Err(_) => {
507                return Err(AwsServiceError::aws_error(
508                    StatusCode::NOT_FOUND,
509                    "ResourceNotFoundException",
510                    "Secrets Manager can't find the specified secret.",
511                ));
512            }
513        };
514
515        if secret.deleted {
516            return Err(AwsServiceError::aws_error(
517                StatusCode::BAD_REQUEST,
518                "InvalidRequestException",
519                "You can't perform this operation on the secret because it was marked for deletion.",
520            ));
521        }
522
523        if let Some(desc) = body["Description"].as_str() {
524            secret.description = Some(desc.to_string());
525        }
526        if let Some(kms) = body["KmsKeyId"].as_str() {
527            secret.kms_key_id = Some(kms.to_string());
528        }
529
530        // If SecretString or SecretBinary is provided, create a new version
531        let secret_string = body["SecretString"].as_str().map(|s| s.to_string());
532        let secret_binary = body["SecretBinary"].as_str().and_then(base64_decode);
533
534        let version_id = if secret_string.is_some() || secret_binary.is_some() {
535            let vid = body["ClientRequestToken"]
536                .as_str()
537                .map(|s| s.to_string())
538                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
539
540            match check_secret_version_idempotency(
541                &secret.versions,
542                &vid,
543                &secret_string,
544                &secret_binary,
545            ) {
546                VersionIdempotency::Match => {
547                    return Ok(AwsResponse::ok_json(json!({
548                        "ARN": secret.arn,
549                        "Name": secret.name,
550                        "VersionId": vid,
551                    })));
552                }
553                VersionIdempotency::Conflict => {
554                    return Err(AwsServiceError::aws_error(
555                        StatusCode::BAD_REQUEST,
556                        "ResourceExistsException",
557                        format!(
558                            "You can't use ClientRequestToken {vid} because that value is already in use for a version of secret {}.",
559                            secret.arn
560                        ),
561                    ));
562                }
563                VersionIdempotency::NotFound => {}
564            }
565
566            let now = Utc::now();
567
568            // Move AWSCURRENT -> AWSPREVIOUS on old version
569            if let Some(ref old_vid) = secret.current_version_id.clone() {
570                if let Some(old_v) = secret.versions.get_mut(old_vid) {
571                    old_v.stages.retain(|s| s != "AWSCURRENT");
572                    if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
573                        old_v.stages.push("AWSPREVIOUS".to_string());
574                    }
575                }
576            }
577
578            let version = SecretVersion {
579                version_id: vid.clone(),
580                secret_string,
581                secret_binary,
582                stages: vec!["AWSCURRENT".to_string()],
583                created_at: now,
584            };
585            secret.versions.insert(vid.clone(), version);
586            secret.current_version_id = Some(vid.clone());
587            secret.last_changed_at = now;
588            Some(vid)
589        } else {
590            secret.last_changed_at = Utc::now();
591            None
592        };
593
594        let mut response = json!({
595            "ARN": secret.arn,
596            "Name": secret.name,
597        });
598        if let Some(vid) = version_id {
599            response["VersionId"] = json!(vid);
600        }
601
602        Ok(AwsResponse::ok_json(response))
603    }
604
605    fn delete_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
606        let body = req.json_body();
607        let secret_id = require_secret_id(&body)?;
608
609        let force_delete = body["ForceDeleteWithoutRecovery"]
610            .as_bool()
611            .unwrap_or(false);
612        let recovery_window = body.get("RecoveryWindowInDays").and_then(|v| v.as_i64());
613
614        // Validate recovery window range first (AWS validates this before the conflict check)
615        if let Some(days) = recovery_window {
616            if !(7..=30).contains(&days) {
617                return Err(AwsServiceError::aws_error(
618                    StatusCode::BAD_REQUEST,
619                    "InvalidParameterException",
620                    "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: RecoveryWindowInDays value must be between 7 and 30 days (inclusive).",
621                ));
622            }
623        }
624
625        // Validate: can't use both force delete and recovery window
626        if force_delete && recovery_window.is_some() {
627            return Err(AwsServiceError::aws_error(
628                StatusCode::BAD_REQUEST,
629                "InvalidParameterException",
630                "An error occurred (InvalidParameterException) when calling the DeleteSecret operation: You can't use ForceDeleteWithoutRecovery in conjunction with RecoveryWindowInDays.",
631            ));
632        }
633
634        let mut accounts = self.state.write();
635        let state = accounts.get_or_create(&req.account_id);
636
637        if force_delete {
638            // Force delete: if secret doesn't exist, create a fake response
639            match self.find_secret_mut(state, &secret_id) {
640                Ok(secret) => {
641                    let arn = secret.arn.clone();
642                    let name = secret.name.clone();
643                    let deletion_date = Utc::now();
644                    state.secrets.remove(&name);
645                    let response = json!({
646                        "ARN": arn,
647                        "Name": name,
648                        "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
649                    });
650                    return Ok(AwsResponse::ok_json(response));
651                }
652                Err(_) => {
653                    // For force delete of non-existent secret, AWS returns success
654                    let arn = format!(
655                        "arn:aws:secretsmanager:{}:{}:secret:{}-{}",
656                        req.region,
657                        req.account_id,
658                        secret_id,
659                        &uuid::Uuid::new_v4().to_string()[..6]
660                    );
661                    let deletion_date = Utc::now();
662                    let response = json!({
663                        "ARN": arn,
664                        "Name": secret_id,
665                        "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
666                    });
667                    return Ok(AwsResponse::ok_json(response));
668                }
669            }
670        }
671
672        let secret = self.find_secret_mut(state, &secret_id)?;
673
674        if secret.deleted {
675            return Err(AwsServiceError::aws_error(
676                StatusCode::BAD_REQUEST,
677                "InvalidRequestException",
678                "You can't perform this operation on the secret because it was already scheduled for deletion.",
679            ));
680        }
681
682        let now = Utc::now();
683        let days = recovery_window.unwrap_or(30);
684        let deletion_date = now + chrono::Duration::days(days);
685        secret.deleted = true;
686        secret.deletion_date = Some(deletion_date);
687
688        let response = json!({
689            "ARN": secret.arn,
690            "Name": secret.name,
691            "DeletionDate": deletion_date.timestamp_millis() as f64 / 1000.0,
692        });
693
694        Ok(AwsResponse::ok_json(response))
695    }
696
697    fn restore_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
698        let body = req.json_body();
699        let secret_id = require_secret_id(&body)?;
700
701        let mut accounts = self.state.write();
702        let state = accounts.get_or_create(&req.account_id);
703        let secret = self.find_secret_mut(state, &secret_id)?;
704
705        // AWS allows restoring a secret that is not deleted (no-op)
706        secret.deleted = false;
707        secret.deletion_date = None;
708
709        let response = json!({
710            "ARN": secret.arn,
711            "Name": secret.name,
712        });
713
714        Ok(AwsResponse::ok_json(response))
715    }
716
717    fn describe_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
718        let body = req.json_body();
719        let secret_id = require_secret_id(&body)?;
720
721        let accounts = self.state.read();
722        let empty = SecretsManagerState::new(&req.account_id, &req.region);
723        let state = accounts.get(&req.account_id).unwrap_or(&empty);
724        let secret = self.find_secret_ref(state, &secret_id)?;
725
726        let mut response = json!({
727            "ARN": secret.arn,
728            "Name": secret.name,
729            "CreatedDate": secret.created_at.timestamp_millis() as f64 / 1000.0,
730            "LastChangedDate": secret.last_changed_at.timestamp_millis() as f64 / 1000.0,
731        });
732
733        if !secret.versions.is_empty() {
734            let mut version_ids_to_stages: serde_json::Map<String, Value> = serde_json::Map::new();
735            for (vid, version) in &secret.versions {
736                version_ids_to_stages.insert(vid.clone(), json!(version.stages));
737            }
738            response["VersionIdsToStages"] = Value::Object(version_ids_to_stages);
739        }
740
741        if let Some(ref desc) = secret.description {
742            if !desc.is_empty() {
743                response["Description"] = json!(desc);
744            }
745        }
746
747        if secret.tags_ever_set || !secret.tags.is_empty() {
748            response["Tags"] = json!(tags_to_json(&secret.tags));
749        }
750
751        if let Some(ref kms) = secret.kms_key_id {
752            response["KmsKeyId"] = json!(kms);
753        }
754        if secret.deleted {
755            response["DeletedDate"] = json!(secret
756                .deletion_date
757                .map(|d| d.timestamp_millis() as f64 / 1000.0));
758        }
759        if let Some(rotation_enabled) = secret.rotation_enabled {
760            response["RotationEnabled"] = json!(rotation_enabled);
761        }
762        if let Some(ref lambda_arn) = secret.rotation_lambda_arn {
763            response["RotationLambdaARN"] = json!(lambda_arn);
764        }
765        if let Some(ref rules) = secret.rotation_rules {
766            let mut rules_json = json!({});
767            if let Some(days) = rules.automatically_after_days {
768                rules_json["AutomaticallyAfterDays"] = json!(days);
769            }
770            response["RotationRules"] = rules_json;
771        }
772        if let Some(last_rotated) = secret.last_rotated_at {
773            response["LastRotatedDate"] = json!(last_rotated.timestamp_millis() as f64 / 1000.0);
774        }
775        // Calculate NextRotationDate if rotation is enabled
776        if secret.rotation_enabled == Some(true) {
777            if let Some(ref rules) = secret.rotation_rules {
778                if let Some(days) = rules.automatically_after_days {
779                    let base = secret.last_rotated_at.unwrap_or(secret.created_at);
780                    let next = base + chrono::Duration::days(days);
781                    response["NextRotationDate"] = json!(next.timestamp_millis() as f64 / 1000.0);
782                }
783            }
784        }
785
786        Ok(AwsResponse::ok_json(response))
787    }
788
789    fn list_secrets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
790        let body = req.json_body();
791        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
792        validate_optional_range_i64("maxResults", body["MaxResults"].as_i64(), 1, 100)?;
793        validate_optional_enum("sortBy", body["SortBy"].as_str(), &["name", "created-date"])?;
794        validate_optional_enum("sortOrder", body["SortOrder"].as_str(), &["asc", "desc"])?;
795        let max_results = body["MaxResults"].as_i64().unwrap_or(100) as usize;
796        let next_token = body["NextToken"].as_str();
797        let filters = body["Filters"].as_array();
798        let include_deleted = body["IncludePlannedDeletion"].as_bool().unwrap_or(false);
799
800        // Validate filters
801        if let Some(filters) = filters {
802            for filter in filters {
803                let key = filter["Key"].as_str().unwrap_or("");
804                let values = filter["Values"].as_array();
805
806                if key.is_empty() {
807                    return Err(AwsServiceError::aws_error(
808                        StatusCode::BAD_REQUEST,
809                        "InvalidParameterException",
810                        "Invalid filter key",
811                    ));
812                }
813
814                let valid_keys = [
815                    "all",
816                    "name",
817                    "tag-key",
818                    "description",
819                    "tag-value",
820                    "owning-service",
821                    "primary-region",
822                ];
823                if !valid_keys.contains(&key) {
824                    return Err(AwsServiceError::aws_error(
825                        StatusCode::BAD_REQUEST,
826                        "ValidationException",
827                        format!(
828                            "1 validation error detected: Value '{}' at 'filters.1.member.key' failed to satisfy constraint: Member must satisfy enum value set: [all, name, tag-key, description, tag-value]",
829                            key
830                        ),
831                    ));
832                }
833
834                if values.is_none() || values.unwrap().is_empty() {
835                    return Err(AwsServiceError::aws_error(
836                        StatusCode::BAD_REQUEST,
837                        "InvalidParameterException",
838                        format!("Invalid filter values for key: {key}"),
839                    ));
840                }
841            }
842        }
843
844        let accounts = self.state.read();
845        let empty = SecretsManagerState::new(&req.account_id, &req.region);
846        let state = accounts.get(&req.account_id).unwrap_or(&empty);
847
848        let mut secrets: Vec<&Secret> = state
849            .secrets
850            .values()
851            .filter(|s| {
852                // Exclude deleted unless IncludePlannedDeletion
853                if s.deleted && !include_deleted {
854                    return false;
855                }
856
857                if let Some(filters) = filters {
858                    for filter in filters {
859                        let key = filter["Key"].as_str().unwrap_or("");
860                        let values: Vec<&str> = filter["Values"]
861                            .as_array()
862                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
863                            .unwrap_or_default();
864
865                        let matches = match key {
866                            "name" => filter_name(s, &values),
867                            "description" => filter_description(s, &values),
868                            "tag-key" => filter_tag_key(s, &values),
869                            "tag-value" => filter_tag_value(s, &values),
870                            "all" => filter_all(s, &values),
871                            "owning-service" => false,
872                            "primary-region" => false,
873                            _ => true,
874                        };
875
876                        if !matches {
877                            return false;
878                        }
879                    }
880                }
881                true
882            })
883            .collect();
884        secrets.sort_by_key(|a| a.created_at);
885
886        // Simple pagination with name-based token
887        let start_idx = if let Some(token) = next_token {
888            secrets.iter().position(|s| s.name == token).unwrap_or(0)
889        } else {
890            0
891        };
892
893        let page: Vec<Value> = secrets
894            .iter()
895            .skip(start_idx)
896            .take(max_results)
897            .map(|s| {
898                let mut entry = json!({
899                    "ARN": s.arn,
900                    "Name": s.name,
901                    "CreatedDate": s.created_at.timestamp_millis() as f64 / 1000.0,
902                    "LastChangedDate": s.last_changed_at.timestamp_millis() as f64 / 1000.0,
903                });
904
905                if !s.versions.is_empty() {
906                    let mut version_ids_to_stages: serde_json::Map<String, Value> =
907                        serde_json::Map::new();
908                    for (vid, version) in &s.versions {
909                        version_ids_to_stages.insert(vid.clone(), json!(version.stages));
910                    }
911                    entry["SecretVersionsToStages"] = Value::Object(version_ids_to_stages);
912                }
913
914                if let Some(ref desc) = s.description {
915                    if !desc.is_empty() {
916                        entry["Description"] = json!(desc);
917                    }
918                }
919
920                if s.tags_ever_set || !s.tags.is_empty() {
921                    entry["Tags"] = json!(tags_to_json(&s.tags));
922                }
923
924                if let Some(ref kms) = s.kms_key_id {
925                    entry["KmsKeyId"] = json!(kms);
926                }
927                if s.deleted {
928                    entry["DeletedDate"] = json!(s
929                        .deletion_date
930                        .map(|d| d.timestamp_millis() as f64 / 1000.0));
931                }
932                entry
933            })
934            .collect();
935
936        let has_more = start_idx + max_results < secrets.len();
937        let mut response = json!({
938            "SecretList": page,
939        });
940        if has_more {
941            if let Some(next) = secrets.get(start_idx + max_results) {
942                response["NextToken"] = json!(next.name);
943            }
944        }
945
946        Ok(AwsResponse::ok_json(response))
947    }
948
949    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
950        let body = req.json_body();
951        let secret_id = require_secret_id(&body)?;
952
953        let new_tags = parse_tags(&body["Tags"]);
954
955        let mut accounts = self.state.write();
956        let state = accounts.get_or_create(&req.account_id);
957        let secret = self.find_secret_mut(state, &secret_id)?;
958
959        if !new_tags.is_empty() {
960            secret.tags_ever_set = true;
961        }
962        for (k, v) in new_tags {
963            // Update existing tag or add new one
964            if let Some(existing) = secret.tags.iter_mut().find(|(ek, _)| *ek == k) {
965                existing.1 = v;
966            } else {
967                secret.tags.push((k, v));
968            }
969        }
970
971        Ok(AwsResponse::json(StatusCode::OK, "{}"))
972    }
973
974    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
975        let body = req.json_body();
976        let secret_id = require_secret_id(&body)?;
977
978        let tag_keys: Vec<String> = body["TagKeys"]
979            .as_array()
980            .map(|arr| {
981                arr.iter()
982                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
983                    .collect()
984            })
985            .unwrap_or_default();
986
987        let mut accounts = self.state.write();
988        let state = accounts.get_or_create(&req.account_id);
989        let secret = self.find_secret_mut(state, &secret_id)?;
990
991        secret.tags.retain(|(k, _)| !tag_keys.contains(k));
992
993        Ok(AwsResponse::json(StatusCode::OK, "{}"))
994    }
995
996    fn list_secret_version_ids(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
997        let body = req.json_body();
998        let secret_id = require_secret_id(&body)?;
999        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1000
1001        let accounts = self.state.read();
1002        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1003        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1004        let secret = self.find_secret_ref(state, &secret_id)?;
1005
1006        let versions: Vec<Value> = secret
1007            .versions
1008            .values()
1009            .map(|v| {
1010                json!({
1011                    "VersionId": v.version_id,
1012                    "VersionStages": v.stages,
1013                    "CreatedDate": v.created_at.timestamp_millis() as f64 / 1000.0,
1014                })
1015            })
1016            .collect();
1017
1018        let response = json!({
1019            "ARN": secret.arn,
1020            "Name": secret.name,
1021            "Versions": versions,
1022        });
1023
1024        Ok(AwsResponse::ok_json(response))
1025    }
1026
1027    fn get_random_password(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1028        let body = req.json_body();
1029        let length = body["PasswordLength"].as_i64().unwrap_or(32) as usize;
1030
1031        if length < 4 {
1032            return Err(AwsServiceError::aws_error(
1033                StatusCode::BAD_REQUEST,
1034                "InvalidParameterException",
1035                "InvalidParameterException",
1036            ));
1037        }
1038        if length > 4096 {
1039            return Err(AwsServiceError::aws_error(
1040                StatusCode::BAD_REQUEST,
1041                "InvalidParameterValue",
1042                "InvalidParameterValue",
1043            ));
1044        }
1045
1046        let exclude_lowercase = body["ExcludeLowercase"].as_bool().unwrap_or(false);
1047        let exclude_uppercase = body["ExcludeUppercase"].as_bool().unwrap_or(false);
1048        let exclude_numbers = body["ExcludeNumbers"].as_bool().unwrap_or(false);
1049        let exclude_punctuation = body["ExcludePunctuation"].as_bool().unwrap_or(false);
1050        let include_space = body["IncludeSpace"].as_bool().unwrap_or(false);
1051        let require_each = body["RequireEachIncludedType"].as_bool().unwrap_or(true);
1052        validate_optional_string_length(
1053            "excludeCharacters",
1054            body["ExcludeCharacters"].as_str(),
1055            0,
1056            4096,
1057        )?;
1058        let exclude_chars = body["ExcludeCharacters"].as_str().unwrap_or("").to_string();
1059
1060        let lowercase = "abcdefghijklmnopqrstuvwxyz";
1061        let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1062        let digits = "0123456789";
1063        let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
1064
1065        let mut char_pool = String::new();
1066        let mut required_chars: Vec<String> = Vec::new();
1067
1068        if !exclude_lowercase {
1069            let filtered: String = lowercase
1070                .chars()
1071                .filter(|c| !exclude_chars.contains(*c))
1072                .collect();
1073            if !filtered.is_empty() {
1074                required_chars.push(filtered.clone());
1075                char_pool.push_str(&filtered);
1076            }
1077        }
1078        if !exclude_uppercase {
1079            let filtered: String = uppercase
1080                .chars()
1081                .filter(|c| !exclude_chars.contains(*c))
1082                .collect();
1083            if !filtered.is_empty() {
1084                required_chars.push(filtered.clone());
1085                char_pool.push_str(&filtered);
1086            }
1087        }
1088        if !exclude_numbers {
1089            let filtered: String = digits
1090                .chars()
1091                .filter(|c| !exclude_chars.contains(*c))
1092                .collect();
1093            if !filtered.is_empty() {
1094                required_chars.push(filtered.clone());
1095                char_pool.push_str(&filtered);
1096            }
1097        }
1098        if !exclude_punctuation {
1099            let filtered: String = punctuation
1100                .chars()
1101                .filter(|c| !exclude_chars.contains(*c))
1102                .collect();
1103            if !filtered.is_empty() {
1104                required_chars.push(filtered.clone());
1105                char_pool.push_str(&filtered);
1106            }
1107        }
1108        if include_space && !exclude_chars.contains(' ') {
1109            char_pool.push(' ');
1110        }
1111
1112        if char_pool.is_empty() {
1113            return Err(AwsServiceError::aws_error(
1114                StatusCode::BAD_REQUEST,
1115                "InvalidParameterException",
1116                "InvalidParameterException",
1117            ));
1118        }
1119
1120        let pool_bytes: Vec<char> = char_pool.chars().collect();
1121        let mut password = String::with_capacity(length);
1122
1123        // Use simple random generation
1124        if require_each {
1125            // First, ensure at least one character from each required category
1126            for category in &required_chars {
1127                let chars: Vec<char> = category.chars().collect();
1128                let idx = simple_random() % chars.len();
1129                password.push(chars[idx]);
1130            }
1131            if include_space && !exclude_chars.contains(' ') {
1132                password.push(' ');
1133            }
1134        }
1135
1136        // Fill the rest randomly
1137        while password.len() < length {
1138            let idx = simple_random() % pool_bytes.len();
1139            password.push(pool_bytes[idx]);
1140        }
1141
1142        // Shuffle the password (Fisher-Yates)
1143        let mut chars: Vec<char> = password.chars().collect();
1144        for i in (1..chars.len()).rev() {
1145            let j = simple_random() % (i + 1);
1146            chars.swap(i, j);
1147        }
1148        let password: String = chars.into_iter().take(length).collect();
1149
1150        let response = json!({
1151            "RandomPassword": password,
1152        });
1153
1154        Ok(AwsResponse::ok_json(response))
1155    }
1156
1157    fn rotate_secret(
1158        &self,
1159        req: &AwsRequest,
1160    ) -> Result<(AwsResponse, Option<RotationInvocation>), AwsServiceError> {
1161        let body = req.json_body();
1162        let secret_id = require_secret_id(&body)?;
1163
1164        // Validate ClientRequestToken
1165        if let Some(token) = body["ClientRequestToken"].as_str() {
1166            if token.len() < 32 || token.len() > 64 {
1167                return Err(AwsServiceError::aws_error(
1168                    StatusCode::BAD_REQUEST,
1169                    "InvalidParameterException",
1170                    "ClientRequestToken must be 32-64 characters long.",
1171                ));
1172            }
1173        }
1174
1175        // Validate RotationLambdaARN
1176        if let Some(arn) = body["RotationLambdaARN"].as_str() {
1177            if arn.len() > 2048 {
1178                return Err(AwsServiceError::aws_error(
1179                    StatusCode::BAD_REQUEST,
1180                    "InvalidParameterException",
1181                    "RotationLambdaARN length must be less than or equal to 2048.",
1182                ));
1183            }
1184        }
1185
1186        // Validate RotationRules
1187        if let Some(rules) = body["RotationRules"].as_object() {
1188            if let Some(days) = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64()) {
1189                if !(1..=1000).contains(&days) {
1190                    return Err(AwsServiceError::aws_error(
1191                        StatusCode::BAD_REQUEST,
1192                        "InvalidParameterException",
1193                        "RotationRules.AutomaticallyAfterDays must be within 1-1000.",
1194                    ));
1195                }
1196            }
1197        }
1198
1199        let mut accounts = self.state.write();
1200        let state = accounts.get_or_create(&req.account_id);
1201        let secret = self.find_secret_mut(state, &secret_id)?;
1202
1203        if secret.deleted {
1204            return Err(AwsServiceError::aws_error(
1205                StatusCode::BAD_REQUEST,
1206                "InvalidRequestException",
1207                "You can't perform this operation on the secret because it was marked for deletion.",
1208            ));
1209        }
1210
1211        // Set rotation config
1212        if let Some(lambda_arn) = body["RotationLambdaARN"].as_str() {
1213            secret.rotation_lambda_arn = Some(lambda_arn.to_string());
1214        }
1215
1216        if let Some(rules) = body["RotationRules"].as_object() {
1217            let days = rules.get("AutomaticallyAfterDays").and_then(|v| v.as_i64());
1218            secret.rotation_rules = Some(RotationRules {
1219                automatically_after_days: days,
1220            });
1221        }
1222
1223        secret.rotation_enabled = Some(true);
1224        let now = Utc::now();
1225        secret.last_rotated_at = Some(now);
1226        secret.last_changed_at = now;
1227
1228        let version_id = body["ClientRequestToken"]
1229            .as_str()
1230            .map(|s| s.to_string())
1231            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1232
1233        let has_lambda =
1234            body["RotationLambdaARN"].as_str().is_some() || secret.rotation_lambda_arn.is_some();
1235        let lambda_arn = secret.rotation_lambda_arn.clone();
1236
1237        // If the secret has a value, perform rotation
1238        let mut invocation = None;
1239        if let Some(current_vid) = secret.current_version_id.clone() {
1240            let current_value = secret.versions.get(&current_vid).cloned();
1241
1242            if let Some(cv) = current_value {
1243                if has_lambda {
1244                    // With Lambda: do NOT pre-create the AWSPENDING version. The
1245                    // rotation Lambda is responsible for putting the new value via
1246                    // PutSecretValue with VersionStages=[AWSPENDING] during the
1247                    // createSecret step (matching real AWS Secrets Manager behavior).
1248
1249                    // Schedule Lambda invocation
1250                    if let Some(ref arn) = lambda_arn {
1251                        invocation = Some(RotationInvocation {
1252                            lambda_arn: arn.clone(),
1253                            secret_id: secret.arn.clone(),
1254                            client_request_token: version_id.clone(),
1255                        });
1256                    }
1257                } else {
1258                    // Without Lambda: simple rotation - new version becomes AWSCURRENT
1259                    // Move old version to AWSPREVIOUS
1260                    if let Some(old_v) = secret.versions.get_mut(&current_vid) {
1261                        old_v.stages.retain(|s| s != "AWSCURRENT");
1262                        if !old_v.stages.contains(&"AWSPREVIOUS".to_string()) {
1263                            old_v.stages.push("AWSPREVIOUS".to_string());
1264                        }
1265                    }
1266                    let version = SecretVersion {
1267                        version_id: version_id.clone(),
1268                        secret_string: cv.secret_string.clone(),
1269                        secret_binary: cv.secret_binary.clone(),
1270                        stages: vec!["AWSCURRENT".to_string()],
1271                        created_at: now,
1272                    };
1273                    secret.versions.insert(version_id.clone(), version);
1274                    secret.current_version_id = Some(version_id.clone());
1275                }
1276            }
1277        }
1278
1279        let response = json!({
1280            "ARN": secret.arn,
1281            "Name": secret.name,
1282            "VersionId": version_id,
1283        });
1284
1285        Ok((AwsResponse::ok_json(response), invocation))
1286    }
1287
1288    fn cancel_rotate_secret(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1289        let body = req.json_body();
1290        let secret_id = require_secret_id(&body)?;
1291
1292        let mut accounts = self.state.write();
1293        let state = accounts.get_or_create(&req.account_id);
1294        let secret = self.find_secret_mut(state, &secret_id)?;
1295
1296        if secret.deleted {
1297            return Err(AwsServiceError::aws_error(
1298                StatusCode::BAD_REQUEST,
1299                "InvalidRequestException",
1300                "You can't perform this operation on the secret because it was marked for deletion.",
1301            ));
1302        }
1303
1304        if secret.rotation_enabled != Some(true) {
1305            return Err(AwsServiceError::aws_error(
1306                StatusCode::BAD_REQUEST,
1307                "InvalidRequestException",
1308                "You can't cancel rotation for a secret that does not have rotation enabled.",
1309            ));
1310        }
1311
1312        secret.rotation_enabled = Some(false);
1313
1314        let response = json!({
1315            "ARN": secret.arn,
1316            "Name": secret.name,
1317        });
1318
1319        Ok(AwsResponse::ok_json(response))
1320    }
1321
1322    fn update_secret_version_stage(
1323        &self,
1324        req: &AwsRequest,
1325    ) -> Result<AwsResponse, AwsServiceError> {
1326        let body = req.json_body();
1327        let secret_id = require_secret_id(&body)?;
1328        let version_stage = body["VersionStage"]
1329            .as_str()
1330            .ok_or_else(|| {
1331                AwsServiceError::aws_error(
1332                    StatusCode::BAD_REQUEST,
1333                    "InvalidParameterException",
1334                    "VersionStage is required",
1335                )
1336            })?
1337            .to_string();
1338        validate_string_length("versionStage", &version_stage, 1, 256)?;
1339        validate_optional_string_length(
1340            "removeFromVersionId",
1341            body["RemoveFromVersionId"].as_str(),
1342            32,
1343            64,
1344        )?;
1345        validate_optional_string_length(
1346            "moveToVersionId",
1347            body["MoveToVersionId"].as_str(),
1348            32,
1349            64,
1350        )?;
1351
1352        let move_to = body["MoveToVersionId"].as_str().map(|s| s.to_string());
1353        let remove_from = body["RemoveFromVersionId"].as_str().map(|s| s.to_string());
1354
1355        let mut accounts = self.state.write();
1356        let state = accounts.get_or_create(&req.account_id);
1357        let secret = self.find_secret_mut(state, &secret_id)?;
1358
1359        // Validate: if moving AWSCURRENT, must specify RemoveFromVersionId
1360        if version_stage == "AWSCURRENT" && move_to.is_some() && remove_from.is_none() {
1361            // Find the version that currently has AWSCURRENT
1362            let current_holder = secret
1363                .versions
1364                .iter()
1365                .find(|(_, v)| v.stages.contains(&"AWSCURRENT".to_string()))
1366                .map(|(id, _)| id.clone());
1367
1368            if let Some(current_vid) = current_holder {
1369                return Err(AwsServiceError::aws_error(
1370                    StatusCode::BAD_REQUEST,
1371                    "InvalidParameterException",
1372                    format!(
1373                        "The parameter RemoveFromVersionId can't be empty. Staging label AWSCURRENT is currently attached to version {current_vid}, so you must explicitly reference that version in RemoveFromVersionId."
1374                    ),
1375                ));
1376            }
1377        }
1378
1379        // Remove stage from specified version
1380        if let Some(ref remove_vid) = remove_from {
1381            if let Some(version) = secret.versions.get_mut(remove_vid) {
1382                version.stages.retain(|s| s != &version_stage);
1383                // If moving AWSCURRENT away, add AWSPREVIOUS and remove from others
1384                if version_stage == "AWSCURRENT" {
1385                    // Remove AWSPREVIOUS from all other versions first
1386                    for (id, v) in secret.versions.iter_mut() {
1387                        if id != remove_vid {
1388                            v.stages.retain(|s| s != "AWSPREVIOUS");
1389                        }
1390                    }
1391                    // Now add AWSPREVIOUS to the version losing AWSCURRENT
1392                    if let Some(v) = secret.versions.get_mut(remove_vid) {
1393                        if !v.stages.contains(&"AWSPREVIOUS".to_string()) {
1394                            v.stages.push("AWSPREVIOUS".to_string());
1395                        }
1396                    }
1397                }
1398            }
1399        }
1400
1401        // Add stage to specified version
1402        if let Some(ref move_vid) = move_to {
1403            if let Some(version) = secret.versions.get_mut(move_vid) {
1404                if !version.stages.contains(&version_stage) {
1405                    version.stages.push(version_stage.clone());
1406                }
1407            }
1408            // Update current_version_id if we moved AWSCURRENT
1409            if version_stage == "AWSCURRENT" {
1410                secret.current_version_id = Some(move_vid.clone());
1411            }
1412        }
1413
1414        let response = json!({
1415            "ARN": secret.arn,
1416            "Name": secret.name,
1417        });
1418
1419        Ok(AwsResponse::ok_json(response))
1420    }
1421
1422    fn batch_get_secret_value(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1423        let body = req.json_body();
1424        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 4096)?;
1425        let secret_id_list = body["SecretIdList"].as_array();
1426        let filters = body["Filters"].as_array();
1427        let max_results = body.get("MaxResults").and_then(|v| v.as_i64());
1428
1429        // Validate: can't use both SecretIdList and Filters
1430        if secret_id_list.is_some() && filters.is_some() {
1431            return Err(AwsServiceError::aws_error(
1432                StatusCode::BAD_REQUEST,
1433                "InvalidParameterException",
1434                "Either 'SecretIdList' or 'Filters' must be provided, but not both.",
1435            ));
1436        }
1437
1438        // Validate: MaxResults requires Filters
1439        if max_results.is_some() && filters.is_none() {
1440            return Err(AwsServiceError::aws_error(
1441                StatusCode::BAD_REQUEST,
1442                "InvalidParameterException",
1443                "'Filters' not specified. 'Filters' must also be specified when 'MaxResults' is provided.",
1444            ));
1445        }
1446
1447        let accounts = self.state.read();
1448        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1449        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1450        let mut secret_values: Vec<Value> = Vec::new();
1451        let mut errors: Vec<Value> = Vec::new();
1452
1453        if let Some(id_list) = secret_id_list {
1454            for id_val in id_list {
1455                let sid = id_val.as_str().unwrap_or("");
1456                match self.find_secret_ref(state, sid) {
1457                    Ok(secret) => {
1458                        if secret.deleted {
1459                            errors.push(json!({
1460                                "SecretId": sid,
1461                                "ErrorCode": "InvalidRequestException",
1462                                "Message": "Secret is currently marked deleted. Secret can be recovered with RestoreSecret. Secret is currently marked deleted.",
1463                            }));
1464                        } else if let Some(ref current_vid) = secret.current_version_id {
1465                            if let Some(version) = secret.versions.get(current_vid) {
1466                                let mut entry = json!({
1467                                    "ARN": secret.arn,
1468                                    "Name": secret.name,
1469                                    "VersionId": version.version_id,
1470                                    "VersionStages": version.stages,
1471                                    "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1472                                });
1473                                if let Some(ref s) = version.secret_string {
1474                                    entry["SecretString"] = json!(s);
1475                                }
1476                                if let Some(ref b) = version.secret_binary {
1477                                    entry["SecretBinary"] = json!(base64_encode(b));
1478                                }
1479                                secret_values.push(entry);
1480                            } else {
1481                                errors.push(json!({
1482                                    "SecretId": sid,
1483                                    "ErrorCode": "ResourceNotFoundException",
1484                                    "Message": "Secrets Manager can't find the specified secret.",
1485                                }));
1486                            }
1487                        } else {
1488                            errors.push(json!({
1489                                "SecretId": sid,
1490                                "ErrorCode": "ResourceNotFoundException",
1491                                "Message": "Secrets Manager can't find the specified secret.",
1492                            }));
1493                        }
1494                    }
1495                    Err(_) => {
1496                        errors.push(json!({
1497                            "SecretId": sid,
1498                            "ErrorCode": "ResourceNotFoundException",
1499                            "Message": "Secrets Manager can't find the specified secret.",
1500                        }));
1501                    }
1502                }
1503            }
1504        } else if let Some(filters) = filters {
1505            // Get secrets matching filters
1506            let matching: Vec<&Secret> = state
1507                .secrets
1508                .values()
1509                .filter(|s| {
1510                    if s.deleted {
1511                        return false;
1512                    }
1513                    for filter in filters {
1514                        let key = filter["Key"].as_str().unwrap_or("");
1515                        let values: Vec<&str> = filter["Values"]
1516                            .as_array()
1517                            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
1518                            .unwrap_or_default();
1519                        let matches = match key {
1520                            "name" => filter_name(s, &values),
1521                            "description" => filter_description(s, &values),
1522                            "tag-key" => filter_tag_key(s, &values),
1523                            "tag-value" => filter_tag_value(s, &values),
1524                            "all" => filter_all(s, &values),
1525                            _ => true,
1526                        };
1527                        if !matches {
1528                            return false;
1529                        }
1530                    }
1531                    true
1532                })
1533                .collect();
1534
1535            let limit = max_results.unwrap_or(100) as usize;
1536            let mut no_value_found = false;
1537            let mut matching = matching;
1538            matching.sort_by(|a, b| a.name.cmp(&b.name));
1539
1540            for secret in matching.iter().take(limit) {
1541                if let Some(ref current_vid) = secret.current_version_id {
1542                    if let Some(version) = secret.versions.get(current_vid) {
1543                        let mut entry = json!({
1544                            "ARN": secret.arn,
1545                            "Name": secret.name,
1546                            "VersionId": version.version_id,
1547                            "VersionStages": version.stages,
1548                            "CreatedDate": version.created_at.timestamp_millis() as f64 / 1000.0,
1549                        });
1550                        if let Some(ref s) = version.secret_string {
1551                            entry["SecretString"] = json!(s);
1552                        }
1553                        if let Some(ref b) = version.secret_binary {
1554                            entry["SecretBinary"] = json!(base64_encode(b));
1555                        }
1556                        secret_values.push(entry);
1557                    } else {
1558                        no_value_found = true;
1559                    }
1560                } else {
1561                    no_value_found = true;
1562                }
1563            }
1564
1565            if no_value_found && secret_values.is_empty() {
1566                return Err(AwsServiceError::aws_error(
1567                    StatusCode::NOT_FOUND,
1568                    "ResourceNotFoundException",
1569                    "Secrets Manager can't find the specified secret.",
1570                ));
1571            }
1572        }
1573
1574        let mut response = json!({
1575            "SecretValues": secret_values,
1576            "Errors": errors,
1577        });
1578
1579        // Remove empty arrays
1580        if errors.is_empty() {
1581            response.as_object_mut().unwrap().remove("Errors");
1582        }
1583
1584        Ok(AwsResponse::ok_json(response))
1585    }
1586
1587    fn get_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1588        let body = req.json_body();
1589        let secret_id = require_secret_id(&body)?;
1590
1591        let accounts = self.state.read();
1592        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1593        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1594        let secret = self.find_secret_ref(state, &secret_id)?;
1595
1596        let mut response = json!({
1597            "ARN": secret.arn,
1598            "Name": secret.name,
1599        });
1600
1601        if let Some(ref policy) = secret.resource_policy {
1602            response["ResourcePolicy"] = json!(policy);
1603        }
1604
1605        Ok(AwsResponse::ok_json(response))
1606    }
1607
1608    fn validate_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1609        let body = req.json_body();
1610        validate_optional_string_length("secretId", body["SecretId"].as_str(), 1, 2048)?;
1611        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1612        let policy_str = body["ResourcePolicy"].as_str().ok_or_else(|| {
1613            AwsServiceError::aws_error(
1614                StatusCode::BAD_REQUEST,
1615                "InvalidParameterException",
1616                "ResourcePolicy must be a string",
1617            )
1618        })?;
1619        validate_string_length("resourcePolicy", policy_str, 1, 20480)?;
1620
1621        // If SecretId is provided, verify the secret exists
1622        if let Some(secret_id) = body["SecretId"].as_str() {
1623            let accounts = self.state.read();
1624            let empty = SecretsManagerState::new(&req.account_id, &req.region);
1625            let state = accounts.get(&req.account_id).unwrap_or(&empty);
1626            self.find_secret_key(state, secret_id)?;
1627        }
1628
1629        let response = json!({
1630            "PolicyValidationPassed": true,
1631            "ValidationErrors": [],
1632        });
1633        Ok(AwsResponse::ok_json(response))
1634    }
1635
1636    fn put_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1637        let body = req.json_body();
1638        let secret_id = require_secret_id(&body)?;
1639        validate_required("ResourcePolicy", &body["ResourcePolicy"])?;
1640        validate_optional_string_length(
1641            "resourcePolicy",
1642            body["ResourcePolicy"].as_str(),
1643            1,
1644            20480,
1645        )?;
1646        let policy = body["ResourcePolicy"].as_str().map(|s| s.to_string());
1647
1648        let mut accounts = self.state.write();
1649        let state = accounts.get_or_create(&req.account_id);
1650        let secret = self.find_secret_mut(state, &secret_id)?;
1651        secret.resource_policy = policy;
1652
1653        let response = json!({
1654            "ARN": secret.arn,
1655            "Name": secret.name,
1656        });
1657
1658        Ok(AwsResponse::ok_json(response))
1659    }
1660
1661    fn delete_resource_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1662        let body = req.json_body();
1663        let secret_id = require_secret_id(&body)?;
1664
1665        let mut accounts = self.state.write();
1666        let state = accounts.get_or_create(&req.account_id);
1667        let secret = self.find_secret_mut(state, &secret_id)?;
1668        secret.resource_policy = None;
1669
1670        let response = json!({
1671            "ARN": secret.arn,
1672            "Name": secret.name,
1673        });
1674
1675        Ok(AwsResponse::ok_json(response))
1676    }
1677
1678    fn replicate_secret_to_regions(
1679        &self,
1680        req: &AwsRequest,
1681    ) -> Result<AwsResponse, AwsServiceError> {
1682        let body = req.json_body();
1683        let secret_id = require_secret_id(&body)?;
1684
1685        let accounts = self.state.read();
1686        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1687        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1688        let secret = self.find_secret_ref(state, &secret_id)?;
1689
1690        let response = json!({
1691            "ARN": secret.arn,
1692            "ReplicationStatus": [],
1693        });
1694        Ok(AwsResponse::ok_json(response))
1695    }
1696
1697    fn remove_regions_from_replication(
1698        &self,
1699        req: &AwsRequest,
1700    ) -> Result<AwsResponse, AwsServiceError> {
1701        let body = req.json_body();
1702        let secret_id = require_secret_id(&body)?;
1703
1704        let accounts = self.state.read();
1705        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1706        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1707        let secret = self.find_secret_ref(state, &secret_id)?;
1708
1709        let response = json!({
1710            "ARN": secret.arn,
1711            "ReplicationStatus": [],
1712        });
1713        Ok(AwsResponse::ok_json(response))
1714    }
1715
1716    fn stop_replication_to_replica(
1717        &self,
1718        req: &AwsRequest,
1719    ) -> Result<AwsResponse, AwsServiceError> {
1720        let body = req.json_body();
1721        let secret_id = require_secret_id(&body)?;
1722
1723        let accounts = self.state.read();
1724        let empty = SecretsManagerState::new(&req.account_id, &req.region);
1725        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1726        let secret = self.find_secret_ref(state, &secret_id)?;
1727
1728        let response = json!({
1729            "ARN": secret.arn,
1730        });
1731        Ok(AwsResponse::ok_json(response))
1732    }
1733
1734    /// Find a secret by name, full ARN, or partial ARN (mutable).
1735    fn find_secret_mut<'a>(
1736        &self,
1737        state: &'a mut crate::state::SecretsManagerState,
1738        secret_id: &str,
1739    ) -> Result<&'a mut Secret, AwsServiceError> {
1740        let key = self.find_secret_key(state, secret_id)?;
1741        Ok(state.secrets.get_mut(&key).unwrap())
1742    }
1743
1744    fn find_secret_key(
1745        &self,
1746        state: &crate::state::SecretsManagerState,
1747        secret_id: &str,
1748    ) -> Result<String, AwsServiceError> {
1749        if state.secrets.contains_key(secret_id) {
1750            return Ok(secret_id.to_string());
1751        }
1752
1753        for secret in state.secrets.values() {
1754            if secret.arn == secret_id {
1755                return Ok(secret.name.clone());
1756            }
1757        }
1758
1759        if secret_id.starts_with("arn:aws:secretsmanager:") {
1760            for secret in state.secrets.values() {
1761                if secret.arn.starts_with(secret_id) {
1762                    return Ok(secret.name.clone());
1763                }
1764            }
1765        }
1766
1767        Err(AwsServiceError::aws_error(
1768            StatusCode::NOT_FOUND,
1769            "ResourceNotFoundException",
1770            "Secrets Manager can't find the specified secret.",
1771        ))
1772    }
1773
1774    /// Find a secret by name, full ARN, or partial ARN (immutable).
1775    fn find_secret_ref<'a>(
1776        &self,
1777        state: &'a crate::state::SecretsManagerState,
1778        secret_id: &str,
1779    ) -> Result<&'a Secret, AwsServiceError> {
1780        if let Some(secret) = state.secrets.get(secret_id) {
1781            return Ok(secret);
1782        }
1783
1784        // Search by full ARN
1785        for secret in state.secrets.values() {
1786            if secret.arn == secret_id {
1787                return Ok(secret);
1788            }
1789        }
1790
1791        // Search by partial ARN
1792        if secret_id.starts_with("arn:aws:secretsmanager:") {
1793            for secret in state.secrets.values() {
1794                if secret.arn.starts_with(secret_id) {
1795                    return Ok(secret);
1796                }
1797            }
1798        }
1799
1800        Err(AwsServiceError::aws_error(
1801            StatusCode::NOT_FOUND,
1802            "ResourceNotFoundException",
1803            "Secrets Manager can't find the specified secret.",
1804        ))
1805    }
1806}
1807
1808/// Parsed + validated inputs for `CreateSecret`.
1809struct CreateSecretInput {
1810    name: String,
1811    client_request_token: Option<String>,
1812    description: Option<String>,
1813    kms_key_id: Option<String>,
1814    secret_string: Option<String>,
1815    secret_binary: Option<Vec<u8>>,
1816    tags: Vec<(String, String)>,
1817}
1818
1819impl CreateSecretInput {
1820    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1821        validate_required("Name", &body["Name"])?;
1822        let name = body["Name"]
1823            .as_str()
1824            .ok_or_else(|| {
1825                AwsServiceError::aws_error(
1826                    StatusCode::BAD_REQUEST,
1827                    "InvalidParameterException",
1828                    "Name is required",
1829                )
1830            })?
1831            .to_string();
1832        validate_string_length("name", &name, 1, 512)?;
1833        validate_optional_string_length(
1834            "clientRequestToken",
1835            body["ClientRequestToken"].as_str(),
1836            32,
1837            64,
1838        )?;
1839        validate_optional_string_length("description", body["Description"].as_str(), 0, 2048)?;
1840        validate_optional_string_length("kmsKeyId", body["KmsKeyId"].as_str(), 0, 2048)?;
1841        validate_optional_string_length("secretString", body["SecretString"].as_str(), 1, 65536)?;
1842
1843        Ok(Self {
1844            name,
1845            client_request_token: body["ClientRequestToken"].as_str().map(|s| s.to_string()),
1846            description: body["Description"].as_str().map(|s| s.to_string()),
1847            kms_key_id: body["KmsKeyId"].as_str().map(|s| s.to_string()),
1848            secret_string: body["SecretString"].as_str().map(|s| s.to_string()),
1849            secret_binary: body["SecretBinary"].as_str().and_then(base64_decode),
1850            tags: parse_tags(&body["Tags"]),
1851        })
1852    }
1853}
1854
1855fn require_secret_id(body: &Value) -> Result<String, AwsServiceError> {
1856    let id = body["SecretId"].as_str().ok_or_else(|| {
1857        AwsServiceError::aws_error(
1858            StatusCode::BAD_REQUEST,
1859            "InvalidParameterException",
1860            "SecretId is required",
1861        )
1862    })?;
1863    validate_string_length("secretId", id, 1, 2048)?;
1864    Ok(id.to_string())
1865}
1866
1867fn parse_tags(tags_val: &Value) -> Vec<(String, String)> {
1868    tags_val
1869        .as_array()
1870        .map(|arr| {
1871            arr.iter()
1872                .filter_map(|t| {
1873                    let key = t["Key"].as_str()?;
1874                    let value = t["Value"].as_str()?;
1875                    Some((key.to_string(), value.to_string()))
1876                })
1877                .collect()
1878        })
1879        .unwrap_or_default()
1880}
1881
1882fn tags_to_json(tags: &[(String, String)]) -> Vec<Value> {
1883    tags.iter()
1884        .map(|(k, v)| json!({"Key": k, "Value": v}))
1885        .collect()
1886}
1887
1888/// Split text into words for secret name filtering.
1889/// Splits on special characters (/ - _ + = . @) and camelCase.
1890/// If multiple different special characters are present, doesn't split.
1891/// Spaces are always split on first.
1892fn split_words(text: &str) -> Vec<String> {
1893    // First split on whitespace, then apply word splitting to each part
1894    let mut all_words = Vec::new();
1895    for space_part in text.split_whitespace() {
1896        all_words.extend(split_words_no_space(space_part));
1897    }
1898    all_words
1899}
1900
1901fn split_words_no_space(text: &str) -> Vec<String> {
1902    let special_chars = ['/', '-', '_', '+', '=', '.', '@'];
1903
1904    // Check if text is just a special char
1905    if text.len() == 1 && special_chars.contains(&text.chars().next().unwrap_or(' ')) {
1906        return vec![];
1907    }
1908
1909    // Find which special chars are present
1910    let present: Vec<char> = special_chars
1911        .iter()
1912        .filter(|&&c| text.contains(c))
1913        .copied()
1914        .collect();
1915
1916    if present.len() > 1 {
1917        // Multiple different special chars: don't split
1918        return vec![text.to_string()];
1919    }
1920
1921    if present.len() == 1 {
1922        let ch = present[0];
1923        let parts: Vec<&str> = text.split(ch).filter(|s| !s.is_empty()).collect();
1924        let mut result = Vec::new();
1925        for part in parts {
1926            result.extend(split_by_uppercase(part));
1927        }
1928        return result;
1929    }
1930
1931    // No special chars: split by uppercase
1932    split_by_uppercase(text)
1933}
1934
1935/// Split a string by the pattern: a non-lowercase char followed by one or more lowercase chars.
1936/// Equivalent to Python regex: re.split(r"([^a-z][a-z]+)", s)
1937fn split_by_uppercase(text: &str) -> Vec<String> {
1938    // Implement the equivalent of Python's re.split(r"([^a-z][a-z]+)", text)
1939    // re.split with capturing group returns: [before, match, between, match, ..., after]
1940    let chars: Vec<char> = text.chars().collect();
1941    let mut words = Vec::new();
1942    let mut last_end = 0;
1943    let mut i = 0;
1944
1945    while i < chars.len() {
1946        // Try to find pattern: [^a-z][a-z]+
1947        if !chars[i].is_ascii_lowercase()
1948            && i + 1 < chars.len()
1949            && chars[i + 1].is_ascii_lowercase()
1950        {
1951            // Text before this match (between previous match end and this match start)
1952            if i > last_end {
1953                let between: String = chars[last_end..i].iter().collect();
1954                let trimmed = between.trim().to_string();
1955                if !trimmed.is_empty() {
1956                    words.push(trimmed);
1957                }
1958            }
1959
1960            // The match itself
1961            let start = i;
1962            i += 2;
1963            while i < chars.len() && chars[i].is_ascii_lowercase() {
1964                i += 1;
1965            }
1966            let word: String = chars[start..i].iter().collect();
1967            let trimmed = word.trim().to_string();
1968            if !trimmed.is_empty() {
1969                words.push(trimmed);
1970            }
1971            last_end = i;
1972        } else {
1973            i += 1;
1974        }
1975    }
1976
1977    // Text after last match
1978    if last_end < chars.len() {
1979        let after: String = chars[last_end..].iter().collect();
1980        let trimmed = after.trim().to_string();
1981        if !trimmed.is_empty() {
1982            words.push(trimmed);
1983        }
1984    }
1985
1986    words
1987}
1988
1989/// Match a pattern against a value.
1990/// - match_prefix=true: simple prefix match on the full string
1991/// - match_prefix=false: split both into words, all pattern words must prefix-match some value word
1992fn match_pattern(pattern: &str, value: &str, match_prefix: bool, case_sensitive: bool) -> bool {
1993    if match_prefix {
1994        if case_sensitive {
1995            value.starts_with(pattern)
1996        } else {
1997            value.to_lowercase().starts_with(&pattern.to_lowercase())
1998        }
1999    } else {
2000        let mut pattern_words = split_words(pattern);
2001        if pattern_words.is_empty() {
2002            return false;
2003        }
2004        let mut value_words = split_words(value);
2005        if !case_sensitive {
2006            pattern_words = pattern_words.iter().map(|w| w.to_lowercase()).collect();
2007            value_words = value_words.iter().map(|w| w.to_lowercase()).collect();
2008        }
2009        for pw in &pattern_words {
2010            if !value_words.iter().any(|vw| vw.starts_with(pw.as_str())) {
2011                return false;
2012            }
2013        }
2014        true
2015    }
2016}
2017
2018/// The main matcher: check patterns against a list of strings.
2019/// Supports negation (!pattern), prefix matching, and case sensitivity.
2020fn matcher(patterns: &[&str], strings: &[&str], match_prefix: bool, case_sensitive: bool) -> bool {
2021    // First check negated patterns
2022    for pattern in patterns.iter().filter(|p| p.starts_with('!')) {
2023        let inner = &pattern[1..];
2024        for s in strings {
2025            if !match_pattern(inner, s, match_prefix, case_sensitive) {
2026                return true;
2027            }
2028        }
2029    }
2030
2031    // Then check positive patterns
2032    for pattern in patterns.iter().filter(|p| !p.starts_with('!')) {
2033        for s in strings {
2034            if match_pattern(pattern, s, match_prefix, case_sensitive) {
2035                return true;
2036            }
2037        }
2038    }
2039    false
2040}
2041
2042/// Name filter: prefix match, case sensitive
2043fn filter_name(secret: &Secret, values: &[&str]) -> bool {
2044    matcher(values, &[secret.name.as_str()], true, true)
2045}
2046
2047/// Description filter: word match, case insensitive
2048fn filter_description(secret: &Secret, values: &[&str]) -> bool {
2049    match secret.description.as_deref() {
2050        Some(desc) if !desc.is_empty() => matcher(values, &[desc], false, false),
2051        _ => false,
2052    }
2053}
2054
2055/// Tag key filter: prefix match, case sensitive
2056fn filter_tag_key(secret: &Secret, values: &[&str]) -> bool {
2057    if secret.tags.is_empty() {
2058        return false;
2059    }
2060    let keys: Vec<&str> = secret.tags.iter().map(|(k, _)| k.as_str()).collect();
2061    matcher(values, &keys, true, true)
2062}
2063
2064/// Tag value filter: prefix match, case sensitive
2065fn filter_tag_value(secret: &Secret, values: &[&str]) -> bool {
2066    if secret.tags.is_empty() {
2067        return false;
2068    }
2069    let vals: Vec<&str> = secret.tags.iter().map(|(_, v)| v.as_str()).collect();
2070    matcher(values, &vals, true, true)
2071}
2072
2073/// All filter: word match, case insensitive, across all fields
2074fn filter_all(secret: &Secret, values: &[&str]) -> bool {
2075    let mut attributes: Vec<&str> = vec![secret.name.as_str()];
2076    if let Some(ref desc) = secret.description {
2077        if !desc.is_empty() {
2078            attributes.push(desc.as_str());
2079        }
2080    }
2081    for (k, v) in &secret.tags {
2082        attributes.push(k.as_str());
2083        attributes.push(v.as_str());
2084    }
2085    matcher(values, &attributes, false, false)
2086}
2087
2088fn simple_random() -> usize {
2089    use std::collections::hash_map::RandomState;
2090    use std::hash::{BuildHasher, Hasher};
2091    let s = RandomState::new();
2092    let mut hasher = s.build_hasher();
2093    hasher.write_usize(0);
2094    hasher.finish() as usize
2095}
2096
2097#[async_trait]
2098impl AwsService for SecretsManagerService {
2099    fn service_name(&self) -> &str {
2100        "secretsmanager"
2101    }
2102
2103    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2104        let mutates = is_mutating_action(req.action.as_str());
2105        let result = match req.action.as_str() {
2106            "CreateSecret" => self.create_secret(&req),
2107            "GetSecretValue" => self.get_secret_value(&req),
2108            "PutSecretValue" => self.put_secret_value(&req),
2109            "UpdateSecret" => self.update_secret(&req),
2110            "DeleteSecret" => self.delete_secret(&req),
2111            "RestoreSecret" => self.restore_secret(&req),
2112            "DescribeSecret" => self.describe_secret(&req),
2113            "ListSecrets" => self.list_secrets(&req),
2114            "TagResource" => self.tag_resource(&req),
2115            "UntagResource" => self.untag_resource(&req),
2116            "ListSecretVersionIds" => self.list_secret_version_ids(&req),
2117            "GetRandomPassword" => self.get_random_password(&req),
2118            "RotateSecret" => {
2119                let (response, invocation) = self.rotate_secret(&req)?;
2120                if let Some(inv) = invocation {
2121                    if let Some(ref bus) = self.delivery_bus {
2122                        let bus = bus.clone();
2123                        // AWS invokes the rotation Lambda asynchronously for each step.
2124                        tokio::spawn(async move {
2125                            for step in &["createSecret", "setSecret", "testSecret", "finishSecret"]
2126                            {
2127                                let payload = serde_json::json!({
2128                                    "SecretId": inv.secret_id,
2129                                    "ClientRequestToken": inv.client_request_token,
2130                                    "Step": step,
2131                                });
2132                                let payload_str = payload.to_string();
2133                                match bus.invoke_lambda(&inv.lambda_arn, &payload_str).await {
2134                                    Some(Ok(_)) => {}
2135                                    Some(Err(e)) => {
2136                                        tracing::warn!(
2137                                            step = step,
2138                                            error = %e,
2139                                            "rotation Lambda invocation failed"
2140                                        );
2141                                    }
2142                                    None => {
2143                                        tracing::warn!(
2144                                            lambda_arn = %inv.lambda_arn,
2145                                            step = step,
2146                                            "rotation Lambda delivery not configured; \
2147                                             Lambda invocation skipped"
2148                                        );
2149                                        break;
2150                                    }
2151                                }
2152                            }
2153                        });
2154                    }
2155                }
2156                Ok(response)
2157            }
2158            "CancelRotateSecret" => self.cancel_rotate_secret(&req),
2159            "UpdateSecretVersionStage" => self.update_secret_version_stage(&req),
2160            "BatchGetSecretValue" => self.batch_get_secret_value(&req),
2161            "GetResourcePolicy" => self.get_resource_policy(&req),
2162            "PutResourcePolicy" => self.put_resource_policy(&req),
2163            "DeleteResourcePolicy" => self.delete_resource_policy(&req),
2164            "ValidateResourcePolicy" => self.validate_resource_policy(&req),
2165            "ReplicateSecretToRegions" => self.replicate_secret_to_regions(&req),
2166            "RemoveRegionsFromReplication" => self.remove_regions_from_replication(&req),
2167            "StopReplicationToReplica" => self.stop_replication_to_replica(&req),
2168            _ => Err(AwsServiceError::action_not_implemented(
2169                "secretsmanager",
2170                &req.action,
2171            )),
2172        };
2173        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
2174            self.save_snapshot().await;
2175        }
2176        result
2177    }
2178
2179    fn supported_actions(&self) -> &[&str] {
2180        &[
2181            "CreateSecret",
2182            "GetSecretValue",
2183            "PutSecretValue",
2184            "UpdateSecret",
2185            "DeleteSecret",
2186            "RestoreSecret",
2187            "DescribeSecret",
2188            "ListSecrets",
2189            "TagResource",
2190            "UntagResource",
2191            "ListSecretVersionIds",
2192            "GetRandomPassword",
2193            "RotateSecret",
2194            "CancelRotateSecret",
2195            "UpdateSecretVersionStage",
2196            "BatchGetSecretValue",
2197            "GetResourcePolicy",
2198            "PutResourcePolicy",
2199            "DeleteResourcePolicy",
2200            "ValidateResourcePolicy",
2201            "ReplicateSecretToRegions",
2202            "RemoveRegionsFromReplication",
2203            "StopReplicationToReplica",
2204        ]
2205    }
2206}
2207
2208fn base64_decode(input: &str) -> Option<Vec<u8>> {
2209    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2210    let mut buf = Vec::new();
2211    let mut bits: u32 = 0;
2212    let mut count = 0;
2213    for &b in input.as_bytes() {
2214        if b == b'=' || b == b'\n' || b == b'\r' {
2215            continue;
2216        }
2217        let val = table.iter().position(|&c| c == b)? as u32;
2218        bits = (bits << 6) | val;
2219        count += 1;
2220        if count == 4 {
2221            buf.push((bits >> 16) as u8);
2222            buf.push((bits >> 8) as u8);
2223            buf.push(bits as u8);
2224            bits = 0;
2225            count = 0;
2226        }
2227    }
2228    match count {
2229        2 => {
2230            bits <<= 12;
2231            buf.push((bits >> 16) as u8);
2232        }
2233        3 => {
2234            bits <<= 6;
2235            buf.push((bits >> 16) as u8);
2236            buf.push((bits >> 8) as u8);
2237        }
2238        _ => {}
2239    }
2240    Some(buf)
2241}
2242
2243fn base64_encode(input: &[u8]) -> String {
2244    let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2245    let mut result = String::new();
2246    for chunk in input.chunks(3) {
2247        let b0 = chunk[0] as u32;
2248        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
2249        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
2250        let triple = (b0 << 16) | (b1 << 8) | b2;
2251        result.push(table[((triple >> 18) & 0x3F) as usize] as char);
2252        result.push(table[((triple >> 12) & 0x3F) as usize] as char);
2253        if chunk.len() > 1 {
2254            result.push(table[((triple >> 6) & 0x3F) as usize] as char);
2255        } else {
2256            result.push('=');
2257        }
2258        if chunk.len() > 2 {
2259            result.push(table[(triple & 0x3F) as usize] as char);
2260        } else {
2261            result.push('=');
2262        }
2263    }
2264    result
2265}
2266
2267#[cfg(test)]
2268mod tests {
2269    use super::*;
2270    use bytes::Bytes;
2271    use http::{HeaderMap, Method};
2272    use parking_lot::RwLock;
2273    use std::collections::HashMap;
2274    use std::sync::Arc;
2275
2276    fn make_state() -> SharedSecretsManagerState {
2277        Arc::new(RwLock::new(
2278            fakecloud_core::multi_account::MultiAccountState::new(
2279                "123456789012",
2280                "us-east-1",
2281                "http://localhost:4566",
2282            ),
2283        ))
2284    }
2285
2286    fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
2287        match result {
2288            Err(e) => e,
2289            Ok(_) => panic!("expected error, got Ok"),
2290        }
2291    }
2292
2293    fn make_request(action: &str, body: &str) -> AwsRequest {
2294        AwsRequest {
2295            service: "secretsmanager".to_string(),
2296            action: action.to_string(),
2297            region: "us-east-1".to_string(),
2298            account_id: "123456789012".to_string(),
2299            request_id: "test-request-id".to_string(),
2300            headers: HeaderMap::new(),
2301            query_params: HashMap::new(),
2302            body: Bytes::from(body.to_string()),
2303            path_segments: vec![],
2304            raw_path: "/".to_string(),
2305            raw_query: String::new(),
2306            method: Method::POST,
2307            is_query_protocol: false,
2308            access_key_id: None,
2309            principal: None,
2310        }
2311    }
2312
2313    #[tokio::test]
2314    async fn test_create_and_get_secret() {
2315        let state = make_state();
2316        let svc = SecretsManagerService::new(state);
2317
2318        let req = make_request(
2319            "CreateSecret",
2320            r#"{"Name": "test/secret", "SecretString": "mysecretvalue"}"#,
2321        );
2322        let resp = svc.handle(req).await.unwrap();
2323        assert_eq!(resp.status, StatusCode::OK);
2324        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2325        assert_eq!(body["Name"], "test/secret");
2326        assert!(body["ARN"].as_str().unwrap().contains("test/secret"));
2327
2328        let req = make_request("GetSecretValue", r#"{"SecretId": "test/secret"}"#);
2329        let resp = svc.handle(req).await.unwrap();
2330        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2331        assert_eq!(body["SecretString"], "mysecretvalue");
2332    }
2333
2334    #[tokio::test]
2335    async fn test_create_secret_without_value() {
2336        let state = make_state();
2337        let svc = SecretsManagerService::new(state);
2338
2339        let req = make_request("CreateSecret", r#"{"Name": "empty-secret"}"#);
2340        let resp = svc.handle(req).await.unwrap();
2341        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2342        assert_eq!(body["Name"], "empty-secret");
2343        assert!(body.get("VersionId").is_none());
2344    }
2345
2346    #[tokio::test]
2347    async fn test_put_secret_value_creates_version() {
2348        let state = make_state();
2349        let svc = SecretsManagerService::new(state);
2350
2351        let req = make_request(
2352            "CreateSecret",
2353            r#"{"Name": "versioned", "SecretString": "v1"}"#,
2354        );
2355        svc.handle(req).await.unwrap();
2356
2357        let req = make_request(
2358            "PutSecretValue",
2359            r#"{"SecretId": "versioned", "SecretString": "v2"}"#,
2360        );
2361        let resp = svc.handle(req).await.unwrap();
2362        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2363        assert_eq!(body["Name"], "versioned");
2364
2365        // Get should return v2
2366        let req = make_request("GetSecretValue", r#"{"SecretId": "versioned"}"#);
2367        let resp = svc.handle(req).await.unwrap();
2368        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2369        assert_eq!(body["SecretString"], "v2");
2370    }
2371
2372    #[tokio::test]
2373    async fn test_delete_and_restore_secret() {
2374        let state = make_state();
2375        let svc = SecretsManagerService::new(state);
2376
2377        let req = make_request(
2378            "CreateSecret",
2379            r#"{"Name": "deleteme", "SecretString": "value"}"#,
2380        );
2381        svc.handle(req).await.unwrap();
2382
2383        // Delete (soft)
2384        let req = make_request("DeleteSecret", r#"{"SecretId": "deleteme"}"#);
2385        let resp = svc.handle(req).await.unwrap();
2386        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2387        assert!(body["DeletionDate"].as_f64().is_some());
2388
2389        // GetSecretValue should fail
2390        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2391        assert!(svc.handle(req).await.is_err());
2392
2393        // Restore
2394        let req = make_request("RestoreSecret", r#"{"SecretId": "deleteme"}"#);
2395        let resp = svc.handle(req).await.unwrap();
2396        assert_eq!(resp.status, StatusCode::OK);
2397
2398        // GetSecretValue should work again
2399        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2400        let resp = svc.handle(req).await.unwrap();
2401        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2402        assert_eq!(body["SecretString"], "value");
2403    }
2404
2405    #[tokio::test]
2406    async fn test_list_secrets() {
2407        let state = make_state();
2408        let svc = SecretsManagerService::new(state);
2409
2410        for name in &["alpha", "beta", "gamma"] {
2411            let req = make_request(
2412                "CreateSecret",
2413                &format!(r#"{{"Name": "{name}", "SecretString": "val"}}"#),
2414            );
2415            svc.handle(req).await.unwrap();
2416        }
2417
2418        let req = make_request("ListSecrets", "{}");
2419        let resp = svc.handle(req).await.unwrap();
2420        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2421        assert_eq!(body["SecretList"].as_array().unwrap().len(), 3);
2422    }
2423
2424    #[tokio::test]
2425    async fn test_tags() {
2426        let state = make_state();
2427        let svc = SecretsManagerService::new(state);
2428
2429        let req = make_request(
2430            "CreateSecret",
2431            r#"{"Name": "tagged", "SecretString": "val"}"#,
2432        );
2433        svc.handle(req).await.unwrap();
2434
2435        let req = make_request(
2436            "TagResource",
2437            r#"{"SecretId": "tagged", "Tags": [{"Key": "env", "Value": "prod"}]}"#,
2438        );
2439        svc.handle(req).await.unwrap();
2440
2441        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2442        let resp = svc.handle(req).await.unwrap();
2443        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2444        let tags = body["Tags"].as_array().unwrap();
2445        assert!(tags
2446            .iter()
2447            .any(|t| t["Key"] == "env" && t["Value"] == "prod"));
2448
2449        let req = make_request(
2450            "UntagResource",
2451            r#"{"SecretId": "tagged", "TagKeys": ["env"]}"#,
2452        );
2453        svc.handle(req).await.unwrap();
2454
2455        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2456        let resp = svc.handle(req).await.unwrap();
2457        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2458        // Tags should be empty list after untagging all (but key present since tags were set)
2459        assert_eq!(body["Tags"].as_array().unwrap().len(), 0);
2460    }
2461
2462    #[tokio::test]
2463    async fn test_get_random_password() {
2464        let state = make_state();
2465        let svc = SecretsManagerService::new(state);
2466
2467        let req = make_request("GetRandomPassword", "{}");
2468        let resp = svc.handle(req).await.unwrap();
2469        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2470        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 32);
2471    }
2472
2473    #[tokio::test]
2474    async fn test_replication_ops_return_arn() {
2475        let state = make_state();
2476        let svc = SecretsManagerService::new(state);
2477
2478        let req = make_request(
2479            "CreateSecret",
2480            r#"{"Name": "repl-secret", "SecretString": "val"}"#,
2481        );
2482        let resp = svc.handle(req).await.unwrap();
2483        let create_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2484        let expected_arn = create_body["ARN"].as_str().unwrap();
2485
2486        for action in &[
2487            "ReplicateSecretToRegions",
2488            "RemoveRegionsFromReplication",
2489            "StopReplicationToReplica",
2490        ] {
2491            let req = make_request(action, r#"{"SecretId": "repl-secret"}"#);
2492            let resp = svc.handle(req).await.unwrap();
2493            let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2494            assert_eq!(
2495                body["ARN"].as_str().unwrap(),
2496                expected_arn,
2497                "{action} should return the secret's actual ARN"
2498            );
2499        }
2500    }
2501
2502    #[tokio::test]
2503    async fn test_secret_id_length_validation() {
2504        let state = make_state();
2505        let svc = SecretsManagerService::new(state);
2506
2507        // SecretId too long (> 2048)
2508        let long_id = "x".repeat(2049);
2509        let req = make_request("GetSecretValue", &format!(r#"{{"SecretId": "{long_id}"}}"#));
2510        match svc.handle(req).await {
2511            Err(e) => assert!(e.to_string().contains("ValidationException")),
2512            Ok(_) => panic!("expected ValidationException"),
2513        }
2514    }
2515
2516    #[tokio::test]
2517    async fn test_name_length_validation() {
2518        let state = make_state();
2519        let svc = SecretsManagerService::new(state);
2520
2521        // Name too long (> 512)
2522        let long_name = "x".repeat(513);
2523        let req = make_request(
2524            "CreateSecret",
2525            &format!(r#"{{"Name": "{long_name}", "SecretString": "val"}}"#),
2526        );
2527        match svc.handle(req).await {
2528            Err(e) => assert!(e.to_string().contains("ValidationException")),
2529            Ok(_) => panic!("expected ValidationException"),
2530        }
2531    }
2532
2533    #[tokio::test]
2534    async fn test_next_token_length_validation() {
2535        let state = make_state();
2536        let svc = SecretsManagerService::new(state);
2537
2538        // NextToken too long (> 4096)
2539        let long_token = "x".repeat(4097);
2540        let req = make_request(
2541            "ListSecrets",
2542            &format!(r#"{{"NextToken": "{long_token}"}}"#),
2543        );
2544        match svc.handle(req).await {
2545            Err(e) => assert!(e.to_string().contains("ValidationException")),
2546            Ok(_) => panic!("expected ValidationException"),
2547        }
2548    }
2549
2550    #[tokio::test]
2551    async fn test_client_request_token_length_validation() {
2552        let state = make_state();
2553        let svc = SecretsManagerService::new(state);
2554
2555        // ClientRequestToken too short (< 32)
2556        let req = make_request(
2557            "CreateSecret",
2558            r#"{"Name": "test", "SecretString": "val", "ClientRequestToken": "short"}"#,
2559        );
2560        match svc.handle(req).await {
2561            Err(e) => assert!(e.to_string().contains("ValidationException")),
2562            Ok(_) => panic!("expected ValidationException"),
2563        }
2564    }
2565
2566    #[tokio::test]
2567    async fn test_rotate_secret_with_lambda_creates_pending_version() {
2568        let state = make_state();
2569        let svc = SecretsManagerService::new(state.clone());
2570
2571        // Create a secret
2572        let req = make_request(
2573            "CreateSecret",
2574            r#"{"Name": "rotate-me", "SecretString": "old-password"}"#,
2575        );
2576        svc.handle(req).await.unwrap();
2577
2578        // Rotate with a Lambda ARN
2579        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2580        let body = serde_json::json!({
2581            "SecretId": "rotate-me",
2582            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:rotator",
2583            "ClientRequestToken": token,
2584        });
2585        let req = make_request("RotateSecret", &body.to_string());
2586        let resp = svc.handle(req).await.unwrap();
2587        assert_eq!(resp.status, StatusCode::OK);
2588        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2589        assert_eq!(resp_body["VersionId"], token);
2590
2591        // Real AWS leaves the AWSPENDING version creation to the rotation
2592        // Lambda's createSecret step, so we should NOT pre-create it. Verify
2593        // that no version with the rotation token exists yet.
2594        let _accts = state.read();
2595        let s = _accts.default_ref();
2596        let secret = s.secrets.get("rotate-me").unwrap();
2597        assert!(
2598            !secret.versions.contains_key(token),
2599            "AWSPENDING version must not be pre-created; the rotation Lambda creates it"
2600        );
2601
2602        // Verify rotation config was set
2603        assert_eq!(
2604            secret.rotation_lambda_arn.as_deref(),
2605            Some("arn:aws:lambda:us-east-1:123456789012:function:rotator")
2606        );
2607        assert_eq!(secret.rotation_enabled, Some(true));
2608    }
2609
2610    #[tokio::test]
2611    async fn test_rotate_secret_without_lambda_promotes_directly() {
2612        let state = make_state();
2613        let svc = SecretsManagerService::new(state.clone());
2614
2615        // Create a secret
2616        let req = make_request(
2617            "CreateSecret",
2618            r#"{"Name": "rotate-no-lambda", "SecretString": "value1"}"#,
2619        );
2620        svc.handle(req).await.unwrap();
2621
2622        // Rotate without Lambda ARN
2623        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2624        let body = serde_json::json!({
2625            "SecretId": "rotate-no-lambda",
2626            "ClientRequestToken": token,
2627        });
2628        let req = make_request("RotateSecret", &body.to_string());
2629        svc.handle(req).await.unwrap();
2630
2631        // Verify the new version is AWSCURRENT (no pending)
2632        let _accts = state.read();
2633        let s = _accts.default_ref();
2634        let secret = s.secrets.get("rotate-no-lambda").unwrap();
2635        let new_ver = secret.versions.get(token).unwrap();
2636        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2637        assert_eq!(secret.current_version_id.as_deref(), Some(token));
2638    }
2639
2640    #[tokio::test]
2641    async fn test_rotate_secret_stores_rotation_config() {
2642        let state = make_state();
2643        let svc = SecretsManagerService::new(state.clone());
2644
2645        let req = make_request(
2646            "CreateSecret",
2647            r#"{"Name": "rot-cfg", "SecretString": "pw"}"#,
2648        );
2649        svc.handle(req).await.unwrap();
2650
2651        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2652        let body = serde_json::json!({
2653            "SecretId": "rot-cfg",
2654            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-rotator",
2655            "RotationRules": { "AutomaticallyAfterDays": 30 },
2656            "ClientRequestToken": token,
2657        });
2658        let req = make_request("RotateSecret", &body.to_string());
2659        let resp = svc.handle(req).await.unwrap();
2660        assert_eq!(resp.status, StatusCode::OK);
2661
2662        let _accts = state.read();
2663        let s = _accts.default_ref();
2664        let secret = s.secrets.get("rot-cfg").unwrap();
2665        assert_eq!(secret.rotation_enabled, Some(true));
2666        assert_eq!(
2667            secret.rotation_lambda_arn.as_deref(),
2668            Some("arn:aws:lambda:us-east-1:123456789012:function:my-rotator")
2669        );
2670        assert!(secret.last_rotated_at.is_some());
2671        let rules = secret.rotation_rules.as_ref().unwrap();
2672        assert_eq!(rules.automatically_after_days, Some(30));
2673
2674        // The AWSPENDING version is created by the rotation Lambda's
2675        // createSecret step, not by RotateSecret itself, so verify that no
2676        // version with this token exists yet.
2677        assert!(!secret.versions.contains_key(token));
2678    }
2679
2680    #[tokio::test]
2681    async fn test_rotate_secret_version_stages_change() {
2682        let state = make_state();
2683        let svc = SecretsManagerService::new(state.clone());
2684
2685        let req = make_request(
2686            "CreateSecret",
2687            r#"{"Name": "rot-stages", "SecretString": "original"}"#,
2688        );
2689        svc.handle(req).await.unwrap();
2690
2691        // Get original version id
2692        let original_vid = {
2693            let _accts = state.read();
2694            let s = _accts.default_ref();
2695            let secret = s.secrets.get("rot-stages").unwrap();
2696            secret.current_version_id.clone().unwrap()
2697        };
2698
2699        // Rotate without Lambda (simple rotation)
2700        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2701        let body = serde_json::json!({
2702            "SecretId": "rot-stages",
2703            "ClientRequestToken": token,
2704        });
2705        let req = make_request("RotateSecret", &body.to_string());
2706        svc.handle(req).await.unwrap();
2707
2708        let _accts = state.read();
2709        let s = _accts.default_ref();
2710        let secret = s.secrets.get("rot-stages").unwrap();
2711
2712        // New version should be AWSCURRENT
2713        let new_ver = secret.versions.get(token).unwrap();
2714        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2715
2716        // Old version should be AWSPREVIOUS
2717        let old_ver = secret.versions.get(&original_vid).unwrap();
2718        assert!(old_ver.stages.contains(&"AWSPREVIOUS".to_string()));
2719        assert!(!old_ver.stages.contains(&"AWSCURRENT".to_string()));
2720    }
2721
2722    #[tokio::test]
2723    async fn test_cancel_rotate_secret() {
2724        let state = make_state();
2725        let svc = SecretsManagerService::new(state.clone());
2726
2727        let req = make_request(
2728            "CreateSecret",
2729            r#"{"Name": "cancel-rot", "SecretString": "pw"}"#,
2730        );
2731        svc.handle(req).await.unwrap();
2732
2733        // Enable rotation first
2734        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2735        let body = serde_json::json!({
2736            "SecretId": "cancel-rot",
2737            "ClientRequestToken": token,
2738        });
2739        let req = make_request("RotateSecret", &body.to_string());
2740        svc.handle(req).await.unwrap();
2741
2742        // Verify rotation is enabled
2743        {
2744            let _accts = state.read();
2745            let s = _accts.default_ref();
2746            let secret = s.secrets.get("cancel-rot").unwrap();
2747            assert_eq!(secret.rotation_enabled, Some(true));
2748        }
2749
2750        // Cancel rotation
2751        let req = make_request("CancelRotateSecret", r#"{"SecretId": "cancel-rot"}"#);
2752        let resp = svc.handle(req).await.unwrap();
2753        assert_eq!(resp.status, StatusCode::OK);
2754        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2755        assert_eq!(body["Name"], "cancel-rot");
2756
2757        // Verify rotation is disabled
2758        let _accts = state.read();
2759        let s = _accts.default_ref();
2760        let secret = s.secrets.get("cancel-rot").unwrap();
2761        assert_eq!(secret.rotation_enabled, Some(false));
2762    }
2763
2764    #[tokio::test]
2765    async fn test_cancel_rotate_secret_fails_when_not_enabled() {
2766        let state = make_state();
2767        let svc = SecretsManagerService::new(state);
2768
2769        let req = make_request(
2770            "CreateSecret",
2771            r#"{"Name": "no-rot", "SecretString": "pw"}"#,
2772        );
2773        svc.handle(req).await.unwrap();
2774
2775        let req = make_request("CancelRotateSecret", r#"{"SecretId": "no-rot"}"#);
2776        let result = svc.handle(req).await;
2777        assert!(result.is_err());
2778    }
2779
2780    #[tokio::test]
2781    async fn test_batch_get_secret_value_multiple() {
2782        let state = make_state();
2783        let svc = SecretsManagerService::new(state);
2784
2785        for (name, val) in &[("batch-a", "va"), ("batch-b", "vb"), ("batch-c", "vc")] {
2786            let req = make_request(
2787                "CreateSecret",
2788                &format!(r#"{{"Name": "{name}", "SecretString": "{val}"}}"#),
2789            );
2790            svc.handle(req).await.unwrap();
2791        }
2792
2793        let body = serde_json::json!({
2794            "SecretIdList": ["batch-a", "batch-b", "batch-c"]
2795        });
2796        let req = make_request("BatchGetSecretValue", &body.to_string());
2797        let resp = svc.handle(req).await.unwrap();
2798        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2799
2800        let values = resp_body["SecretValues"].as_array().unwrap();
2801        assert_eq!(values.len(), 3);
2802
2803        // Verify each secret has the right value
2804        let names: Vec<&str> = values.iter().map(|v| v["Name"].as_str().unwrap()).collect();
2805        assert!(names.contains(&"batch-a"));
2806        assert!(names.contains(&"batch-b"));
2807        assert!(names.contains(&"batch-c"));
2808
2809        // Verify no errors
2810        assert!(resp_body.get("Errors").is_none());
2811    }
2812
2813    #[tokio::test]
2814    async fn test_batch_get_secret_value_with_missing() {
2815        let state = make_state();
2816        let svc = SecretsManagerService::new(state);
2817
2818        let req = make_request(
2819            "CreateSecret",
2820            r#"{"Name": "exists", "SecretString": "val"}"#,
2821        );
2822        svc.handle(req).await.unwrap();
2823
2824        let body = serde_json::json!({
2825            "SecretIdList": ["exists", "nonexistent"]
2826        });
2827        let req = make_request("BatchGetSecretValue", &body.to_string());
2828        let resp = svc.handle(req).await.unwrap();
2829        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2830
2831        let values = resp_body["SecretValues"].as_array().unwrap();
2832        assert_eq!(values.len(), 1);
2833        assert_eq!(values[0]["Name"], "exists");
2834
2835        let errors = resp_body["Errors"].as_array().unwrap();
2836        assert_eq!(errors.len(), 1);
2837        assert_eq!(errors[0]["SecretId"], "nonexistent");
2838        assert_eq!(errors[0]["ErrorCode"], "ResourceNotFoundException");
2839    }
2840
2841    #[tokio::test]
2842    async fn test_update_secret_changes_description_and_kms() {
2843        let state = make_state();
2844        let svc = SecretsManagerService::new(state);
2845
2846        let req = make_request(
2847            "CreateSecret",
2848            r#"{"Name": "updatable", "SecretString": "val", "Description": "old desc"}"#,
2849        );
2850        svc.handle(req).await.unwrap();
2851
2852        // Update description and KmsKeyId
2853        let body = serde_json::json!({
2854            "SecretId": "updatable",
2855            "Description": "new desc",
2856            "KmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/my-key"
2857        });
2858        let req = make_request("UpdateSecret", &body.to_string());
2859        let resp = svc.handle(req).await.unwrap();
2860        assert_eq!(resp.status, StatusCode::OK);
2861        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2862        assert_eq!(resp_body["Name"], "updatable");
2863        // No VersionId since no new value was provided
2864        assert!(resp_body.get("VersionId").is_none());
2865
2866        // Describe to verify changes
2867        let req = make_request("DescribeSecret", r#"{"SecretId": "updatable"}"#);
2868        let resp = svc.handle(req).await.unwrap();
2869        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2870        assert_eq!(body["Description"], "new desc");
2871        assert_eq!(
2872            body["KmsKeyId"],
2873            "arn:aws:kms:us-east-1:123456789012:key/my-key"
2874        );
2875    }
2876
2877    #[tokio::test]
2878    async fn test_update_secret_with_new_value() {
2879        let state = make_state();
2880        let svc = SecretsManagerService::new(state);
2881
2882        let req = make_request(
2883            "CreateSecret",
2884            r#"{"Name": "upd-val", "SecretString": "old"}"#,
2885        );
2886        svc.handle(req).await.unwrap();
2887
2888        // Update with a new value
2889        let body = serde_json::json!({
2890            "SecretId": "upd-val",
2891            "SecretString": "new-value"
2892        });
2893        let req = make_request("UpdateSecret", &body.to_string());
2894        let resp = svc.handle(req).await.unwrap();
2895        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2896        assert!(resp_body["VersionId"].as_str().is_some());
2897
2898        // Get should return new value
2899        let req = make_request("GetSecretValue", r#"{"SecretId": "upd-val"}"#);
2900        let resp = svc.handle(req).await.unwrap();
2901        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2902        assert_eq!(body["SecretString"], "new-value");
2903    }
2904
2905    #[tokio::test]
2906    async fn test_get_random_password_custom_length() {
2907        let state = make_state();
2908        let svc = SecretsManagerService::new(state);
2909
2910        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 64}"#);
2911        let resp = svc.handle(req).await.unwrap();
2912        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2913        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 64);
2914    }
2915
2916    #[tokio::test]
2917    async fn test_get_random_password_exclude_chars() {
2918        let state = make_state();
2919        let svc = SecretsManagerService::new(state);
2920
2921        let req = make_request(
2922            "GetRandomPassword",
2923            r#"{"PasswordLength": 100, "ExcludeCharacters": "abc123"}"#,
2924        );
2925        let resp = svc.handle(req).await.unwrap();
2926        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2927        let password = body["RandomPassword"].as_str().unwrap();
2928        assert_eq!(password.len(), 100);
2929        assert!(!password.contains('a'));
2930        assert!(!password.contains('b'));
2931        assert!(!password.contains('c'));
2932        assert!(!password.contains('1'));
2933        assert!(!password.contains('2'));
2934        assert!(!password.contains('3'));
2935    }
2936
2937    #[tokio::test]
2938    async fn test_get_random_password_exclude_types() {
2939        let state = make_state();
2940        let svc = SecretsManagerService::new(state);
2941
2942        // Exclude everything except lowercase
2943        let body = serde_json::json!({
2944            "PasswordLength": 50,
2945            "ExcludeUppercase": true,
2946            "ExcludeNumbers": true,
2947            "ExcludePunctuation": true,
2948            "RequireEachIncludedType": false,
2949        });
2950        let req = make_request("GetRandomPassword", &body.to_string());
2951        let resp = svc.handle(req).await.unwrap();
2952        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2953        let password = resp_body["RandomPassword"].as_str().unwrap();
2954        assert_eq!(password.len(), 50);
2955        assert!(password.chars().all(|c| c.is_ascii_lowercase()));
2956    }
2957
2958    #[tokio::test]
2959    async fn test_get_random_password_too_short() {
2960        let state = make_state();
2961        let svc = SecretsManagerService::new(state);
2962
2963        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 3}"#);
2964        assert!(svc.handle(req).await.is_err());
2965    }
2966
2967    #[tokio::test]
2968    async fn test_get_random_password_too_long() {
2969        let state = make_state();
2970        let svc = SecretsManagerService::new(state);
2971
2972        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 4097}"#);
2973        assert!(svc.handle(req).await.is_err());
2974    }
2975
2976    #[tokio::test]
2977    async fn test_update_secret_version_stage_move_current() {
2978        let state = make_state();
2979        let svc = SecretsManagerService::new(state.clone());
2980
2981        let req = make_request(
2982            "CreateSecret",
2983            r#"{"Name": "stage-test", "SecretString": "v1"}"#,
2984        );
2985        svc.handle(req).await.unwrap();
2986
2987        // Put a second version
2988        let req = make_request(
2989            "PutSecretValue",
2990            r#"{"SecretId": "stage-test", "SecretString": "v2"}"#,
2991        );
2992        svc.handle(req).await.unwrap();
2993
2994        // Get version IDs
2995        let (v1_id, v2_id) = {
2996            let _accts = state.read();
2997            let s = _accts.default_ref();
2998            let secret = s.secrets.get("stage-test").unwrap();
2999            let current = secret.current_version_id.clone().unwrap();
3000            let previous = secret
3001                .versions
3002                .iter()
3003                .find(|(id, _)| **id != current)
3004                .map(|(id, _)| id.clone())
3005                .unwrap();
3006            (previous, current)
3007        };
3008
3009        // Move AWSCURRENT from v2 back to v1
3010        let body = serde_json::json!({
3011            "SecretId": "stage-test",
3012            "VersionStage": "AWSCURRENT",
3013            "MoveToVersionId": v1_id,
3014            "RemoveFromVersionId": v2_id,
3015        });
3016        let req = make_request("UpdateSecretVersionStage", &body.to_string());
3017        let resp = svc.handle(req).await.unwrap();
3018        assert_eq!(resp.status, StatusCode::OK);
3019
3020        // Verify v1 is now AWSCURRENT
3021        let _accts = state.read();
3022        let s = _accts.default_ref();
3023        let secret = s.secrets.get("stage-test").unwrap();
3024        let v1 = secret.versions.get(&v1_id).unwrap();
3025        assert!(v1.stages.contains(&"AWSCURRENT".to_string()));
3026
3027        // v2 should have AWSPREVIOUS
3028        let v2 = secret.versions.get(&v2_id).unwrap();
3029        assert!(v2.stages.contains(&"AWSPREVIOUS".to_string()));
3030        assert!(!v2.stages.contains(&"AWSCURRENT".to_string()));
3031
3032        assert_eq!(secret.current_version_id.as_deref(), Some(v1_id.as_str()));
3033    }
3034
3035    #[tokio::test]
3036    async fn test_update_secret_version_stage_custom_label() {
3037        let state = make_state();
3038        let svc = SecretsManagerService::new(state.clone());
3039
3040        let req = make_request(
3041            "CreateSecret",
3042            r#"{"Name": "custom-stage", "SecretString": "v1"}"#,
3043        );
3044        svc.handle(req).await.unwrap();
3045
3046        let vid = {
3047            let _accts = state.read();
3048            let s = _accts.default_ref();
3049            s.secrets
3050                .get("custom-stage")
3051                .unwrap()
3052                .current_version_id
3053                .clone()
3054                .unwrap()
3055        };
3056
3057        // Add a custom label
3058        let body = serde_json::json!({
3059            "SecretId": "custom-stage",
3060            "VersionStage": "MYAPP_LIVE",
3061            "MoveToVersionId": vid,
3062        });
3063        let req = make_request("UpdateSecretVersionStage", &body.to_string());
3064        svc.handle(req).await.unwrap();
3065
3066        let _accts = state.read();
3067        let s = _accts.default_ref();
3068        let secret = s.secrets.get("custom-stage").unwrap();
3069        let ver = secret.versions.get(&vid).unwrap();
3070        assert!(ver.stages.contains(&"MYAPP_LIVE".to_string()));
3071        assert!(ver.stages.contains(&"AWSCURRENT".to_string()));
3072    }
3073
3074    #[tokio::test]
3075    async fn test_validate_resource_policy() {
3076        let state = make_state();
3077        let svc = SecretsManagerService::new(state);
3078
3079        let policy = serde_json::json!({
3080            "Version": "2012-10-17",
3081            "Statement": [{
3082                "Effect": "Allow",
3083                "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
3084                "Action": "secretsmanager:GetSecretValue",
3085                "Resource": "*"
3086            }]
3087        });
3088
3089        let body = serde_json::json!({
3090            "ResourcePolicy": policy.to_string(),
3091        });
3092        let req = make_request("ValidateResourcePolicy", &body.to_string());
3093        let resp = svc.handle(req).await.unwrap();
3094        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3095        assert_eq!(resp_body["PolicyValidationPassed"], true);
3096        assert_eq!(resp_body["ValidationErrors"].as_array().unwrap().len(), 0);
3097    }
3098
3099    #[tokio::test]
3100    async fn test_validate_resource_policy_requires_policy() {
3101        let state = make_state();
3102        let svc = SecretsManagerService::new(state);
3103
3104        let req = make_request("ValidateResourcePolicy", r#"{}"#);
3105        assert!(svc.handle(req).await.is_err());
3106    }
3107
3108    #[tokio::test]
3109    async fn test_put_get_delete_resource_policy() {
3110        let state = make_state();
3111        let svc = SecretsManagerService::new(state);
3112
3113        let req = make_request(
3114            "CreateSecret",
3115            r#"{"Name": "policy-secret", "SecretString": "val"}"#,
3116        );
3117        svc.handle(req).await.unwrap();
3118
3119        // Get policy (should be empty initially)
3120        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3121        let resp = svc.handle(req).await.unwrap();
3122        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3123        assert_eq!(body["Name"], "policy-secret");
3124        assert!(body.get("ResourcePolicy").is_none());
3125
3126        // Put policy
3127        let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
3128        let put_body = serde_json::json!({
3129            "SecretId": "policy-secret",
3130            "ResourcePolicy": policy,
3131        });
3132        let req = make_request("PutResourcePolicy", &put_body.to_string());
3133        let resp = svc.handle(req).await.unwrap();
3134        assert_eq!(resp.status, StatusCode::OK);
3135
3136        // Get policy (should have it now)
3137        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3138        let resp = svc.handle(req).await.unwrap();
3139        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3140        assert_eq!(body["ResourcePolicy"], policy);
3141
3142        // Delete policy
3143        let req = make_request("DeleteResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3144        let resp = svc.handle(req).await.unwrap();
3145        assert_eq!(resp.status, StatusCode::OK);
3146
3147        // Get again (should be gone)
3148        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3149        let resp = svc.handle(req).await.unwrap();
3150        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3151        assert!(body.get("ResourcePolicy").is_none());
3152    }
3153
3154    #[tokio::test]
3155    async fn test_batch_get_secret_value_with_deleted() {
3156        let state = make_state();
3157        let svc = SecretsManagerService::new(state);
3158
3159        let req = make_request(
3160            "CreateSecret",
3161            r#"{"Name": "batch-del", "SecretString": "val"}"#,
3162        );
3163        svc.handle(req).await.unwrap();
3164
3165        // Soft-delete it
3166        let req = make_request("DeleteSecret", r#"{"SecretId": "batch-del"}"#);
3167        svc.handle(req).await.unwrap();
3168
3169        let body = serde_json::json!({
3170            "SecretIdList": ["batch-del"]
3171        });
3172        let req = make_request("BatchGetSecretValue", &body.to_string());
3173        let resp = svc.handle(req).await.unwrap();
3174        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3175
3176        // Should have 0 values and 1 error
3177        assert_eq!(resp_body["SecretValues"].as_array().unwrap().len(), 0);
3178        let errors = resp_body["Errors"].as_array().unwrap();
3179        assert_eq!(errors.len(), 1);
3180        assert_eq!(errors[0]["ErrorCode"], "InvalidRequestException");
3181    }
3182
3183    // ── CreateSecret idempotency ──
3184
3185    #[tokio::test]
3186    async fn create_secret_idempotent_same_value() {
3187        let state = make_state();
3188        let svc = SecretsManagerService::new(state);
3189
3190        let token = "a".repeat(32);
3191        let body = serde_json::json!({
3192            "Name": "idem",
3193            "SecretString": "val",
3194            "ClientRequestToken": token,
3195        });
3196        let req = make_request("CreateSecret", &body.to_string());
3197        svc.handle(req).await.unwrap();
3198
3199        // Same token + same value -> success (idempotent)
3200        let req = make_request("CreateSecret", &body.to_string());
3201        let resp = svc.handle(req).await.unwrap();
3202        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3203        assert_eq!(b["Name"], "idem");
3204        assert_eq!(b["VersionId"], token);
3205    }
3206
3207    #[tokio::test]
3208    async fn create_secret_idempotent_conflict() {
3209        let state = make_state();
3210        let svc = SecretsManagerService::new(state);
3211
3212        let token = "a".repeat(32);
3213        let body = serde_json::json!({
3214            "Name": "conflict",
3215            "SecretString": "val1",
3216            "ClientRequestToken": token,
3217        });
3218        let req = make_request("CreateSecret", &body.to_string());
3219        svc.handle(req).await.unwrap();
3220
3221        // Same token + different value -> ResourceExistsException
3222        let body2 = serde_json::json!({
3223            "Name": "conflict",
3224            "SecretString": "val2",
3225            "ClientRequestToken": token,
3226        });
3227        let req = make_request("CreateSecret", &body2.to_string());
3228        let err = expect_err(svc.handle(req).await);
3229        assert!(err.to_string().contains("ResourceExistsException"));
3230    }
3231
3232    #[tokio::test]
3233    async fn create_secret_duplicate_name_no_token() {
3234        let state = make_state();
3235        let svc = SecretsManagerService::new(state);
3236
3237        let req = make_request("CreateSecret", r#"{"Name": "dup", "SecretString": "v1"}"#);
3238        svc.handle(req).await.unwrap();
3239
3240        let req = make_request("CreateSecret", r#"{"Name": "dup", "SecretString": "v2"}"#);
3241        let err = expect_err(svc.handle(req).await);
3242        assert!(err.to_string().contains("ResourceExistsException"));
3243    }
3244
3245    #[tokio::test]
3246    async fn create_secret_with_tags_and_description() {
3247        let state = make_state();
3248        let svc = SecretsManagerService::new(state);
3249
3250        let body = serde_json::json!({
3251            "Name": "full-secret",
3252            "SecretString": "v",
3253            "Description": "my secret desc",
3254            "KmsKeyId": "alias/my-key",
3255            "Tags": [{"Key": "env", "Value": "staging"}],
3256        });
3257        let req = make_request("CreateSecret", &body.to_string());
3258        svc.handle(req).await.unwrap();
3259
3260        let req = make_request("DescribeSecret", r#"{"SecretId": "full-secret"}"#);
3261        let resp = svc.handle(req).await.unwrap();
3262        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3263        assert_eq!(b["Description"], "my secret desc");
3264        assert_eq!(b["KmsKeyId"], "alias/my-key");
3265        assert_eq!(b["Tags"][0]["Key"], "env");
3266    }
3267
3268    // ── PutSecretValue edge cases ──
3269
3270    #[tokio::test]
3271    async fn put_secret_value_requires_value() {
3272        let state = make_state();
3273        let svc = SecretsManagerService::new(state);
3274
3275        let req = make_request(
3276            "CreateSecret",
3277            r#"{"Name": "novalue", "SecretString": "v"}"#,
3278        );
3279        svc.handle(req).await.unwrap();
3280
3281        let req = make_request("PutSecretValue", r#"{"SecretId": "novalue"}"#);
3282        let err = expect_err(svc.handle(req).await);
3283        assert!(err.to_string().contains("InvalidRequestException"));
3284    }
3285
3286    #[tokio::test]
3287    async fn put_secret_value_not_found() {
3288        let state = make_state();
3289        let svc = SecretsManagerService::new(state);
3290
3291        let req = make_request(
3292            "PutSecretValue",
3293            r#"{"SecretId": "ghost", "SecretString": "v"}"#,
3294        );
3295        let err = expect_err(svc.handle(req).await);
3296        assert!(err.to_string().contains("ResourceNotFoundException"));
3297    }
3298
3299    #[tokio::test]
3300    async fn put_secret_value_on_deleted_secret() {
3301        let state = make_state();
3302        let svc = SecretsManagerService::new(state);
3303
3304        let req = make_request(
3305            "CreateSecret",
3306            r#"{"Name": "del-put", "SecretString": "v"}"#,
3307        );
3308        svc.handle(req).await.unwrap();
3309        let req = make_request("DeleteSecret", r#"{"SecretId": "del-put"}"#);
3310        svc.handle(req).await.unwrap();
3311
3312        let req = make_request(
3313            "PutSecretValue",
3314            r#"{"SecretId": "del-put", "SecretString": "v2"}"#,
3315        );
3316        let err = expect_err(svc.handle(req).await);
3317        assert!(err.to_string().contains("InvalidRequestException"));
3318    }
3319
3320    #[tokio::test]
3321    async fn put_secret_value_idempotent_match() {
3322        let state = make_state();
3323        let svc = SecretsManagerService::new(state);
3324
3325        let req = make_request(
3326            "CreateSecret",
3327            r#"{"Name": "put-idem", "SecretString": "original"}"#,
3328        );
3329        svc.handle(req).await.unwrap();
3330
3331        let token = "b".repeat(32);
3332        let body = serde_json::json!({
3333            "SecretId": "put-idem",
3334            "SecretString": "new-val",
3335            "ClientRequestToken": token,
3336        });
3337        let req = make_request("PutSecretValue", &body.to_string());
3338        svc.handle(req).await.unwrap();
3339
3340        // Same token + same value -> idempotent success
3341        let req = make_request("PutSecretValue", &body.to_string());
3342        let resp = svc.handle(req).await.unwrap();
3343        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3344        assert_eq!(b["VersionId"], token);
3345    }
3346
3347    #[tokio::test]
3348    async fn put_secret_value_idempotent_conflict() {
3349        let state = make_state();
3350        let svc = SecretsManagerService::new(state);
3351
3352        let req = make_request(
3353            "CreateSecret",
3354            r#"{"Name": "put-conflict", "SecretString": "original"}"#,
3355        );
3356        svc.handle(req).await.unwrap();
3357
3358        let token = "c".repeat(32);
3359        let body = serde_json::json!({
3360            "SecretId": "put-conflict",
3361            "SecretString": "val-a",
3362            "ClientRequestToken": token,
3363        });
3364        let req = make_request("PutSecretValue", &body.to_string());
3365        svc.handle(req).await.unwrap();
3366
3367        // Same token + different value -> conflict
3368        let body2 = serde_json::json!({
3369            "SecretId": "put-conflict",
3370            "SecretString": "val-b",
3371            "ClientRequestToken": token,
3372        });
3373        let req = make_request("PutSecretValue", &body2.to_string());
3374        let err = expect_err(svc.handle(req).await);
3375        assert!(err.to_string().contains("ResourceExistsException"));
3376    }
3377
3378    #[tokio::test]
3379    async fn put_secret_value_with_custom_stages() {
3380        let state = make_state();
3381        let svc = SecretsManagerService::new(state.clone());
3382
3383        let req = make_request(
3384            "CreateSecret",
3385            r#"{"Name": "staged", "SecretString": "v1"}"#,
3386        );
3387        svc.handle(req).await.unwrap();
3388
3389        let body = serde_json::json!({
3390            "SecretId": "staged",
3391            "SecretString": "v2",
3392            "VersionStages": ["AWSCURRENT", "MYAPP_V2"],
3393        });
3394        let req = make_request("PutSecretValue", &body.to_string());
3395        let resp = svc.handle(req).await.unwrap();
3396        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3397        let stages = b["VersionStages"].as_array().unwrap();
3398        assert!(stages.iter().any(|s| s == "MYAPP_V2"));
3399    }
3400
3401    // ── UpdateSecret edge cases ──
3402
3403    #[tokio::test]
3404    async fn update_secret_not_found() {
3405        let state = make_state();
3406        let svc = SecretsManagerService::new(state);
3407
3408        let body = serde_json::json!({
3409            "SecretId": "ghost",
3410            "Description": "new",
3411        });
3412        let req = make_request("UpdateSecret", &body.to_string());
3413        let err = expect_err(svc.handle(req).await);
3414        assert!(err.to_string().contains("ResourceNotFoundException"));
3415    }
3416
3417    #[tokio::test]
3418    async fn update_secret_on_deleted() {
3419        let state = make_state();
3420        let svc = SecretsManagerService::new(state);
3421
3422        let req = make_request(
3423            "CreateSecret",
3424            r#"{"Name": "upd-del", "SecretString": "v"}"#,
3425        );
3426        svc.handle(req).await.unwrap();
3427        let req = make_request("DeleteSecret", r#"{"SecretId": "upd-del"}"#);
3428        svc.handle(req).await.unwrap();
3429
3430        let body = serde_json::json!({
3431            "SecretId": "upd-del",
3432            "Description": "new",
3433        });
3434        let req = make_request("UpdateSecret", &body.to_string());
3435        let err = expect_err(svc.handle(req).await);
3436        assert!(err.to_string().contains("InvalidRequestException"));
3437    }
3438
3439    #[tokio::test]
3440    async fn update_secret_idempotent_match() {
3441        let state = make_state();
3442        let svc = SecretsManagerService::new(state);
3443
3444        let req = make_request(
3445            "CreateSecret",
3446            r#"{"Name": "upd-idem", "SecretString": "orig"}"#,
3447        );
3448        svc.handle(req).await.unwrap();
3449
3450        let token = "d".repeat(32);
3451        let body = serde_json::json!({
3452            "SecretId": "upd-idem",
3453            "SecretString": "new-val",
3454            "ClientRequestToken": token,
3455        });
3456        let req = make_request("UpdateSecret", &body.to_string());
3457        svc.handle(req).await.unwrap();
3458
3459        // Repeat -> idempotent
3460        let req = make_request("UpdateSecret", &body.to_string());
3461        let resp = svc.handle(req).await.unwrap();
3462        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3463        assert_eq!(b["VersionId"], token);
3464    }
3465
3466    // ── DeleteSecret edge cases ──
3467
3468    #[tokio::test]
3469    async fn delete_secret_force() {
3470        let state = make_state();
3471        let svc = SecretsManagerService::new(state.clone());
3472
3473        let req = make_request(
3474            "CreateSecret",
3475            r#"{"Name": "force-del", "SecretString": "v"}"#,
3476        );
3477        svc.handle(req).await.unwrap();
3478
3479        let body = serde_json::json!({
3480            "SecretId": "force-del",
3481            "ForceDeleteWithoutRecovery": true,
3482        });
3483        let req = make_request("DeleteSecret", &body.to_string());
3484        let resp = svc.handle(req).await.unwrap();
3485        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3486        assert_eq!(b["Name"], "force-del");
3487
3488        // Secret should be gone entirely
3489        let _accts = state.read();
3490        let s = _accts.default_ref();
3491        assert!(!s.secrets.contains_key("force-del"));
3492    }
3493
3494    #[tokio::test]
3495    async fn delete_secret_force_nonexistent() {
3496        let state = make_state();
3497        let svc = SecretsManagerService::new(state);
3498
3499        let body = serde_json::json!({
3500            "SecretId": "not-here",
3501            "ForceDeleteWithoutRecovery": true,
3502        });
3503        let req = make_request("DeleteSecret", &body.to_string());
3504        let resp = svc.handle(req).await.unwrap();
3505        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3506        assert_eq!(b["Name"], "not-here");
3507    }
3508
3509    #[tokio::test]
3510    async fn delete_secret_recovery_window() {
3511        let state = make_state();
3512        let svc = SecretsManagerService::new(state);
3513
3514        let req = make_request(
3515            "CreateSecret",
3516            r#"{"Name": "rec-win", "SecretString": "v"}"#,
3517        );
3518        svc.handle(req).await.unwrap();
3519
3520        let body = serde_json::json!({
3521            "SecretId": "rec-win",
3522            "RecoveryWindowInDays": 7,
3523        });
3524        let req = make_request("DeleteSecret", &body.to_string());
3525        let resp = svc.handle(req).await.unwrap();
3526        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3527        assert!(b["DeletionDate"].as_f64().is_some());
3528    }
3529
3530    #[tokio::test]
3531    async fn delete_secret_invalid_recovery_window() {
3532        let state = make_state();
3533        let svc = SecretsManagerService::new(state);
3534
3535        let req = make_request(
3536            "CreateSecret",
3537            r#"{"Name": "bad-win", "SecretString": "v"}"#,
3538        );
3539        svc.handle(req).await.unwrap();
3540
3541        // Too short
3542        let body = serde_json::json!({
3543            "SecretId": "bad-win",
3544            "RecoveryWindowInDays": 3,
3545        });
3546        let req = make_request("DeleteSecret", &body.to_string());
3547        let err = expect_err(svc.handle(req).await);
3548        assert!(err.to_string().contains("InvalidParameterException"));
3549
3550        // Too long
3551        let body = serde_json::json!({
3552            "SecretId": "bad-win",
3553            "RecoveryWindowInDays": 31,
3554        });
3555        let req = make_request("DeleteSecret", &body.to_string());
3556        let err = expect_err(svc.handle(req).await);
3557        assert!(err.to_string().contains("InvalidParameterException"));
3558    }
3559
3560    #[tokio::test]
3561    async fn delete_secret_force_and_recovery_conflict() {
3562        let state = make_state();
3563        let svc = SecretsManagerService::new(state);
3564
3565        let req = make_request("CreateSecret", r#"{"Name": "both", "SecretString": "v"}"#);
3566        svc.handle(req).await.unwrap();
3567
3568        let body = serde_json::json!({
3569            "SecretId": "both",
3570            "ForceDeleteWithoutRecovery": true,
3571            "RecoveryWindowInDays": 7,
3572        });
3573        let req = make_request("DeleteSecret", &body.to_string());
3574        let err = expect_err(svc.handle(req).await);
3575        assert!(err.to_string().contains("InvalidParameterException"));
3576    }
3577
3578    #[tokio::test]
3579    async fn delete_already_deleted_secret() {
3580        let state = make_state();
3581        let svc = SecretsManagerService::new(state);
3582
3583        let req = make_request(
3584            "CreateSecret",
3585            r#"{"Name": "dbl-del", "SecretString": "v"}"#,
3586        );
3587        svc.handle(req).await.unwrap();
3588
3589        let req = make_request("DeleteSecret", r#"{"SecretId": "dbl-del"}"#);
3590        svc.handle(req).await.unwrap();
3591
3592        let req = make_request("DeleteSecret", r#"{"SecretId": "dbl-del"}"#);
3593        let err = expect_err(svc.handle(req).await);
3594        assert!(err.to_string().contains("InvalidRequestException"));
3595    }
3596
3597    // ── GetSecretValue edge cases ──
3598
3599    #[tokio::test]
3600    async fn get_secret_value_by_version_id() {
3601        let state = make_state();
3602        let svc = SecretsManagerService::new(state.clone());
3603
3604        let req = make_request(
3605            "CreateSecret",
3606            r#"{"Name": "ver-get", "SecretString": "v1"}"#,
3607        );
3608        svc.handle(req).await.unwrap();
3609
3610        let v1_id = {
3611            let _accts = state.read();
3612            let s = _accts.default_ref();
3613            s.secrets
3614                .get("ver-get")
3615                .unwrap()
3616                .current_version_id
3617                .clone()
3618                .unwrap()
3619        };
3620
3621        let req = make_request(
3622            "PutSecretValue",
3623            r#"{"SecretId": "ver-get", "SecretString": "v2"}"#,
3624        );
3625        svc.handle(req).await.unwrap();
3626
3627        // Get old version by ID
3628        let body = serde_json::json!({
3629            "SecretId": "ver-get",
3630            "VersionId": v1_id,
3631            "VersionStage": "AWSPREVIOUS",
3632        });
3633        let req = make_request("GetSecretValue", &body.to_string());
3634        let resp = svc.handle(req).await.unwrap();
3635        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3636        assert_eq!(b["SecretString"], "v1");
3637    }
3638
3639    #[tokio::test]
3640    async fn get_secret_value_version_stage_mismatch() {
3641        let state = make_state();
3642        let svc = SecretsManagerService::new(state.clone());
3643
3644        let req = make_request(
3645            "CreateSecret",
3646            r#"{"Name": "mismatch", "SecretString": "v1"}"#,
3647        );
3648        svc.handle(req).await.unwrap();
3649
3650        let vid = {
3651            let _accts = state.read();
3652            let s = _accts.default_ref();
3653            s.secrets
3654                .get("mismatch")
3655                .unwrap()
3656                .current_version_id
3657                .clone()
3658                .unwrap()
3659        };
3660
3661        // Request with VersionId but wrong stage
3662        let body = serde_json::json!({
3663            "SecretId": "mismatch",
3664            "VersionId": vid,
3665            "VersionStage": "AWSPREVIOUS",
3666        });
3667        let req = make_request("GetSecretValue", &body.to_string());
3668        let err = expect_err(svc.handle(req).await);
3669        assert!(err.to_string().contains("ResourceNotFoundException"));
3670    }
3671
3672    #[tokio::test]
3673    async fn get_secret_value_not_found() {
3674        let state = make_state();
3675        let svc = SecretsManagerService::new(state);
3676
3677        let req = make_request("GetSecretValue", r#"{"SecretId": "nope"}"#);
3678        let err = expect_err(svc.handle(req).await);
3679        assert!(err.to_string().contains("ResourceNotFoundException"));
3680    }
3681
3682    #[tokio::test]
3683    async fn get_secret_value_no_versions() {
3684        let state = make_state();
3685        let svc = SecretsManagerService::new(state);
3686
3687        let req = make_request("CreateSecret", r#"{"Name": "empty-ver"}"#);
3688        svc.handle(req).await.unwrap();
3689
3690        let req = make_request("GetSecretValue", r#"{"SecretId": "empty-ver"}"#);
3691        let err = expect_err(svc.handle(req).await);
3692        assert!(err.to_string().contains("ResourceNotFoundException"));
3693    }
3694
3695    #[tokio::test]
3696    async fn get_secret_value_with_binary() {
3697        let state = make_state();
3698        let svc = SecretsManagerService::new(state);
3699
3700        // SecretBinary is base64 encoded
3701        let body = serde_json::json!({
3702            "Name": "bin-secret",
3703            "SecretBinary": "SGVsbG8=",  // "Hello" in base64
3704        });
3705        let req = make_request("CreateSecret", &body.to_string());
3706        svc.handle(req).await.unwrap();
3707
3708        let req = make_request("GetSecretValue", r#"{"SecretId": "bin-secret"}"#);
3709        let resp = svc.handle(req).await.unwrap();
3710        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3711        assert!(b.get("SecretBinary").is_some());
3712        assert!(b.get("SecretString").is_none());
3713    }
3714
3715    // ── ListSecrets with filters ──
3716
3717    #[tokio::test]
3718    async fn list_secrets_filter_by_name() {
3719        let state = make_state();
3720        let svc = SecretsManagerService::new(state);
3721
3722        for name in &["prod/db", "prod/api", "staging/db"] {
3723            let body = serde_json::json!({"Name": name, "SecretString": "v"});
3724            let req = make_request("CreateSecret", &body.to_string());
3725            svc.handle(req).await.unwrap();
3726        }
3727
3728        let body = serde_json::json!({
3729            "Filters": [{"Key": "name", "Values": ["prod/"]}]
3730        });
3731        let req = make_request("ListSecrets", &body.to_string());
3732        let resp = svc.handle(req).await.unwrap();
3733        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3734        assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
3735    }
3736
3737    #[tokio::test]
3738    async fn list_secrets_filter_by_tag_key() {
3739        let state = make_state();
3740        let svc = SecretsManagerService::new(state);
3741
3742        let body = serde_json::json!({
3743            "Name": "tagged-s",
3744            "SecretString": "v",
3745            "Tags": [{"Key": "team", "Value": "backend"}],
3746        });
3747        let req = make_request("CreateSecret", &body.to_string());
3748        svc.handle(req).await.unwrap();
3749
3750        let body = serde_json::json!({"Name": "untagged-s", "SecretString": "v"});
3751        let req = make_request("CreateSecret", &body.to_string());
3752        svc.handle(req).await.unwrap();
3753
3754        let body = serde_json::json!({
3755            "Filters": [{"Key": "tag-key", "Values": ["team"]}]
3756        });
3757        let req = make_request("ListSecrets", &body.to_string());
3758        let resp = svc.handle(req).await.unwrap();
3759        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3760        assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
3761        assert_eq!(b["SecretList"][0]["Name"], "tagged-s");
3762    }
3763
3764    #[tokio::test]
3765    async fn list_secrets_filter_by_description() {
3766        let state = make_state();
3767        let svc = SecretsManagerService::new(state);
3768
3769        let body = serde_json::json!({
3770            "Name": "desc-match",
3771            "SecretString": "v",
3772            "Description": "Database credentials for production",
3773        });
3774        let req = make_request("CreateSecret", &body.to_string());
3775        svc.handle(req).await.unwrap();
3776
3777        let body = serde_json::json!({"Name": "no-desc", "SecretString": "v"});
3778        let req = make_request("CreateSecret", &body.to_string());
3779        svc.handle(req).await.unwrap();
3780
3781        let body = serde_json::json!({
3782            "Filters": [{"Key": "description", "Values": ["Database"]}]
3783        });
3784        let req = make_request("ListSecrets", &body.to_string());
3785        let resp = svc.handle(req).await.unwrap();
3786        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3787        assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
3788    }
3789
3790    #[tokio::test]
3791    async fn list_secrets_include_planned_deletion() {
3792        let state = make_state();
3793        let svc = SecretsManagerService::new(state);
3794
3795        let req = make_request("CreateSecret", r#"{"Name": "alive", "SecretString": "v"}"#);
3796        svc.handle(req).await.unwrap();
3797
3798        let req = make_request("CreateSecret", r#"{"Name": "doomed", "SecretString": "v"}"#);
3799        svc.handle(req).await.unwrap();
3800        let req = make_request("DeleteSecret", r#"{"SecretId": "doomed"}"#);
3801        svc.handle(req).await.unwrap();
3802
3803        // Without IncludePlannedDeletion
3804        let req = make_request("ListSecrets", "{}");
3805        let resp = svc.handle(req).await.unwrap();
3806        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3807        assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
3808
3809        // With IncludePlannedDeletion
3810        let body = serde_json::json!({"IncludePlannedDeletion": true});
3811        let req = make_request("ListSecrets", &body.to_string());
3812        let resp = svc.handle(req).await.unwrap();
3813        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3814        assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
3815    }
3816
3817    #[tokio::test]
3818    async fn list_secrets_pagination() {
3819        let state = make_state();
3820        let svc = SecretsManagerService::new(state);
3821
3822        for i in 0..5 {
3823            let body = serde_json::json!({
3824                "Name": format!("page-{i}"),
3825                "SecretString": "v",
3826            });
3827            let req = make_request("CreateSecret", &body.to_string());
3828            svc.handle(req).await.unwrap();
3829        }
3830
3831        let body = serde_json::json!({"MaxResults": 2});
3832        let req = make_request("ListSecrets", &body.to_string());
3833        let resp = svc.handle(req).await.unwrap();
3834        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3835        assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
3836        assert!(b["NextToken"].as_str().is_some());
3837    }
3838
3839    #[tokio::test]
3840    async fn list_secrets_invalid_filter_key() {
3841        let state = make_state();
3842        let svc = SecretsManagerService::new(state);
3843
3844        let body = serde_json::json!({
3845            "Filters": [{"Key": "bogus", "Values": ["x"]}]
3846        });
3847        let req = make_request("ListSecrets", &body.to_string());
3848        let err = expect_err(svc.handle(req).await);
3849        assert!(err.to_string().contains("ValidationException"));
3850    }
3851
3852    #[tokio::test]
3853    async fn list_secrets_empty_filter_values() {
3854        let state = make_state();
3855        let svc = SecretsManagerService::new(state);
3856
3857        let body = serde_json::json!({
3858            "Filters": [{"Key": "name", "Values": []}]
3859        });
3860        let req = make_request("ListSecrets", &body.to_string());
3861        let err = expect_err(svc.handle(req).await);
3862        assert!(err.to_string().contains("InvalidParameterException"));
3863    }
3864
3865    // ── ListSecretVersionIds ──
3866
3867    #[tokio::test]
3868    async fn list_secret_version_ids() {
3869        let state = make_state();
3870        let svc = SecretsManagerService::new(state);
3871
3872        let req = make_request(
3873            "CreateSecret",
3874            r#"{"Name": "multi-ver", "SecretString": "v1"}"#,
3875        );
3876        svc.handle(req).await.unwrap();
3877
3878        let req = make_request(
3879            "PutSecretValue",
3880            r#"{"SecretId": "multi-ver", "SecretString": "v2"}"#,
3881        );
3882        svc.handle(req).await.unwrap();
3883
3884        let req = make_request("ListSecretVersionIds", r#"{"SecretId": "multi-ver"}"#);
3885        let resp = svc.handle(req).await.unwrap();
3886        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3887        assert_eq!(b["Name"], "multi-ver");
3888        assert_eq!(b["Versions"].as_array().unwrap().len(), 2);
3889    }
3890
3891    // ── DescribeSecret with rotation info ──
3892
3893    #[tokio::test]
3894    async fn describe_secret_with_rotation_and_next_date() {
3895        let state = make_state();
3896        let svc = SecretsManagerService::new(state);
3897
3898        let req = make_request(
3899            "CreateSecret",
3900            r#"{"Name": "rot-desc", "SecretString": "pw"}"#,
3901        );
3902        svc.handle(req).await.unwrap();
3903
3904        let token = "e".repeat(32);
3905        let body = serde_json::json!({
3906            "SecretId": "rot-desc",
3907            "RotationRules": {"AutomaticallyAfterDays": 14},
3908            "ClientRequestToken": token,
3909        });
3910        let req = make_request("RotateSecret", &body.to_string());
3911        svc.handle(req).await.unwrap();
3912
3913        let req = make_request("DescribeSecret", r#"{"SecretId": "rot-desc"}"#);
3914        let resp = svc.handle(req).await.unwrap();
3915        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3916        assert_eq!(b["RotationEnabled"], true);
3917        assert!(b["LastRotatedDate"].as_f64().is_some());
3918        assert!(b["NextRotationDate"].as_f64().is_some());
3919        assert_eq!(b["RotationRules"]["AutomaticallyAfterDays"], 14);
3920    }
3921
3922    #[tokio::test]
3923    async fn describe_secret_deleted_shows_deletion_date() {
3924        let state = make_state();
3925        let svc = SecretsManagerService::new(state);
3926
3927        let req = make_request(
3928            "CreateSecret",
3929            r#"{"Name": "del-desc", "SecretString": "v"}"#,
3930        );
3931        svc.handle(req).await.unwrap();
3932        let req = make_request("DeleteSecret", r#"{"SecretId": "del-desc"}"#);
3933        svc.handle(req).await.unwrap();
3934
3935        let req = make_request("DescribeSecret", r#"{"SecretId": "del-desc"}"#);
3936        let resp = svc.handle(req).await.unwrap();
3937        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3938        assert!(b["DeletedDate"].as_f64().is_some());
3939    }
3940
3941    // ── BatchGetSecretValue edge cases ──
3942
3943    #[tokio::test]
3944    async fn batch_get_secret_value_both_list_and_filters() {
3945        let state = make_state();
3946        let svc = SecretsManagerService::new(state);
3947
3948        let body = serde_json::json!({
3949            "SecretIdList": ["a"],
3950            "Filters": [{"Key": "name", "Values": ["a"]}],
3951        });
3952        let req = make_request("BatchGetSecretValue", &body.to_string());
3953        let err = expect_err(svc.handle(req).await);
3954        assert!(err.to_string().contains("InvalidParameterException"));
3955    }
3956
3957    #[tokio::test]
3958    async fn batch_get_secret_value_max_results_without_filters() {
3959        let state = make_state();
3960        let svc = SecretsManagerService::new(state);
3961
3962        let body = serde_json::json!({
3963            "SecretIdList": ["a"],
3964            "MaxResults": 10,
3965        });
3966        let req = make_request("BatchGetSecretValue", &body.to_string());
3967        let err = expect_err(svc.handle(req).await);
3968        assert!(err.to_string().contains("InvalidParameterException"));
3969    }
3970
3971    #[tokio::test]
3972    async fn batch_get_secret_value_with_filters() {
3973        let state = make_state();
3974        let svc = SecretsManagerService::new(state);
3975
3976        for name in &["batch-f-a", "batch-f-b", "other-c"] {
3977            let body = serde_json::json!({"Name": name, "SecretString": "v"});
3978            let req = make_request("CreateSecret", &body.to_string());
3979            svc.handle(req).await.unwrap();
3980        }
3981
3982        let body = serde_json::json!({
3983            "Filters": [{"Key": "name", "Values": ["batch-f"]}],
3984        });
3985        let req = make_request("BatchGetSecretValue", &body.to_string());
3986        let resp = svc.handle(req).await.unwrap();
3987        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3988        assert_eq!(b["SecretValues"].as_array().unwrap().len(), 2);
3989    }
3990
3991    // ── RotateSecret validation ──
3992
3993    #[tokio::test]
3994    async fn rotate_secret_invalid_token_length() {
3995        let state = make_state();
3996        let svc = SecretsManagerService::new(state);
3997
3998        let req = make_request(
3999            "CreateSecret",
4000            r#"{"Name": "rot-val", "SecretString": "v"}"#,
4001        );
4002        svc.handle(req).await.unwrap();
4003
4004        let body = serde_json::json!({
4005            "SecretId": "rot-val",
4006            "ClientRequestToken": "short",
4007        });
4008        let req = make_request("RotateSecret", &body.to_string());
4009        let err = expect_err(svc.handle(req).await);
4010        assert!(err.to_string().contains("InvalidParameterException"));
4011    }
4012
4013    #[tokio::test]
4014    async fn rotate_secret_invalid_rules() {
4015        let state = make_state();
4016        let svc = SecretsManagerService::new(state);
4017
4018        let req = make_request(
4019            "CreateSecret",
4020            r#"{"Name": "rot-rules", "SecretString": "v"}"#,
4021        );
4022        svc.handle(req).await.unwrap();
4023
4024        let body = serde_json::json!({
4025            "SecretId": "rot-rules",
4026            "RotationRules": {"AutomaticallyAfterDays": 0},
4027        });
4028        let req = make_request("RotateSecret", &body.to_string());
4029        let err = expect_err(svc.handle(req).await);
4030        assert!(err.to_string().contains("InvalidParameterException"));
4031    }
4032
4033    #[tokio::test]
4034    async fn rotate_secret_on_deleted() {
4035        let state = make_state();
4036        let svc = SecretsManagerService::new(state);
4037
4038        let req = make_request(
4039            "CreateSecret",
4040            r#"{"Name": "rot-del", "SecretString": "v"}"#,
4041        );
4042        svc.handle(req).await.unwrap();
4043        let req = make_request("DeleteSecret", r#"{"SecretId": "rot-del"}"#);
4044        svc.handle(req).await.unwrap();
4045
4046        let body = serde_json::json!({"SecretId": "rot-del"});
4047        let req = make_request("RotateSecret", &body.to_string());
4048        let err = expect_err(svc.handle(req).await);
4049        assert!(err.to_string().contains("InvalidRequestException"));
4050    }
4051
4052    // ── CancelRotateSecret on deleted ──
4053
4054    #[tokio::test]
4055    async fn cancel_rotate_on_deleted() {
4056        let state = make_state();
4057        let svc = SecretsManagerService::new(state);
4058
4059        let req = make_request("CreateSecret", r#"{"Name": "cr-del", "SecretString": "v"}"#);
4060        svc.handle(req).await.unwrap();
4061        let req = make_request("DeleteSecret", r#"{"SecretId": "cr-del"}"#);
4062        svc.handle(req).await.unwrap();
4063
4064        let req = make_request("CancelRotateSecret", r#"{"SecretId": "cr-del"}"#);
4065        let err = expect_err(svc.handle(req).await);
4066        assert!(err.to_string().contains("InvalidRequestException"));
4067    }
4068
4069    // ── UpdateSecretVersionStage edge cases ──
4070
4071    #[tokio::test]
4072    async fn update_version_stage_missing_remove_from() {
4073        let state = make_state();
4074        let svc = SecretsManagerService::new(state.clone());
4075
4076        let req = make_request(
4077            "CreateSecret",
4078            r#"{"Name": "stage-err", "SecretString": "v1"}"#,
4079        );
4080        svc.handle(req).await.unwrap();
4081
4082        let req = make_request(
4083            "PutSecretValue",
4084            r#"{"SecretId": "stage-err", "SecretString": "v2"}"#,
4085        );
4086        svc.handle(req).await.unwrap();
4087
4088        let new_vid = {
4089            let _accts = state.read();
4090            let s = _accts.default_ref();
4091            let secret = s.secrets.get("stage-err").unwrap();
4092            secret
4093                .versions
4094                .iter()
4095                .find(|(_, v)| v.stages.contains(&"AWSPREVIOUS".to_string()))
4096                .map(|(id, _)| id.clone())
4097                .unwrap()
4098        };
4099
4100        // Move AWSCURRENT without RemoveFromVersionId -> error
4101        let body = serde_json::json!({
4102            "SecretId": "stage-err",
4103            "VersionStage": "AWSCURRENT",
4104            "MoveToVersionId": new_vid,
4105        });
4106        let req = make_request("UpdateSecretVersionStage", &body.to_string());
4107        let err = expect_err(svc.handle(req).await);
4108        assert!(err.to_string().contains("InvalidParameterException"));
4109    }
4110
4111    // ── Find secret by ARN ──
4112
4113    #[tokio::test]
4114    async fn find_secret_by_arn() {
4115        let state = make_state();
4116        let svc = SecretsManagerService::new(state);
4117
4118        let req = make_request(
4119            "CreateSecret",
4120            r#"{"Name": "arn-lookup", "SecretString": "v"}"#,
4121        );
4122        let resp = svc.handle(req).await.unwrap();
4123        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4124        let arn = b["ARN"].as_str().unwrap();
4125
4126        // Lookup by full ARN
4127        let body = serde_json::json!({"SecretId": arn});
4128        let req = make_request("GetSecretValue", &body.to_string());
4129        let resp = svc.handle(req).await.unwrap();
4130        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4131        assert_eq!(b["SecretString"], "v");
4132    }
4133
4134    #[tokio::test]
4135    async fn find_secret_by_partial_arn() {
4136        let state = make_state();
4137        let svc = SecretsManagerService::new(state);
4138
4139        let req = make_request(
4140            "CreateSecret",
4141            r#"{"Name": "partial-arn", "SecretString": "v"}"#,
4142        );
4143        svc.handle(req).await.unwrap();
4144
4145        // Partial ARN (without the random suffix)
4146        let partial = "arn:aws:secretsmanager:us-east-1:123456789012:secret:partial-arn";
4147        let body = serde_json::json!({"SecretId": partial});
4148        let req = make_request("GetSecretValue", &body.to_string());
4149        let resp = svc.handle(req).await.unwrap();
4150        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4151        assert_eq!(b["SecretString"], "v");
4152    }
4153
4154    // ── ValidateResourcePolicy edge cases ──
4155
4156    #[tokio::test]
4157    async fn validate_resource_policy_with_secret_id() {
4158        let state = make_state();
4159        let svc = SecretsManagerService::new(state);
4160
4161        let req = make_request(
4162            "CreateSecret",
4163            r#"{"Name": "pol-val", "SecretString": "v"}"#,
4164        );
4165        svc.handle(req).await.unwrap();
4166
4167        let body = serde_json::json!({
4168            "SecretId": "pol-val",
4169            "ResourcePolicy": r#"{"Version":"2012-10-17","Statement":[]}"#,
4170        });
4171        let req = make_request("ValidateResourcePolicy", &body.to_string());
4172        let resp = svc.handle(req).await.unwrap();
4173        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4174        assert_eq!(b["PolicyValidationPassed"], true);
4175    }
4176
4177    #[tokio::test]
4178    async fn validate_resource_policy_nonexistent_secret() {
4179        let state = make_state();
4180        let svc = SecretsManagerService::new(state);
4181
4182        let body = serde_json::json!({
4183            "SecretId": "ghost",
4184            "ResourcePolicy": r#"{"Version":"2012-10-17","Statement":[]}"#,
4185        });
4186        let req = make_request("ValidateResourcePolicy", &body.to_string());
4187        let err = expect_err(svc.handle(req).await);
4188        assert!(err.to_string().contains("ResourceNotFoundException"));
4189    }
4190
4191    // ── Tag operations edge cases ──
4192
4193    #[tokio::test]
4194    async fn tag_resource_updates_existing_tag() {
4195        let state = make_state();
4196        let svc = SecretsManagerService::new(state);
4197
4198        let body = serde_json::json!({
4199            "Name": "tag-upd",
4200            "SecretString": "v",
4201            "Tags": [{"Key": "env", "Value": "dev"}],
4202        });
4203        let req = make_request("CreateSecret", &body.to_string());
4204        svc.handle(req).await.unwrap();
4205
4206        // Update existing tag value
4207        let body = serde_json::json!({
4208            "SecretId": "tag-upd",
4209            "Tags": [{"Key": "env", "Value": "prod"}],
4210        });
4211        let req = make_request("TagResource", &body.to_string());
4212        svc.handle(req).await.unwrap();
4213
4214        let req = make_request("DescribeSecret", r#"{"SecretId": "tag-upd"}"#);
4215        let resp = svc.handle(req).await.unwrap();
4216        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4217        let tags = b["Tags"].as_array().unwrap();
4218        assert_eq!(tags.len(), 1);
4219        assert_eq!(tags[0]["Value"], "prod");
4220    }
4221
4222    // ── Unsupported action ──
4223
4224    #[tokio::test]
4225    async fn unsupported_action_returns_error() {
4226        let state = make_state();
4227        let svc = SecretsManagerService::new(state);
4228
4229        let req = make_request("BogusAction", "{}");
4230        let err = expect_err(svc.handle(req).await);
4231        assert!(err.to_string().contains("BogusAction"));
4232    }
4233
4234    // ── Helper function tests ──
4235
4236    #[test]
4237    fn test_split_words_basic() {
4238        assert_eq!(split_words("hello"), vec!["hello"]);
4239        assert_eq!(split_words("HelloWorld"), vec!["Hello", "World"]);
4240        assert_eq!(split_words("my/secret/name"), vec!["my", "secret", "name"]);
4241        assert_eq!(split_words("my-secret-name"), vec!["my", "secret", "name"]);
4242        assert_eq!(split_words("my_secret_name"), vec!["my", "secret", "name"]);
4243    }
4244
4245    #[test]
4246    fn test_split_words_multiple_delimiters() {
4247        // Multiple different special chars -> don't split
4248        assert_eq!(split_words("my/secret-name"), vec!["my/secret-name"]);
4249    }
4250
4251    #[test]
4252    fn test_split_words_with_spaces() {
4253        let words = split_words("hello world");
4254        assert_eq!(words, vec!["hello", "world"]);
4255    }
4256
4257    #[test]
4258    fn test_match_pattern_prefix() {
4259        assert!(match_pattern("prod", "production", true, true));
4260        assert!(!match_pattern("Prod", "production", true, true));
4261        assert!(match_pattern("Prod", "production", true, false));
4262    }
4263
4264    #[test]
4265    fn test_match_pattern_word() {
4266        assert!(match_pattern("hello", "HelloWorld", false, false));
4267        assert!(match_pattern("world", "HelloWorld", false, false));
4268    }
4269
4270    #[test]
4271    fn test_matcher_negation() {
4272        // Negated: "!prod" matches strings that DON'T match "prod"
4273        assert!(matcher(&["!prod"], &["staging"], true, true));
4274    }
4275
4276    #[test]
4277    fn test_base64_roundtrip() {
4278        let data = b"Hello, World!";
4279        let encoded = base64_encode(data);
4280        let decoded = base64_decode(&encoded).unwrap();
4281        assert_eq!(&decoded, data);
4282    }
4283
4284    #[test]
4285    fn test_base64_decode_invalid() {
4286        // Invalid base64 char
4287        assert!(base64_decode("!!!").is_none());
4288    }
4289
4290    #[test]
4291    fn test_check_version_idempotency() {
4292        let mut versions = HashMap::new();
4293        versions.insert(
4294            "v1".to_string(),
4295            SecretVersion {
4296                version_id: "v1".to_string(),
4297                secret_string: Some("hello".to_string()),
4298                secret_binary: None,
4299                stages: vec!["AWSCURRENT".to_string()],
4300                created_at: Utc::now(),
4301            },
4302        );
4303
4304        // Not found
4305        assert!(matches!(
4306            check_secret_version_idempotency(&versions, "v2", &Some("x".to_string()), &None),
4307            VersionIdempotency::NotFound
4308        ));
4309
4310        // Match
4311        assert!(matches!(
4312            check_secret_version_idempotency(&versions, "v1", &Some("hello".to_string()), &None),
4313            VersionIdempotency::Match
4314        ));
4315
4316        // Conflict
4317        assert!(matches!(
4318            check_secret_version_idempotency(
4319                &versions,
4320                "v1",
4321                &Some("different".to_string()),
4322                &None
4323            ),
4324            VersionIdempotency::Conflict
4325        ));
4326    }
4327
4328    #[test]
4329    fn test_is_mutating_action() {
4330        assert!(is_mutating_action("CreateSecret"));
4331        assert!(is_mutating_action("DeleteSecret"));
4332        assert!(is_mutating_action("TagResource"));
4333        assert!(!is_mutating_action("GetSecretValue"));
4334        assert!(!is_mutating_action("ListSecrets"));
4335        assert!(!is_mutating_action("DescribeSecret"));
4336    }
4337
4338    #[test]
4339    fn test_parse_tags_empty() {
4340        let val = serde_json::json!(null);
4341        assert_eq!(parse_tags(&val), vec![]);
4342    }
4343
4344    #[test]
4345    fn test_tags_to_json_roundtrip() {
4346        let tags = vec![
4347            ("k1".to_string(), "v1".to_string()),
4348            ("k2".to_string(), "v2".to_string()),
4349        ];
4350        let json = tags_to_json(&tags);
4351        assert_eq!(json.len(), 2);
4352        assert_eq!(json[0]["Key"], "k1");
4353        assert_eq!(json[1]["Value"], "v2");
4354    }
4355
4356    #[test]
4357    fn test_filter_name_prefix() {
4358        let secret = Secret {
4359            name: "prod/database".to_string(),
4360            arn: "arn".to_string(),
4361            description: None,
4362            kms_key_id: None,
4363            versions: HashMap::new(),
4364            current_version_id: None,
4365            tags: vec![],
4366            tags_ever_set: false,
4367            deleted: false,
4368            deletion_date: None,
4369            created_at: Utc::now(),
4370            last_changed_at: Utc::now(),
4371            last_accessed_at: None,
4372            rotation_enabled: None,
4373            rotation_lambda_arn: None,
4374            rotation_rules: None,
4375            last_rotated_at: None,
4376            resource_policy: None,
4377        };
4378        assert!(filter_name(&secret, &["prod/"]));
4379        assert!(!filter_name(&secret, &["staging/"]));
4380    }
4381
4382    #[test]
4383    fn test_filter_tag_value() {
4384        let secret = Secret {
4385            name: "s".to_string(),
4386            arn: "arn".to_string(),
4387            description: None,
4388            kms_key_id: None,
4389            versions: HashMap::new(),
4390            current_version_id: None,
4391            tags: vec![("env".to_string(), "production".to_string())],
4392            tags_ever_set: true,
4393            deleted: false,
4394            deletion_date: None,
4395            created_at: Utc::now(),
4396            last_changed_at: Utc::now(),
4397            last_accessed_at: None,
4398            rotation_enabled: None,
4399            rotation_lambda_arn: None,
4400            rotation_rules: None,
4401            last_rotated_at: None,
4402            resource_policy: None,
4403        };
4404        assert!(filter_tag_value(&secret, &["prod"]));
4405        assert!(!filter_tag_value(&secret, &["staging"]));
4406    }
4407
4408    #[test]
4409    fn test_filter_all_searches_name_desc_tags() {
4410        let secret = Secret {
4411            name: "my-secret".to_string(),
4412            arn: "arn".to_string(),
4413            description: Some("important database".to_string()),
4414            kms_key_id: None,
4415            versions: HashMap::new(),
4416            current_version_id: None,
4417            tags: vec![("team".to_string(), "backend".to_string())],
4418            tags_ever_set: true,
4419            deleted: false,
4420            deletion_date: None,
4421            created_at: Utc::now(),
4422            last_changed_at: Utc::now(),
4423            last_accessed_at: None,
4424            rotation_enabled: None,
4425            rotation_lambda_arn: None,
4426            rotation_rules: None,
4427            last_rotated_at: None,
4428            resource_policy: None,
4429        };
4430        // Matches name
4431        assert!(filter_all(&secret, &["my"]));
4432        // Matches description
4433        assert!(filter_all(&secret, &["database"]));
4434        // Matches tag key
4435        assert!(filter_all(&secret, &["team"]));
4436        // Matches tag value
4437        assert!(filter_all(&secret, &["backend"]));
4438        // No match
4439        assert!(!filter_all(&secret, &["zzzz"]));
4440    }
4441}