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