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            path_segments: vec![],
2433            raw_path: "/".to_string(),
2434            raw_query: String::new(),
2435            method: Method::POST,
2436            is_query_protocol: false,
2437            access_key_id: None,
2438            principal: None,
2439        }
2440    }
2441
2442    #[tokio::test]
2443    async fn test_create_and_get_secret() {
2444        let state = make_state();
2445        let svc = SecretsManagerService::new(state);
2446
2447        let req = make_request(
2448            "CreateSecret",
2449            r#"{"Name": "test/secret", "SecretString": "mysecretvalue"}"#,
2450        );
2451        let resp = svc.handle(req).await.unwrap();
2452        assert_eq!(resp.status, StatusCode::OK);
2453        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2454        assert_eq!(body["Name"], "test/secret");
2455        assert!(body["ARN"].as_str().unwrap().contains("test/secret"));
2456
2457        let req = make_request("GetSecretValue", r#"{"SecretId": "test/secret"}"#);
2458        let resp = svc.handle(req).await.unwrap();
2459        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2460        assert_eq!(body["SecretString"], "mysecretvalue");
2461    }
2462
2463    #[tokio::test]
2464    async fn test_create_secret_without_value() {
2465        let state = make_state();
2466        let svc = SecretsManagerService::new(state);
2467
2468        let req = make_request("CreateSecret", r#"{"Name": "empty-secret"}"#);
2469        let resp = svc.handle(req).await.unwrap();
2470        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2471        assert_eq!(body["Name"], "empty-secret");
2472        assert!(body.get("VersionId").is_none());
2473    }
2474
2475    #[tokio::test]
2476    async fn test_put_secret_value_creates_version() {
2477        let state = make_state();
2478        let svc = SecretsManagerService::new(state);
2479
2480        let req = make_request(
2481            "CreateSecret",
2482            r#"{"Name": "versioned", "SecretString": "v1"}"#,
2483        );
2484        svc.handle(req).await.unwrap();
2485
2486        let req = make_request(
2487            "PutSecretValue",
2488            r#"{"SecretId": "versioned", "SecretString": "v2"}"#,
2489        );
2490        let resp = svc.handle(req).await.unwrap();
2491        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2492        assert_eq!(body["Name"], "versioned");
2493
2494        // Get should return v2
2495        let req = make_request("GetSecretValue", r#"{"SecretId": "versioned"}"#);
2496        let resp = svc.handle(req).await.unwrap();
2497        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2498        assert_eq!(body["SecretString"], "v2");
2499    }
2500
2501    #[tokio::test]
2502    async fn test_delete_and_restore_secret() {
2503        let state = make_state();
2504        let svc = SecretsManagerService::new(state);
2505
2506        let req = make_request(
2507            "CreateSecret",
2508            r#"{"Name": "deleteme", "SecretString": "value"}"#,
2509        );
2510        svc.handle(req).await.unwrap();
2511
2512        // Delete (soft)
2513        let req = make_request("DeleteSecret", r#"{"SecretId": "deleteme"}"#);
2514        let resp = svc.handle(req).await.unwrap();
2515        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2516        assert!(body["DeletionDate"].as_f64().is_some());
2517
2518        // GetSecretValue should fail
2519        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2520        assert!(svc.handle(req).await.is_err());
2521
2522        // Restore
2523        let req = make_request("RestoreSecret", r#"{"SecretId": "deleteme"}"#);
2524        let resp = svc.handle(req).await.unwrap();
2525        assert_eq!(resp.status, StatusCode::OK);
2526
2527        // GetSecretValue should work again
2528        let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
2529        let resp = svc.handle(req).await.unwrap();
2530        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2531        assert_eq!(body["SecretString"], "value");
2532    }
2533
2534    #[tokio::test]
2535    async fn test_list_secrets() {
2536        let state = make_state();
2537        let svc = SecretsManagerService::new(state);
2538
2539        for name in &["alpha", "beta", "gamma"] {
2540            let req = make_request(
2541                "CreateSecret",
2542                &format!(r#"{{"Name": "{name}", "SecretString": "val"}}"#),
2543            );
2544            svc.handle(req).await.unwrap();
2545        }
2546
2547        let req = make_request("ListSecrets", "{}");
2548        let resp = svc.handle(req).await.unwrap();
2549        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2550        assert_eq!(body["SecretList"].as_array().unwrap().len(), 3);
2551    }
2552
2553    #[tokio::test]
2554    async fn test_tags() {
2555        let state = make_state();
2556        let svc = SecretsManagerService::new(state);
2557
2558        let req = make_request(
2559            "CreateSecret",
2560            r#"{"Name": "tagged", "SecretString": "val"}"#,
2561        );
2562        svc.handle(req).await.unwrap();
2563
2564        let req = make_request(
2565            "TagResource",
2566            r#"{"SecretId": "tagged", "Tags": [{"Key": "env", "Value": "prod"}]}"#,
2567        );
2568        svc.handle(req).await.unwrap();
2569
2570        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2571        let resp = svc.handle(req).await.unwrap();
2572        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2573        let tags = body["Tags"].as_array().unwrap();
2574        assert!(tags
2575            .iter()
2576            .any(|t| t["Key"] == "env" && t["Value"] == "prod"));
2577
2578        let req = make_request(
2579            "UntagResource",
2580            r#"{"SecretId": "tagged", "TagKeys": ["env"]}"#,
2581        );
2582        svc.handle(req).await.unwrap();
2583
2584        let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
2585        let resp = svc.handle(req).await.unwrap();
2586        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2587        // Tags should be empty list after untagging all (but key present since tags were set)
2588        assert_eq!(body["Tags"].as_array().unwrap().len(), 0);
2589    }
2590
2591    #[tokio::test]
2592    async fn test_get_random_password() {
2593        let state = make_state();
2594        let svc = SecretsManagerService::new(state);
2595
2596        let req = make_request("GetRandomPassword", "{}");
2597        let resp = svc.handle(req).await.unwrap();
2598        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2599        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 32);
2600    }
2601
2602    #[tokio::test]
2603    async fn test_replication_ops_return_arn() {
2604        let state = make_state();
2605        let svc = SecretsManagerService::new(state);
2606
2607        let req = make_request(
2608            "CreateSecret",
2609            r#"{"Name": "repl-secret", "SecretString": "val"}"#,
2610        );
2611        let resp = svc.handle(req).await.unwrap();
2612        let create_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2613        let expected_arn = create_body["ARN"].as_str().unwrap();
2614
2615        for action in &[
2616            "ReplicateSecretToRegions",
2617            "RemoveRegionsFromReplication",
2618            "StopReplicationToReplica",
2619        ] {
2620            let req = make_request(action, r#"{"SecretId": "repl-secret"}"#);
2621            let resp = svc.handle(req).await.unwrap();
2622            let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2623            assert_eq!(
2624                body["ARN"].as_str().unwrap(),
2625                expected_arn,
2626                "{action} should return the secret's actual ARN"
2627            );
2628        }
2629    }
2630
2631    #[tokio::test]
2632    async fn test_secret_id_length_validation() {
2633        let state = make_state();
2634        let svc = SecretsManagerService::new(state);
2635
2636        // SecretId too long (> 2048)
2637        let long_id = "x".repeat(2049);
2638        let req = make_request("GetSecretValue", &format!(r#"{{"SecretId": "{long_id}"}}"#));
2639        match svc.handle(req).await {
2640            Err(e) => assert!(e.to_string().contains("ValidationException")),
2641            Ok(_) => panic!("expected ValidationException"),
2642        }
2643    }
2644
2645    #[tokio::test]
2646    async fn test_name_length_validation() {
2647        let state = make_state();
2648        let svc = SecretsManagerService::new(state);
2649
2650        // Name too long (> 512)
2651        let long_name = "x".repeat(513);
2652        let req = make_request(
2653            "CreateSecret",
2654            &format!(r#"{{"Name": "{long_name}", "SecretString": "val"}}"#),
2655        );
2656        match svc.handle(req).await {
2657            Err(e) => assert!(e.to_string().contains("ValidationException")),
2658            Ok(_) => panic!("expected ValidationException"),
2659        }
2660    }
2661
2662    #[tokio::test]
2663    async fn test_next_token_length_validation() {
2664        let state = make_state();
2665        let svc = SecretsManagerService::new(state);
2666
2667        // NextToken too long (> 4096)
2668        let long_token = "x".repeat(4097);
2669        let req = make_request(
2670            "ListSecrets",
2671            &format!(r#"{{"NextToken": "{long_token}"}}"#),
2672        );
2673        match svc.handle(req).await {
2674            Err(e) => assert!(e.to_string().contains("ValidationException")),
2675            Ok(_) => panic!("expected ValidationException"),
2676        }
2677    }
2678
2679    #[tokio::test]
2680    async fn test_client_request_token_length_validation() {
2681        let state = make_state();
2682        let svc = SecretsManagerService::new(state);
2683
2684        // ClientRequestToken too short (< 32)
2685        let req = make_request(
2686            "CreateSecret",
2687            r#"{"Name": "test", "SecretString": "val", "ClientRequestToken": "short"}"#,
2688        );
2689        match svc.handle(req).await {
2690            Err(e) => assert!(e.to_string().contains("ValidationException")),
2691            Ok(_) => panic!("expected ValidationException"),
2692        }
2693    }
2694
2695    #[tokio::test]
2696    async fn test_rotate_secret_with_lambda_creates_pending_version() {
2697        let state = make_state();
2698        let svc = SecretsManagerService::new(state.clone());
2699
2700        // Create a secret
2701        let req = make_request(
2702            "CreateSecret",
2703            r#"{"Name": "rotate-me", "SecretString": "old-password"}"#,
2704        );
2705        svc.handle(req).await.unwrap();
2706
2707        // Rotate with a Lambda ARN
2708        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2709        let body = serde_json::json!({
2710            "SecretId": "rotate-me",
2711            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:rotator",
2712            "ClientRequestToken": token,
2713        });
2714        let req = make_request("RotateSecret", &body.to_string());
2715        let resp = svc.handle(req).await.unwrap();
2716        assert_eq!(resp.status, StatusCode::OK);
2717        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2718        assert_eq!(resp_body["VersionId"], token);
2719
2720        // Real AWS leaves the AWSPENDING version creation to the rotation
2721        // Lambda's createSecret step, so we should NOT pre-create it. Verify
2722        // that no version with the rotation token exists yet.
2723        let _accts = state.read();
2724        let s = _accts.default_ref();
2725        let secret = s.secrets.get("rotate-me").unwrap();
2726        assert!(
2727            !secret.versions.contains_key(token),
2728            "AWSPENDING version must not be pre-created; the rotation Lambda creates it"
2729        );
2730
2731        // Verify rotation config was set
2732        assert_eq!(
2733            secret.rotation_lambda_arn.as_deref(),
2734            Some("arn:aws:lambda:us-east-1:123456789012:function:rotator")
2735        );
2736        assert_eq!(secret.rotation_enabled, Some(true));
2737    }
2738
2739    #[tokio::test]
2740    async fn test_rotate_secret_without_lambda_promotes_directly() {
2741        let state = make_state();
2742        let svc = SecretsManagerService::new(state.clone());
2743
2744        // Create a secret
2745        let req = make_request(
2746            "CreateSecret",
2747            r#"{"Name": "rotate-no-lambda", "SecretString": "value1"}"#,
2748        );
2749        svc.handle(req).await.unwrap();
2750
2751        // Rotate without Lambda ARN
2752        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2753        let body = serde_json::json!({
2754            "SecretId": "rotate-no-lambda",
2755            "ClientRequestToken": token,
2756        });
2757        let req = make_request("RotateSecret", &body.to_string());
2758        svc.handle(req).await.unwrap();
2759
2760        // Verify the new version is AWSCURRENT (no pending)
2761        let _accts = state.read();
2762        let s = _accts.default_ref();
2763        let secret = s.secrets.get("rotate-no-lambda").unwrap();
2764        let new_ver = secret.versions.get(token).unwrap();
2765        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2766        assert_eq!(secret.current_version_id.as_deref(), Some(token));
2767    }
2768
2769    #[tokio::test]
2770    async fn test_rotate_secret_stores_rotation_config() {
2771        let state = make_state();
2772        let svc = SecretsManagerService::new(state.clone());
2773
2774        let req = make_request(
2775            "CreateSecret",
2776            r#"{"Name": "rot-cfg", "SecretString": "pw"}"#,
2777        );
2778        svc.handle(req).await.unwrap();
2779
2780        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2781        let body = serde_json::json!({
2782            "SecretId": "rot-cfg",
2783            "RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-rotator",
2784            "RotationRules": { "AutomaticallyAfterDays": 30 },
2785            "ClientRequestToken": token,
2786        });
2787        let req = make_request("RotateSecret", &body.to_string());
2788        let resp = svc.handle(req).await.unwrap();
2789        assert_eq!(resp.status, StatusCode::OK);
2790
2791        let _accts = state.read();
2792        let s = _accts.default_ref();
2793        let secret = s.secrets.get("rot-cfg").unwrap();
2794        assert_eq!(secret.rotation_enabled, Some(true));
2795        assert_eq!(
2796            secret.rotation_lambda_arn.as_deref(),
2797            Some("arn:aws:lambda:us-east-1:123456789012:function:my-rotator")
2798        );
2799        assert!(secret.last_rotated_at.is_some());
2800        let rules = secret.rotation_rules.as_ref().unwrap();
2801        assert_eq!(rules.automatically_after_days, Some(30));
2802
2803        // The AWSPENDING version is created by the rotation Lambda's
2804        // createSecret step, not by RotateSecret itself, so verify that no
2805        // version with this token exists yet.
2806        assert!(!secret.versions.contains_key(token));
2807    }
2808
2809    #[tokio::test]
2810    async fn test_rotate_secret_version_stages_change() {
2811        let state = make_state();
2812        let svc = SecretsManagerService::new(state.clone());
2813
2814        let req = make_request(
2815            "CreateSecret",
2816            r#"{"Name": "rot-stages", "SecretString": "original"}"#,
2817        );
2818        svc.handle(req).await.unwrap();
2819
2820        // Get original version id
2821        let original_vid = {
2822            let _accts = state.read();
2823            let s = _accts.default_ref();
2824            let secret = s.secrets.get("rot-stages").unwrap();
2825            secret.current_version_id.clone().unwrap()
2826        };
2827
2828        // Rotate without Lambda (simple rotation)
2829        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2830        let body = serde_json::json!({
2831            "SecretId": "rot-stages",
2832            "ClientRequestToken": token,
2833        });
2834        let req = make_request("RotateSecret", &body.to_string());
2835        svc.handle(req).await.unwrap();
2836
2837        let _accts = state.read();
2838        let s = _accts.default_ref();
2839        let secret = s.secrets.get("rot-stages").unwrap();
2840
2841        // New version should be AWSCURRENT
2842        let new_ver = secret.versions.get(token).unwrap();
2843        assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
2844
2845        // Old version should be AWSPREVIOUS
2846        let old_ver = secret.versions.get(&original_vid).unwrap();
2847        assert!(old_ver.stages.contains(&"AWSPREVIOUS".to_string()));
2848        assert!(!old_ver.stages.contains(&"AWSCURRENT".to_string()));
2849    }
2850
2851    #[tokio::test]
2852    async fn test_cancel_rotate_secret() {
2853        let state = make_state();
2854        let svc = SecretsManagerService::new(state.clone());
2855
2856        let req = make_request(
2857            "CreateSecret",
2858            r#"{"Name": "cancel-rot", "SecretString": "pw"}"#,
2859        );
2860        svc.handle(req).await.unwrap();
2861
2862        // Enable rotation first
2863        let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
2864        let body = serde_json::json!({
2865            "SecretId": "cancel-rot",
2866            "ClientRequestToken": token,
2867        });
2868        let req = make_request("RotateSecret", &body.to_string());
2869        svc.handle(req).await.unwrap();
2870
2871        // Verify rotation is enabled
2872        {
2873            let _accts = state.read();
2874            let s = _accts.default_ref();
2875            let secret = s.secrets.get("cancel-rot").unwrap();
2876            assert_eq!(secret.rotation_enabled, Some(true));
2877        }
2878
2879        // Cancel rotation
2880        let req = make_request("CancelRotateSecret", r#"{"SecretId": "cancel-rot"}"#);
2881        let resp = svc.handle(req).await.unwrap();
2882        assert_eq!(resp.status, StatusCode::OK);
2883        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2884        assert_eq!(body["Name"], "cancel-rot");
2885
2886        // Verify rotation is disabled
2887        let _accts = state.read();
2888        let s = _accts.default_ref();
2889        let secret = s.secrets.get("cancel-rot").unwrap();
2890        assert_eq!(secret.rotation_enabled, Some(false));
2891    }
2892
2893    #[tokio::test]
2894    async fn test_cancel_rotate_secret_fails_when_not_enabled() {
2895        let state = make_state();
2896        let svc = SecretsManagerService::new(state);
2897
2898        let req = make_request(
2899            "CreateSecret",
2900            r#"{"Name": "no-rot", "SecretString": "pw"}"#,
2901        );
2902        svc.handle(req).await.unwrap();
2903
2904        let req = make_request("CancelRotateSecret", r#"{"SecretId": "no-rot"}"#);
2905        let result = svc.handle(req).await;
2906        assert!(result.is_err());
2907    }
2908
2909    #[tokio::test]
2910    async fn test_batch_get_secret_value_multiple() {
2911        let state = make_state();
2912        let svc = SecretsManagerService::new(state);
2913
2914        for (name, val) in &[("batch-a", "va"), ("batch-b", "vb"), ("batch-c", "vc")] {
2915            let req = make_request(
2916                "CreateSecret",
2917                &format!(r#"{{"Name": "{name}", "SecretString": "{val}"}}"#),
2918            );
2919            svc.handle(req).await.unwrap();
2920        }
2921
2922        let body = serde_json::json!({
2923            "SecretIdList": ["batch-a", "batch-b", "batch-c"]
2924        });
2925        let req = make_request("BatchGetSecretValue", &body.to_string());
2926        let resp = svc.handle(req).await.unwrap();
2927        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2928
2929        let values = resp_body["SecretValues"].as_array().unwrap();
2930        assert_eq!(values.len(), 3);
2931
2932        // Verify each secret has the right value
2933        let names: Vec<&str> = values.iter().map(|v| v["Name"].as_str().unwrap()).collect();
2934        assert!(names.contains(&"batch-a"));
2935        assert!(names.contains(&"batch-b"));
2936        assert!(names.contains(&"batch-c"));
2937
2938        // Verify no errors
2939        assert!(resp_body.get("Errors").is_none());
2940    }
2941
2942    #[tokio::test]
2943    async fn test_batch_get_secret_value_with_missing() {
2944        let state = make_state();
2945        let svc = SecretsManagerService::new(state);
2946
2947        let req = make_request(
2948            "CreateSecret",
2949            r#"{"Name": "exists", "SecretString": "val"}"#,
2950        );
2951        svc.handle(req).await.unwrap();
2952
2953        let body = serde_json::json!({
2954            "SecretIdList": ["exists", "nonexistent"]
2955        });
2956        let req = make_request("BatchGetSecretValue", &body.to_string());
2957        let resp = svc.handle(req).await.unwrap();
2958        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2959
2960        let values = resp_body["SecretValues"].as_array().unwrap();
2961        assert_eq!(values.len(), 1);
2962        assert_eq!(values[0]["Name"], "exists");
2963
2964        let errors = resp_body["Errors"].as_array().unwrap();
2965        assert_eq!(errors.len(), 1);
2966        assert_eq!(errors[0]["SecretId"], "nonexistent");
2967        assert_eq!(errors[0]["ErrorCode"], "ResourceNotFoundException");
2968    }
2969
2970    #[tokio::test]
2971    async fn test_update_secret_changes_description_and_kms() {
2972        let state = make_state();
2973        let svc = SecretsManagerService::new(state);
2974
2975        let req = make_request(
2976            "CreateSecret",
2977            r#"{"Name": "updatable", "SecretString": "val", "Description": "old desc"}"#,
2978        );
2979        svc.handle(req).await.unwrap();
2980
2981        // Update description and KmsKeyId
2982        let body = serde_json::json!({
2983            "SecretId": "updatable",
2984            "Description": "new desc",
2985            "KmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/my-key"
2986        });
2987        let req = make_request("UpdateSecret", &body.to_string());
2988        let resp = svc.handle(req).await.unwrap();
2989        assert_eq!(resp.status, StatusCode::OK);
2990        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2991        assert_eq!(resp_body["Name"], "updatable");
2992        // No VersionId since no new value was provided
2993        assert!(resp_body.get("VersionId").is_none());
2994
2995        // Describe to verify changes
2996        let req = make_request("DescribeSecret", r#"{"SecretId": "updatable"}"#);
2997        let resp = svc.handle(req).await.unwrap();
2998        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2999        assert_eq!(body["Description"], "new desc");
3000        assert_eq!(
3001            body["KmsKeyId"],
3002            "arn:aws:kms:us-east-1:123456789012:key/my-key"
3003        );
3004    }
3005
3006    #[tokio::test]
3007    async fn test_update_secret_with_new_value() {
3008        let state = make_state();
3009        let svc = SecretsManagerService::new(state);
3010
3011        let req = make_request(
3012            "CreateSecret",
3013            r#"{"Name": "upd-val", "SecretString": "old"}"#,
3014        );
3015        svc.handle(req).await.unwrap();
3016
3017        // Update with a new value
3018        let body = serde_json::json!({
3019            "SecretId": "upd-val",
3020            "SecretString": "new-value"
3021        });
3022        let req = make_request("UpdateSecret", &body.to_string());
3023        let resp = svc.handle(req).await.unwrap();
3024        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3025        assert!(resp_body["VersionId"].as_str().is_some());
3026
3027        // Get should return new value
3028        let req = make_request("GetSecretValue", r#"{"SecretId": "upd-val"}"#);
3029        let resp = svc.handle(req).await.unwrap();
3030        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3031        assert_eq!(body["SecretString"], "new-value");
3032    }
3033
3034    #[tokio::test]
3035    async fn test_get_random_password_custom_length() {
3036        let state = make_state();
3037        let svc = SecretsManagerService::new(state);
3038
3039        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 64}"#);
3040        let resp = svc.handle(req).await.unwrap();
3041        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3042        assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 64);
3043    }
3044
3045    #[tokio::test]
3046    async fn test_get_random_password_exclude_chars() {
3047        let state = make_state();
3048        let svc = SecretsManagerService::new(state);
3049
3050        let req = make_request(
3051            "GetRandomPassword",
3052            r#"{"PasswordLength": 100, "ExcludeCharacters": "abc123"}"#,
3053        );
3054        let resp = svc.handle(req).await.unwrap();
3055        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3056        let password = body["RandomPassword"].as_str().unwrap();
3057        assert_eq!(password.len(), 100);
3058        assert!(!password.contains('a'));
3059        assert!(!password.contains('b'));
3060        assert!(!password.contains('c'));
3061        assert!(!password.contains('1'));
3062        assert!(!password.contains('2'));
3063        assert!(!password.contains('3'));
3064    }
3065
3066    #[tokio::test]
3067    async fn test_get_random_password_exclude_types() {
3068        let state = make_state();
3069        let svc = SecretsManagerService::new(state);
3070
3071        // Exclude everything except lowercase
3072        let body = serde_json::json!({
3073            "PasswordLength": 50,
3074            "ExcludeUppercase": true,
3075            "ExcludeNumbers": true,
3076            "ExcludePunctuation": true,
3077            "RequireEachIncludedType": false,
3078        });
3079        let req = make_request("GetRandomPassword", &body.to_string());
3080        let resp = svc.handle(req).await.unwrap();
3081        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3082        let password = resp_body["RandomPassword"].as_str().unwrap();
3083        assert_eq!(password.len(), 50);
3084        assert!(password.chars().all(|c| c.is_ascii_lowercase()));
3085    }
3086
3087    #[tokio::test]
3088    async fn test_get_random_password_too_short() {
3089        let state = make_state();
3090        let svc = SecretsManagerService::new(state);
3091
3092        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 3}"#);
3093        assert!(svc.handle(req).await.is_err());
3094    }
3095
3096    #[tokio::test]
3097    async fn test_get_random_password_too_long() {
3098        let state = make_state();
3099        let svc = SecretsManagerService::new(state);
3100
3101        let req = make_request("GetRandomPassword", r#"{"PasswordLength": 4097}"#);
3102        assert!(svc.handle(req).await.is_err());
3103    }
3104
3105    #[tokio::test]
3106    async fn test_update_secret_version_stage_move_current() {
3107        let state = make_state();
3108        let svc = SecretsManagerService::new(state.clone());
3109
3110        let req = make_request(
3111            "CreateSecret",
3112            r#"{"Name": "stage-test", "SecretString": "v1"}"#,
3113        );
3114        svc.handle(req).await.unwrap();
3115
3116        // Put a second version
3117        let req = make_request(
3118            "PutSecretValue",
3119            r#"{"SecretId": "stage-test", "SecretString": "v2"}"#,
3120        );
3121        svc.handle(req).await.unwrap();
3122
3123        // Get version IDs
3124        let (v1_id, v2_id) = {
3125            let _accts = state.read();
3126            let s = _accts.default_ref();
3127            let secret = s.secrets.get("stage-test").unwrap();
3128            let current = secret.current_version_id.clone().unwrap();
3129            let previous = secret
3130                .versions
3131                .iter()
3132                .find(|(id, _)| **id != current)
3133                .map(|(id, _)| id.clone())
3134                .unwrap();
3135            (previous, current)
3136        };
3137
3138        // Move AWSCURRENT from v2 back to v1
3139        let body = serde_json::json!({
3140            "SecretId": "stage-test",
3141            "VersionStage": "AWSCURRENT",
3142            "MoveToVersionId": v1_id,
3143            "RemoveFromVersionId": v2_id,
3144        });
3145        let req = make_request("UpdateSecretVersionStage", &body.to_string());
3146        let resp = svc.handle(req).await.unwrap();
3147        assert_eq!(resp.status, StatusCode::OK);
3148
3149        // Verify v1 is now AWSCURRENT
3150        let _accts = state.read();
3151        let s = _accts.default_ref();
3152        let secret = s.secrets.get("stage-test").unwrap();
3153        let v1 = secret.versions.get(&v1_id).unwrap();
3154        assert!(v1.stages.contains(&"AWSCURRENT".to_string()));
3155
3156        // v2 should have AWSPREVIOUS
3157        let v2 = secret.versions.get(&v2_id).unwrap();
3158        assert!(v2.stages.contains(&"AWSPREVIOUS".to_string()));
3159        assert!(!v2.stages.contains(&"AWSCURRENT".to_string()));
3160
3161        assert_eq!(secret.current_version_id.as_deref(), Some(v1_id.as_str()));
3162    }
3163
3164    #[tokio::test]
3165    async fn test_update_secret_version_stage_custom_label() {
3166        let state = make_state();
3167        let svc = SecretsManagerService::new(state.clone());
3168
3169        let req = make_request(
3170            "CreateSecret",
3171            r#"{"Name": "custom-stage", "SecretString": "v1"}"#,
3172        );
3173        svc.handle(req).await.unwrap();
3174
3175        let vid = {
3176            let _accts = state.read();
3177            let s = _accts.default_ref();
3178            s.secrets
3179                .get("custom-stage")
3180                .unwrap()
3181                .current_version_id
3182                .clone()
3183                .unwrap()
3184        };
3185
3186        // Add a custom label
3187        let body = serde_json::json!({
3188            "SecretId": "custom-stage",
3189            "VersionStage": "MYAPP_LIVE",
3190            "MoveToVersionId": vid,
3191        });
3192        let req = make_request("UpdateSecretVersionStage", &body.to_string());
3193        svc.handle(req).await.unwrap();
3194
3195        let _accts = state.read();
3196        let s = _accts.default_ref();
3197        let secret = s.secrets.get("custom-stage").unwrap();
3198        let ver = secret.versions.get(&vid).unwrap();
3199        assert!(ver.stages.contains(&"MYAPP_LIVE".to_string()));
3200        assert!(ver.stages.contains(&"AWSCURRENT".to_string()));
3201    }
3202
3203    #[tokio::test]
3204    async fn test_validate_resource_policy() {
3205        let state = make_state();
3206        let svc = SecretsManagerService::new(state);
3207
3208        let policy = serde_json::json!({
3209            "Version": "2012-10-17",
3210            "Statement": [{
3211                "Effect": "Allow",
3212                "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
3213                "Action": "secretsmanager:GetSecretValue",
3214                "Resource": "*"
3215            }]
3216        });
3217
3218        let body = serde_json::json!({
3219            "ResourcePolicy": policy.to_string(),
3220        });
3221        let req = make_request("ValidateResourcePolicy", &body.to_string());
3222        let resp = svc.handle(req).await.unwrap();
3223        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3224        assert_eq!(resp_body["PolicyValidationPassed"], true);
3225        assert_eq!(resp_body["ValidationErrors"].as_array().unwrap().len(), 0);
3226    }
3227
3228    #[tokio::test]
3229    async fn test_validate_resource_policy_requires_policy() {
3230        let state = make_state();
3231        let svc = SecretsManagerService::new(state);
3232
3233        let req = make_request("ValidateResourcePolicy", r#"{}"#);
3234        assert!(svc.handle(req).await.is_err());
3235    }
3236
3237    #[tokio::test]
3238    async fn test_put_get_delete_resource_policy() {
3239        let state = make_state();
3240        let svc = SecretsManagerService::new(state);
3241
3242        let req = make_request(
3243            "CreateSecret",
3244            r#"{"Name": "policy-secret", "SecretString": "val"}"#,
3245        );
3246        svc.handle(req).await.unwrap();
3247
3248        // Get policy (should be empty initially)
3249        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3250        let resp = svc.handle(req).await.unwrap();
3251        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3252        assert_eq!(body["Name"], "policy-secret");
3253        assert!(body.get("ResourcePolicy").is_none());
3254
3255        // Put policy
3256        let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
3257        let put_body = serde_json::json!({
3258            "SecretId": "policy-secret",
3259            "ResourcePolicy": policy,
3260        });
3261        let req = make_request("PutResourcePolicy", &put_body.to_string());
3262        let resp = svc.handle(req).await.unwrap();
3263        assert_eq!(resp.status, StatusCode::OK);
3264
3265        // Get policy (should have it now)
3266        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3267        let resp = svc.handle(req).await.unwrap();
3268        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3269        assert_eq!(body["ResourcePolicy"], policy);
3270
3271        // Delete policy
3272        let req = make_request("DeleteResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3273        let resp = svc.handle(req).await.unwrap();
3274        assert_eq!(resp.status, StatusCode::OK);
3275
3276        // Get again (should be gone)
3277        let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
3278        let resp = svc.handle(req).await.unwrap();
3279        let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3280        assert!(body.get("ResourcePolicy").is_none());
3281    }
3282
3283    #[tokio::test]
3284    async fn test_batch_get_secret_value_with_deleted() {
3285        let state = make_state();
3286        let svc = SecretsManagerService::new(state);
3287
3288        let req = make_request(
3289            "CreateSecret",
3290            r#"{"Name": "batch-del", "SecretString": "val"}"#,
3291        );
3292        svc.handle(req).await.unwrap();
3293
3294        // Soft-delete it
3295        let req = make_request("DeleteSecret", r#"{"SecretId": "batch-del"}"#);
3296        svc.handle(req).await.unwrap();
3297
3298        let body = serde_json::json!({
3299            "SecretIdList": ["batch-del"]
3300        });
3301        let req = make_request("BatchGetSecretValue", &body.to_string());
3302        let resp = svc.handle(req).await.unwrap();
3303        let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3304
3305        // Should have 0 values and 1 error
3306        assert_eq!(resp_body["SecretValues"].as_array().unwrap().len(), 0);
3307        let errors = resp_body["Errors"].as_array().unwrap();
3308        assert_eq!(errors.len(), 1);
3309        assert_eq!(errors[0]["ErrorCode"], "InvalidRequestException");
3310    }
3311
3312    // ── CreateSecret idempotency ──
3313
3314    #[tokio::test]
3315    async fn create_secret_idempotent_same_value() {
3316        let state = make_state();
3317        let svc = SecretsManagerService::new(state);
3318
3319        let token = "a".repeat(32);
3320        let body = serde_json::json!({
3321            "Name": "idem",
3322            "SecretString": "val",
3323            "ClientRequestToken": token,
3324        });
3325        let req = make_request("CreateSecret", &body.to_string());
3326        svc.handle(req).await.unwrap();
3327
3328        // Same token + same value -> success (idempotent)
3329        let req = make_request("CreateSecret", &body.to_string());
3330        let resp = svc.handle(req).await.unwrap();
3331        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3332        assert_eq!(b["Name"], "idem");
3333        assert_eq!(b["VersionId"], token);
3334    }
3335
3336    #[tokio::test]
3337    async fn create_secret_idempotent_conflict() {
3338        let state = make_state();
3339        let svc = SecretsManagerService::new(state);
3340
3341        let token = "a".repeat(32);
3342        let body = serde_json::json!({
3343            "Name": "conflict",
3344            "SecretString": "val1",
3345            "ClientRequestToken": token,
3346        });
3347        let req = make_request("CreateSecret", &body.to_string());
3348        svc.handle(req).await.unwrap();
3349
3350        // Same token + different value -> ResourceExistsException
3351        let body2 = serde_json::json!({
3352            "Name": "conflict",
3353            "SecretString": "val2",
3354            "ClientRequestToken": token,
3355        });
3356        let req = make_request("CreateSecret", &body2.to_string());
3357        let err = expect_err(svc.handle(req).await);
3358        assert!(err.to_string().contains("ResourceExistsException"));
3359    }
3360
3361    #[tokio::test]
3362    async fn create_secret_duplicate_name_no_token() {
3363        let state = make_state();
3364        let svc = SecretsManagerService::new(state);
3365
3366        let req = make_request("CreateSecret", r#"{"Name": "dup", "SecretString": "v1"}"#);
3367        svc.handle(req).await.unwrap();
3368
3369        let req = make_request("CreateSecret", r#"{"Name": "dup", "SecretString": "v2"}"#);
3370        let err = expect_err(svc.handle(req).await);
3371        assert!(err.to_string().contains("ResourceExistsException"));
3372    }
3373
3374    #[tokio::test]
3375    async fn create_secret_with_tags_and_description() {
3376        let state = make_state();
3377        let svc = SecretsManagerService::new(state);
3378
3379        let body = serde_json::json!({
3380            "Name": "full-secret",
3381            "SecretString": "v",
3382            "Description": "my secret desc",
3383            "KmsKeyId": "alias/my-key",
3384            "Tags": [{"Key": "env", "Value": "staging"}],
3385        });
3386        let req = make_request("CreateSecret", &body.to_string());
3387        svc.handle(req).await.unwrap();
3388
3389        let req = make_request("DescribeSecret", r#"{"SecretId": "full-secret"}"#);
3390        let resp = svc.handle(req).await.unwrap();
3391        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3392        assert_eq!(b["Description"], "my secret desc");
3393        assert_eq!(b["KmsKeyId"], "alias/my-key");
3394        assert_eq!(b["Tags"][0]["Key"], "env");
3395    }
3396
3397    // ── PutSecretValue edge cases ──
3398
3399    #[tokio::test]
3400    async fn put_secret_value_requires_value() {
3401        let state = make_state();
3402        let svc = SecretsManagerService::new(state);
3403
3404        let req = make_request(
3405            "CreateSecret",
3406            r#"{"Name": "novalue", "SecretString": "v"}"#,
3407        );
3408        svc.handle(req).await.unwrap();
3409
3410        let req = make_request("PutSecretValue", r#"{"SecretId": "novalue"}"#);
3411        let err = expect_err(svc.handle(req).await);
3412        assert!(err.to_string().contains("InvalidRequestException"));
3413    }
3414
3415    #[tokio::test]
3416    async fn put_secret_value_not_found() {
3417        let state = make_state();
3418        let svc = SecretsManagerService::new(state);
3419
3420        let req = make_request(
3421            "PutSecretValue",
3422            r#"{"SecretId": "ghost", "SecretString": "v"}"#,
3423        );
3424        let err = expect_err(svc.handle(req).await);
3425        assert!(err.to_string().contains("ResourceNotFoundException"));
3426    }
3427
3428    #[tokio::test]
3429    async fn put_secret_value_on_deleted_secret() {
3430        let state = make_state();
3431        let svc = SecretsManagerService::new(state);
3432
3433        let req = make_request(
3434            "CreateSecret",
3435            r#"{"Name": "del-put", "SecretString": "v"}"#,
3436        );
3437        svc.handle(req).await.unwrap();
3438        let req = make_request("DeleteSecret", r#"{"SecretId": "del-put"}"#);
3439        svc.handle(req).await.unwrap();
3440
3441        let req = make_request(
3442            "PutSecretValue",
3443            r#"{"SecretId": "del-put", "SecretString": "v2"}"#,
3444        );
3445        let err = expect_err(svc.handle(req).await);
3446        assert!(err.to_string().contains("InvalidRequestException"));
3447    }
3448
3449    #[tokio::test]
3450    async fn put_secret_value_idempotent_match() {
3451        let state = make_state();
3452        let svc = SecretsManagerService::new(state);
3453
3454        let req = make_request(
3455            "CreateSecret",
3456            r#"{"Name": "put-idem", "SecretString": "original"}"#,
3457        );
3458        svc.handle(req).await.unwrap();
3459
3460        let token = "b".repeat(32);
3461        let body = serde_json::json!({
3462            "SecretId": "put-idem",
3463            "SecretString": "new-val",
3464            "ClientRequestToken": token,
3465        });
3466        let req = make_request("PutSecretValue", &body.to_string());
3467        svc.handle(req).await.unwrap();
3468
3469        // Same token + same value -> idempotent success
3470        let req = make_request("PutSecretValue", &body.to_string());
3471        let resp = svc.handle(req).await.unwrap();
3472        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3473        assert_eq!(b["VersionId"], token);
3474    }
3475
3476    #[tokio::test]
3477    async fn put_secret_value_idempotent_conflict() {
3478        let state = make_state();
3479        let svc = SecretsManagerService::new(state);
3480
3481        let req = make_request(
3482            "CreateSecret",
3483            r#"{"Name": "put-conflict", "SecretString": "original"}"#,
3484        );
3485        svc.handle(req).await.unwrap();
3486
3487        let token = "c".repeat(32);
3488        let body = serde_json::json!({
3489            "SecretId": "put-conflict",
3490            "SecretString": "val-a",
3491            "ClientRequestToken": token,
3492        });
3493        let req = make_request("PutSecretValue", &body.to_string());
3494        svc.handle(req).await.unwrap();
3495
3496        // Same token + different value -> conflict
3497        let body2 = serde_json::json!({
3498            "SecretId": "put-conflict",
3499            "SecretString": "val-b",
3500            "ClientRequestToken": token,
3501        });
3502        let req = make_request("PutSecretValue", &body2.to_string());
3503        let err = expect_err(svc.handle(req).await);
3504        assert!(err.to_string().contains("ResourceExistsException"));
3505    }
3506
3507    #[tokio::test]
3508    async fn put_secret_value_with_custom_stages() {
3509        let state = make_state();
3510        let svc = SecretsManagerService::new(state.clone());
3511
3512        let req = make_request(
3513            "CreateSecret",
3514            r#"{"Name": "staged", "SecretString": "v1"}"#,
3515        );
3516        svc.handle(req).await.unwrap();
3517
3518        let body = serde_json::json!({
3519            "SecretId": "staged",
3520            "SecretString": "v2",
3521            "VersionStages": ["AWSCURRENT", "MYAPP_V2"],
3522        });
3523        let req = make_request("PutSecretValue", &body.to_string());
3524        let resp = svc.handle(req).await.unwrap();
3525        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3526        let stages = b["VersionStages"].as_array().unwrap();
3527        assert!(stages.iter().any(|s| s == "MYAPP_V2"));
3528    }
3529
3530    // ── UpdateSecret edge cases ──
3531
3532    #[tokio::test]
3533    async fn update_secret_not_found() {
3534        let state = make_state();
3535        let svc = SecretsManagerService::new(state);
3536
3537        let body = serde_json::json!({
3538            "SecretId": "ghost",
3539            "Description": "new",
3540        });
3541        let req = make_request("UpdateSecret", &body.to_string());
3542        let err = expect_err(svc.handle(req).await);
3543        assert!(err.to_string().contains("ResourceNotFoundException"));
3544    }
3545
3546    #[tokio::test]
3547    async fn update_secret_on_deleted() {
3548        let state = make_state();
3549        let svc = SecretsManagerService::new(state);
3550
3551        let req = make_request(
3552            "CreateSecret",
3553            r#"{"Name": "upd-del", "SecretString": "v"}"#,
3554        );
3555        svc.handle(req).await.unwrap();
3556        let req = make_request("DeleteSecret", r#"{"SecretId": "upd-del"}"#);
3557        svc.handle(req).await.unwrap();
3558
3559        let body = serde_json::json!({
3560            "SecretId": "upd-del",
3561            "Description": "new",
3562        });
3563        let req = make_request("UpdateSecret", &body.to_string());
3564        let err = expect_err(svc.handle(req).await);
3565        assert!(err.to_string().contains("InvalidRequestException"));
3566    }
3567
3568    #[tokio::test]
3569    async fn update_secret_idempotent_match() {
3570        let state = make_state();
3571        let svc = SecretsManagerService::new(state);
3572
3573        let req = make_request(
3574            "CreateSecret",
3575            r#"{"Name": "upd-idem", "SecretString": "orig"}"#,
3576        );
3577        svc.handle(req).await.unwrap();
3578
3579        let token = "d".repeat(32);
3580        let body = serde_json::json!({
3581            "SecretId": "upd-idem",
3582            "SecretString": "new-val",
3583            "ClientRequestToken": token,
3584        });
3585        let req = make_request("UpdateSecret", &body.to_string());
3586        svc.handle(req).await.unwrap();
3587
3588        // Repeat -> idempotent
3589        let req = make_request("UpdateSecret", &body.to_string());
3590        let resp = svc.handle(req).await.unwrap();
3591        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3592        assert_eq!(b["VersionId"], token);
3593    }
3594
3595    // ── DeleteSecret edge cases ──
3596
3597    #[tokio::test]
3598    async fn delete_secret_force() {
3599        let state = make_state();
3600        let svc = SecretsManagerService::new(state.clone());
3601
3602        let req = make_request(
3603            "CreateSecret",
3604            r#"{"Name": "force-del", "SecretString": "v"}"#,
3605        );
3606        svc.handle(req).await.unwrap();
3607
3608        let body = serde_json::json!({
3609            "SecretId": "force-del",
3610            "ForceDeleteWithoutRecovery": true,
3611        });
3612        let req = make_request("DeleteSecret", &body.to_string());
3613        let resp = svc.handle(req).await.unwrap();
3614        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3615        assert_eq!(b["Name"], "force-del");
3616
3617        // Secret should be gone entirely
3618        let _accts = state.read();
3619        let s = _accts.default_ref();
3620        assert!(!s.secrets.contains_key("force-del"));
3621    }
3622
3623    #[tokio::test]
3624    async fn delete_secret_force_nonexistent() {
3625        let state = make_state();
3626        let svc = SecretsManagerService::new(state);
3627
3628        let body = serde_json::json!({
3629            "SecretId": "not-here",
3630            "ForceDeleteWithoutRecovery": true,
3631        });
3632        let req = make_request("DeleteSecret", &body.to_string());
3633        let resp = svc.handle(req).await.unwrap();
3634        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3635        assert_eq!(b["Name"], "not-here");
3636    }
3637
3638    #[tokio::test]
3639    async fn delete_secret_recovery_window() {
3640        let state = make_state();
3641        let svc = SecretsManagerService::new(state);
3642
3643        let req = make_request(
3644            "CreateSecret",
3645            r#"{"Name": "rec-win", "SecretString": "v"}"#,
3646        );
3647        svc.handle(req).await.unwrap();
3648
3649        let body = serde_json::json!({
3650            "SecretId": "rec-win",
3651            "RecoveryWindowInDays": 7,
3652        });
3653        let req = make_request("DeleteSecret", &body.to_string());
3654        let resp = svc.handle(req).await.unwrap();
3655        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3656        assert!(b["DeletionDate"].as_f64().is_some());
3657    }
3658
3659    #[tokio::test]
3660    async fn delete_secret_invalid_recovery_window() {
3661        let state = make_state();
3662        let svc = SecretsManagerService::new(state);
3663
3664        let req = make_request(
3665            "CreateSecret",
3666            r#"{"Name": "bad-win", "SecretString": "v"}"#,
3667        );
3668        svc.handle(req).await.unwrap();
3669
3670        // Too short
3671        let body = serde_json::json!({
3672            "SecretId": "bad-win",
3673            "RecoveryWindowInDays": 3,
3674        });
3675        let req = make_request("DeleteSecret", &body.to_string());
3676        let err = expect_err(svc.handle(req).await);
3677        assert!(err.to_string().contains("InvalidParameterException"));
3678
3679        // Too long
3680        let body = serde_json::json!({
3681            "SecretId": "bad-win",
3682            "RecoveryWindowInDays": 31,
3683        });
3684        let req = make_request("DeleteSecret", &body.to_string());
3685        let err = expect_err(svc.handle(req).await);
3686        assert!(err.to_string().contains("InvalidParameterException"));
3687    }
3688
3689    #[tokio::test]
3690    async fn delete_secret_force_and_recovery_conflict() {
3691        let state = make_state();
3692        let svc = SecretsManagerService::new(state);
3693
3694        let req = make_request("CreateSecret", r#"{"Name": "both", "SecretString": "v"}"#);
3695        svc.handle(req).await.unwrap();
3696
3697        let body = serde_json::json!({
3698            "SecretId": "both",
3699            "ForceDeleteWithoutRecovery": true,
3700            "RecoveryWindowInDays": 7,
3701        });
3702        let req = make_request("DeleteSecret", &body.to_string());
3703        let err = expect_err(svc.handle(req).await);
3704        assert!(err.to_string().contains("InvalidParameterException"));
3705    }
3706
3707    #[tokio::test]
3708    async fn delete_already_deleted_secret() {
3709        let state = make_state();
3710        let svc = SecretsManagerService::new(state);
3711
3712        let req = make_request(
3713            "CreateSecret",
3714            r#"{"Name": "dbl-del", "SecretString": "v"}"#,
3715        );
3716        svc.handle(req).await.unwrap();
3717
3718        let req = make_request("DeleteSecret", r#"{"SecretId": "dbl-del"}"#);
3719        svc.handle(req).await.unwrap();
3720
3721        let req = make_request("DeleteSecret", r#"{"SecretId": "dbl-del"}"#);
3722        let err = expect_err(svc.handle(req).await);
3723        assert!(err.to_string().contains("InvalidRequestException"));
3724    }
3725
3726    // ── GetSecretValue edge cases ──
3727
3728    #[tokio::test]
3729    async fn get_secret_value_by_version_id() {
3730        let state = make_state();
3731        let svc = SecretsManagerService::new(state.clone());
3732
3733        let req = make_request(
3734            "CreateSecret",
3735            r#"{"Name": "ver-get", "SecretString": "v1"}"#,
3736        );
3737        svc.handle(req).await.unwrap();
3738
3739        let v1_id = {
3740            let _accts = state.read();
3741            let s = _accts.default_ref();
3742            s.secrets
3743                .get("ver-get")
3744                .unwrap()
3745                .current_version_id
3746                .clone()
3747                .unwrap()
3748        };
3749
3750        let req = make_request(
3751            "PutSecretValue",
3752            r#"{"SecretId": "ver-get", "SecretString": "v2"}"#,
3753        );
3754        svc.handle(req).await.unwrap();
3755
3756        // Get old version by ID
3757        let body = serde_json::json!({
3758            "SecretId": "ver-get",
3759            "VersionId": v1_id,
3760            "VersionStage": "AWSPREVIOUS",
3761        });
3762        let req = make_request("GetSecretValue", &body.to_string());
3763        let resp = svc.handle(req).await.unwrap();
3764        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3765        assert_eq!(b["SecretString"], "v1");
3766    }
3767
3768    #[tokio::test]
3769    async fn get_secret_value_version_stage_mismatch() {
3770        let state = make_state();
3771        let svc = SecretsManagerService::new(state.clone());
3772
3773        let req = make_request(
3774            "CreateSecret",
3775            r#"{"Name": "mismatch", "SecretString": "v1"}"#,
3776        );
3777        svc.handle(req).await.unwrap();
3778
3779        let vid = {
3780            let _accts = state.read();
3781            let s = _accts.default_ref();
3782            s.secrets
3783                .get("mismatch")
3784                .unwrap()
3785                .current_version_id
3786                .clone()
3787                .unwrap()
3788        };
3789
3790        // Request with VersionId but wrong stage
3791        let body = serde_json::json!({
3792            "SecretId": "mismatch",
3793            "VersionId": vid,
3794            "VersionStage": "AWSPREVIOUS",
3795        });
3796        let req = make_request("GetSecretValue", &body.to_string());
3797        let err = expect_err(svc.handle(req).await);
3798        assert!(err.to_string().contains("ResourceNotFoundException"));
3799    }
3800
3801    #[tokio::test]
3802    async fn get_secret_value_not_found() {
3803        let state = make_state();
3804        let svc = SecretsManagerService::new(state);
3805
3806        let req = make_request("GetSecretValue", r#"{"SecretId": "nope"}"#);
3807        let err = expect_err(svc.handle(req).await);
3808        assert!(err.to_string().contains("ResourceNotFoundException"));
3809    }
3810
3811    #[tokio::test]
3812    async fn get_secret_value_no_versions() {
3813        let state = make_state();
3814        let svc = SecretsManagerService::new(state);
3815
3816        let req = make_request("CreateSecret", r#"{"Name": "empty-ver"}"#);
3817        svc.handle(req).await.unwrap();
3818
3819        let req = make_request("GetSecretValue", r#"{"SecretId": "empty-ver"}"#);
3820        let err = expect_err(svc.handle(req).await);
3821        assert!(err.to_string().contains("ResourceNotFoundException"));
3822    }
3823
3824    #[tokio::test]
3825    async fn get_secret_value_with_binary() {
3826        let state = make_state();
3827        let svc = SecretsManagerService::new(state);
3828
3829        // SecretBinary is base64 encoded
3830        let body = serde_json::json!({
3831            "Name": "bin-secret",
3832            "SecretBinary": "SGVsbG8=",  // "Hello" in base64
3833        });
3834        let req = make_request("CreateSecret", &body.to_string());
3835        svc.handle(req).await.unwrap();
3836
3837        let req = make_request("GetSecretValue", r#"{"SecretId": "bin-secret"}"#);
3838        let resp = svc.handle(req).await.unwrap();
3839        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3840        assert!(b.get("SecretBinary").is_some());
3841        assert!(b.get("SecretString").is_none());
3842    }
3843
3844    // ── ListSecrets with filters ──
3845
3846    #[tokio::test]
3847    async fn list_secrets_filter_by_name() {
3848        let state = make_state();
3849        let svc = SecretsManagerService::new(state);
3850
3851        for name in &["prod/db", "prod/api", "staging/db"] {
3852            let body = serde_json::json!({"Name": name, "SecretString": "v"});
3853            let req = make_request("CreateSecret", &body.to_string());
3854            svc.handle(req).await.unwrap();
3855        }
3856
3857        let body = serde_json::json!({
3858            "Filters": [{"Key": "name", "Values": ["prod/"]}]
3859        });
3860        let req = make_request("ListSecrets", &body.to_string());
3861        let resp = svc.handle(req).await.unwrap();
3862        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3863        assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
3864    }
3865
3866    #[tokio::test]
3867    async fn list_secrets_filter_by_tag_key() {
3868        let state = make_state();
3869        let svc = SecretsManagerService::new(state);
3870
3871        let body = serde_json::json!({
3872            "Name": "tagged-s",
3873            "SecretString": "v",
3874            "Tags": [{"Key": "team", "Value": "backend"}],
3875        });
3876        let req = make_request("CreateSecret", &body.to_string());
3877        svc.handle(req).await.unwrap();
3878
3879        let body = serde_json::json!({"Name": "untagged-s", "SecretString": "v"});
3880        let req = make_request("CreateSecret", &body.to_string());
3881        svc.handle(req).await.unwrap();
3882
3883        let body = serde_json::json!({
3884            "Filters": [{"Key": "tag-key", "Values": ["team"]}]
3885        });
3886        let req = make_request("ListSecrets", &body.to_string());
3887        let resp = svc.handle(req).await.unwrap();
3888        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3889        assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
3890        assert_eq!(b["SecretList"][0]["Name"], "tagged-s");
3891    }
3892
3893    #[tokio::test]
3894    async fn list_secrets_filter_by_description() {
3895        let state = make_state();
3896        let svc = SecretsManagerService::new(state);
3897
3898        let body = serde_json::json!({
3899            "Name": "desc-match",
3900            "SecretString": "v",
3901            "Description": "Database credentials for production",
3902        });
3903        let req = make_request("CreateSecret", &body.to_string());
3904        svc.handle(req).await.unwrap();
3905
3906        let body = serde_json::json!({"Name": "no-desc", "SecretString": "v"});
3907        let req = make_request("CreateSecret", &body.to_string());
3908        svc.handle(req).await.unwrap();
3909
3910        let body = serde_json::json!({
3911            "Filters": [{"Key": "description", "Values": ["Database"]}]
3912        });
3913        let req = make_request("ListSecrets", &body.to_string());
3914        let resp = svc.handle(req).await.unwrap();
3915        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3916        assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
3917    }
3918
3919    #[tokio::test]
3920    async fn list_secrets_include_planned_deletion() {
3921        let state = make_state();
3922        let svc = SecretsManagerService::new(state);
3923
3924        let req = make_request("CreateSecret", r#"{"Name": "alive", "SecretString": "v"}"#);
3925        svc.handle(req).await.unwrap();
3926
3927        let req = make_request("CreateSecret", r#"{"Name": "doomed", "SecretString": "v"}"#);
3928        svc.handle(req).await.unwrap();
3929        let req = make_request("DeleteSecret", r#"{"SecretId": "doomed"}"#);
3930        svc.handle(req).await.unwrap();
3931
3932        // Without IncludePlannedDeletion
3933        let req = make_request("ListSecrets", "{}");
3934        let resp = svc.handle(req).await.unwrap();
3935        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3936        assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
3937
3938        // With IncludePlannedDeletion
3939        let body = serde_json::json!({"IncludePlannedDeletion": true});
3940        let req = make_request("ListSecrets", &body.to_string());
3941        let resp = svc.handle(req).await.unwrap();
3942        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3943        assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
3944    }
3945
3946    #[tokio::test]
3947    async fn list_secrets_pagination() {
3948        let state = make_state();
3949        let svc = SecretsManagerService::new(state);
3950
3951        for i in 0..5 {
3952            let body = serde_json::json!({
3953                "Name": format!("page-{i}"),
3954                "SecretString": "v",
3955            });
3956            let req = make_request("CreateSecret", &body.to_string());
3957            svc.handle(req).await.unwrap();
3958        }
3959
3960        let body = serde_json::json!({"MaxResults": 2});
3961        let req = make_request("ListSecrets", &body.to_string());
3962        let resp = svc.handle(req).await.unwrap();
3963        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
3964        assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
3965        assert!(b["NextToken"].as_str().is_some());
3966    }
3967
3968    #[tokio::test]
3969    async fn list_secrets_invalid_filter_key() {
3970        let state = make_state();
3971        let svc = SecretsManagerService::new(state);
3972
3973        let body = serde_json::json!({
3974            "Filters": [{"Key": "bogus", "Values": ["x"]}]
3975        });
3976        let req = make_request("ListSecrets", &body.to_string());
3977        let err = expect_err(svc.handle(req).await);
3978        assert!(err.to_string().contains("ValidationException"));
3979    }
3980
3981    #[tokio::test]
3982    async fn list_secrets_empty_filter_values() {
3983        let state = make_state();
3984        let svc = SecretsManagerService::new(state);
3985
3986        let body = serde_json::json!({
3987            "Filters": [{"Key": "name", "Values": []}]
3988        });
3989        let req = make_request("ListSecrets", &body.to_string());
3990        let err = expect_err(svc.handle(req).await);
3991        assert!(err.to_string().contains("InvalidParameterException"));
3992    }
3993
3994    // ── ListSecretVersionIds ──
3995
3996    #[tokio::test]
3997    async fn list_secret_version_ids() {
3998        let state = make_state();
3999        let svc = SecretsManagerService::new(state);
4000
4001        let req = make_request(
4002            "CreateSecret",
4003            r#"{"Name": "multi-ver", "SecretString": "v1"}"#,
4004        );
4005        svc.handle(req).await.unwrap();
4006
4007        let req = make_request(
4008            "PutSecretValue",
4009            r#"{"SecretId": "multi-ver", "SecretString": "v2"}"#,
4010        );
4011        svc.handle(req).await.unwrap();
4012
4013        let req = make_request("ListSecretVersionIds", r#"{"SecretId": "multi-ver"}"#);
4014        let resp = svc.handle(req).await.unwrap();
4015        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4016        assert_eq!(b["Name"], "multi-ver");
4017        assert_eq!(b["Versions"].as_array().unwrap().len(), 2);
4018    }
4019
4020    // ── DescribeSecret with rotation info ──
4021
4022    #[tokio::test]
4023    async fn describe_secret_with_rotation_and_next_date() {
4024        let state = make_state();
4025        let svc = SecretsManagerService::new(state);
4026
4027        let req = make_request(
4028            "CreateSecret",
4029            r#"{"Name": "rot-desc", "SecretString": "pw"}"#,
4030        );
4031        svc.handle(req).await.unwrap();
4032
4033        let token = "e".repeat(32);
4034        let body = serde_json::json!({
4035            "SecretId": "rot-desc",
4036            "RotationRules": {"AutomaticallyAfterDays": 14},
4037            "ClientRequestToken": token,
4038        });
4039        let req = make_request("RotateSecret", &body.to_string());
4040        svc.handle(req).await.unwrap();
4041
4042        let req = make_request("DescribeSecret", r#"{"SecretId": "rot-desc"}"#);
4043        let resp = svc.handle(req).await.unwrap();
4044        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4045        assert_eq!(b["RotationEnabled"], true);
4046        assert!(b["LastRotatedDate"].as_f64().is_some());
4047        assert!(b["NextRotationDate"].as_f64().is_some());
4048        assert_eq!(b["RotationRules"]["AutomaticallyAfterDays"], 14);
4049    }
4050
4051    #[tokio::test]
4052    async fn describe_secret_deleted_shows_deletion_date() {
4053        let state = make_state();
4054        let svc = SecretsManagerService::new(state);
4055
4056        let req = make_request(
4057            "CreateSecret",
4058            r#"{"Name": "del-desc", "SecretString": "v"}"#,
4059        );
4060        svc.handle(req).await.unwrap();
4061        let req = make_request("DeleteSecret", r#"{"SecretId": "del-desc"}"#);
4062        svc.handle(req).await.unwrap();
4063
4064        let req = make_request("DescribeSecret", r#"{"SecretId": "del-desc"}"#);
4065        let resp = svc.handle(req).await.unwrap();
4066        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4067        assert!(b["DeletedDate"].as_f64().is_some());
4068    }
4069
4070    // ── BatchGetSecretValue edge cases ──
4071
4072    #[tokio::test]
4073    async fn batch_get_secret_value_both_list_and_filters() {
4074        let state = make_state();
4075        let svc = SecretsManagerService::new(state);
4076
4077        let body = serde_json::json!({
4078            "SecretIdList": ["a"],
4079            "Filters": [{"Key": "name", "Values": ["a"]}],
4080        });
4081        let req = make_request("BatchGetSecretValue", &body.to_string());
4082        let err = expect_err(svc.handle(req).await);
4083        assert!(err.to_string().contains("InvalidParameterException"));
4084    }
4085
4086    #[tokio::test]
4087    async fn batch_get_secret_value_max_results_without_filters() {
4088        let state = make_state();
4089        let svc = SecretsManagerService::new(state);
4090
4091        let body = serde_json::json!({
4092            "SecretIdList": ["a"],
4093            "MaxResults": 10,
4094        });
4095        let req = make_request("BatchGetSecretValue", &body.to_string());
4096        let err = expect_err(svc.handle(req).await);
4097        assert!(err.to_string().contains("InvalidParameterException"));
4098    }
4099
4100    #[tokio::test]
4101    async fn batch_get_secret_value_with_filters() {
4102        let state = make_state();
4103        let svc = SecretsManagerService::new(state);
4104
4105        for name in &["batch-f-a", "batch-f-b", "other-c"] {
4106            let body = serde_json::json!({"Name": name, "SecretString": "v"});
4107            let req = make_request("CreateSecret", &body.to_string());
4108            svc.handle(req).await.unwrap();
4109        }
4110
4111        let body = serde_json::json!({
4112            "Filters": [{"Key": "name", "Values": ["batch-f"]}],
4113        });
4114        let req = make_request("BatchGetSecretValue", &body.to_string());
4115        let resp = svc.handle(req).await.unwrap();
4116        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4117        assert_eq!(b["SecretValues"].as_array().unwrap().len(), 2);
4118    }
4119
4120    // ── RotateSecret validation ──
4121
4122    #[tokio::test]
4123    async fn rotate_secret_invalid_token_length() {
4124        let state = make_state();
4125        let svc = SecretsManagerService::new(state);
4126
4127        let req = make_request(
4128            "CreateSecret",
4129            r#"{"Name": "rot-val", "SecretString": "v"}"#,
4130        );
4131        svc.handle(req).await.unwrap();
4132
4133        let body = serde_json::json!({
4134            "SecretId": "rot-val",
4135            "ClientRequestToken": "short",
4136        });
4137        let req = make_request("RotateSecret", &body.to_string());
4138        let err = expect_err(svc.handle(req).await);
4139        assert!(err.to_string().contains("InvalidParameterException"));
4140    }
4141
4142    #[tokio::test]
4143    async fn rotate_secret_invalid_rules() {
4144        let state = make_state();
4145        let svc = SecretsManagerService::new(state);
4146
4147        let req = make_request(
4148            "CreateSecret",
4149            r#"{"Name": "rot-rules", "SecretString": "v"}"#,
4150        );
4151        svc.handle(req).await.unwrap();
4152
4153        let body = serde_json::json!({
4154            "SecretId": "rot-rules",
4155            "RotationRules": {"AutomaticallyAfterDays": 0},
4156        });
4157        let req = make_request("RotateSecret", &body.to_string());
4158        let err = expect_err(svc.handle(req).await);
4159        assert!(err.to_string().contains("InvalidParameterException"));
4160    }
4161
4162    #[tokio::test]
4163    async fn rotate_secret_on_deleted() {
4164        let state = make_state();
4165        let svc = SecretsManagerService::new(state);
4166
4167        let req = make_request(
4168            "CreateSecret",
4169            r#"{"Name": "rot-del", "SecretString": "v"}"#,
4170        );
4171        svc.handle(req).await.unwrap();
4172        let req = make_request("DeleteSecret", r#"{"SecretId": "rot-del"}"#);
4173        svc.handle(req).await.unwrap();
4174
4175        let body = serde_json::json!({"SecretId": "rot-del"});
4176        let req = make_request("RotateSecret", &body.to_string());
4177        let err = expect_err(svc.handle(req).await);
4178        assert!(err.to_string().contains("InvalidRequestException"));
4179    }
4180
4181    // ── CancelRotateSecret on deleted ──
4182
4183    #[tokio::test]
4184    async fn cancel_rotate_on_deleted() {
4185        let state = make_state();
4186        let svc = SecretsManagerService::new(state);
4187
4188        let req = make_request("CreateSecret", r#"{"Name": "cr-del", "SecretString": "v"}"#);
4189        svc.handle(req).await.unwrap();
4190        let req = make_request("DeleteSecret", r#"{"SecretId": "cr-del"}"#);
4191        svc.handle(req).await.unwrap();
4192
4193        let req = make_request("CancelRotateSecret", r#"{"SecretId": "cr-del"}"#);
4194        let err = expect_err(svc.handle(req).await);
4195        assert!(err.to_string().contains("InvalidRequestException"));
4196    }
4197
4198    // ── UpdateSecretVersionStage edge cases ──
4199
4200    #[tokio::test]
4201    async fn update_version_stage_missing_remove_from() {
4202        let state = make_state();
4203        let svc = SecretsManagerService::new(state.clone());
4204
4205        let req = make_request(
4206            "CreateSecret",
4207            r#"{"Name": "stage-err", "SecretString": "v1"}"#,
4208        );
4209        svc.handle(req).await.unwrap();
4210
4211        let req = make_request(
4212            "PutSecretValue",
4213            r#"{"SecretId": "stage-err", "SecretString": "v2"}"#,
4214        );
4215        svc.handle(req).await.unwrap();
4216
4217        let new_vid = {
4218            let _accts = state.read();
4219            let s = _accts.default_ref();
4220            let secret = s.secrets.get("stage-err").unwrap();
4221            secret
4222                .versions
4223                .iter()
4224                .find(|(_, v)| v.stages.contains(&"AWSPREVIOUS".to_string()))
4225                .map(|(id, _)| id.clone())
4226                .unwrap()
4227        };
4228
4229        // Move AWSCURRENT without RemoveFromVersionId -> error
4230        let body = serde_json::json!({
4231            "SecretId": "stage-err",
4232            "VersionStage": "AWSCURRENT",
4233            "MoveToVersionId": new_vid,
4234        });
4235        let req = make_request("UpdateSecretVersionStage", &body.to_string());
4236        let err = expect_err(svc.handle(req).await);
4237        assert!(err.to_string().contains("InvalidParameterException"));
4238    }
4239
4240    // ── Find secret by ARN ──
4241
4242    #[tokio::test]
4243    async fn find_secret_by_arn() {
4244        let state = make_state();
4245        let svc = SecretsManagerService::new(state);
4246
4247        let req = make_request(
4248            "CreateSecret",
4249            r#"{"Name": "arn-lookup", "SecretString": "v"}"#,
4250        );
4251        let resp = svc.handle(req).await.unwrap();
4252        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4253        let arn = b["ARN"].as_str().unwrap();
4254
4255        // Lookup by full ARN
4256        let body = serde_json::json!({"SecretId": arn});
4257        let req = make_request("GetSecretValue", &body.to_string());
4258        let resp = svc.handle(req).await.unwrap();
4259        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4260        assert_eq!(b["SecretString"], "v");
4261    }
4262
4263    #[tokio::test]
4264    async fn find_secret_by_partial_arn() {
4265        let state = make_state();
4266        let svc = SecretsManagerService::new(state);
4267
4268        let req = make_request(
4269            "CreateSecret",
4270            r#"{"Name": "partial-arn", "SecretString": "v"}"#,
4271        );
4272        svc.handle(req).await.unwrap();
4273
4274        // Partial ARN (without the random suffix)
4275        let partial = "arn:aws:secretsmanager:us-east-1:123456789012:secret:partial-arn";
4276        let body = serde_json::json!({"SecretId": partial});
4277        let req = make_request("GetSecretValue", &body.to_string());
4278        let resp = svc.handle(req).await.unwrap();
4279        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4280        assert_eq!(b["SecretString"], "v");
4281    }
4282
4283    // ── ValidateResourcePolicy edge cases ──
4284
4285    #[tokio::test]
4286    async fn validate_resource_policy_with_secret_id() {
4287        let state = make_state();
4288        let svc = SecretsManagerService::new(state);
4289
4290        let req = make_request(
4291            "CreateSecret",
4292            r#"{"Name": "pol-val", "SecretString": "v"}"#,
4293        );
4294        svc.handle(req).await.unwrap();
4295
4296        let body = serde_json::json!({
4297            "SecretId": "pol-val",
4298            "ResourcePolicy": r#"{"Version":"2012-10-17","Statement":[]}"#,
4299        });
4300        let req = make_request("ValidateResourcePolicy", &body.to_string());
4301        let resp = svc.handle(req).await.unwrap();
4302        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4303        assert_eq!(b["PolicyValidationPassed"], true);
4304    }
4305
4306    #[tokio::test]
4307    async fn validate_resource_policy_nonexistent_secret() {
4308        let state = make_state();
4309        let svc = SecretsManagerService::new(state);
4310
4311        let body = serde_json::json!({
4312            "SecretId": "ghost",
4313            "ResourcePolicy": r#"{"Version":"2012-10-17","Statement":[]}"#,
4314        });
4315        let req = make_request("ValidateResourcePolicy", &body.to_string());
4316        let err = expect_err(svc.handle(req).await);
4317        assert!(err.to_string().contains("ResourceNotFoundException"));
4318    }
4319
4320    // ── Tag operations edge cases ──
4321
4322    #[tokio::test]
4323    async fn tag_resource_updates_existing_tag() {
4324        let state = make_state();
4325        let svc = SecretsManagerService::new(state);
4326
4327        let body = serde_json::json!({
4328            "Name": "tag-upd",
4329            "SecretString": "v",
4330            "Tags": [{"Key": "env", "Value": "dev"}],
4331        });
4332        let req = make_request("CreateSecret", &body.to_string());
4333        svc.handle(req).await.unwrap();
4334
4335        // Update existing tag value
4336        let body = serde_json::json!({
4337            "SecretId": "tag-upd",
4338            "Tags": [{"Key": "env", "Value": "prod"}],
4339        });
4340        let req = make_request("TagResource", &body.to_string());
4341        svc.handle(req).await.unwrap();
4342
4343        let req = make_request("DescribeSecret", r#"{"SecretId": "tag-upd"}"#);
4344        let resp = svc.handle(req).await.unwrap();
4345        let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4346        let tags = b["Tags"].as_array().unwrap();
4347        assert_eq!(tags.len(), 1);
4348        assert_eq!(tags[0]["Value"], "prod");
4349    }
4350
4351    // ── Unsupported action ──
4352
4353    #[tokio::test]
4354    async fn unsupported_action_returns_error() {
4355        let state = make_state();
4356        let svc = SecretsManagerService::new(state);
4357
4358        let req = make_request("BogusAction", "{}");
4359        let err = expect_err(svc.handle(req).await);
4360        assert!(err.to_string().contains("BogusAction"));
4361    }
4362
4363    // ── Helper function tests ──
4364
4365    #[test]
4366    fn test_split_words_basic() {
4367        assert_eq!(split_words("hello"), vec!["hello"]);
4368        assert_eq!(split_words("HelloWorld"), vec!["Hello", "World"]);
4369        assert_eq!(split_words("my/secret/name"), vec!["my", "secret", "name"]);
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    }
4373
4374    #[test]
4375    fn test_split_words_multiple_delimiters() {
4376        // Multiple different special chars -> don't split
4377        assert_eq!(split_words("my/secret-name"), vec!["my/secret-name"]);
4378    }
4379
4380    #[test]
4381    fn test_split_words_with_spaces() {
4382        let words = split_words("hello world");
4383        assert_eq!(words, vec!["hello", "world"]);
4384    }
4385
4386    #[test]
4387    fn test_match_pattern_prefix() {
4388        assert!(match_pattern("prod", "production", true, true));
4389        assert!(!match_pattern("Prod", "production", true, true));
4390        assert!(match_pattern("Prod", "production", true, false));
4391    }
4392
4393    #[test]
4394    fn test_match_pattern_word() {
4395        assert!(match_pattern("hello", "HelloWorld", false, false));
4396        assert!(match_pattern("world", "HelloWorld", false, false));
4397    }
4398
4399    #[test]
4400    fn test_matcher_negation() {
4401        // Negated: "!prod" matches strings that DON'T match "prod"
4402        assert!(matcher(&["!prod"], &["staging"], true, true));
4403    }
4404
4405    #[test]
4406    fn test_base64_roundtrip() {
4407        let data = b"Hello, World!";
4408        let encoded = base64_encode(data);
4409        let decoded = base64_decode(&encoded).unwrap();
4410        assert_eq!(&decoded, data);
4411    }
4412
4413    #[test]
4414    fn test_base64_decode_invalid() {
4415        // Invalid base64 char
4416        assert!(base64_decode("!!!").is_none());
4417    }
4418
4419    #[test]
4420    fn test_check_version_idempotency() {
4421        let mut versions = HashMap::new();
4422        versions.insert(
4423            "v1".to_string(),
4424            SecretVersion {
4425                version_id: "v1".to_string(),
4426                secret_string: Some("hello".to_string()),
4427                secret_binary: None,
4428                stages: vec!["AWSCURRENT".to_string()],
4429                created_at: Utc::now(),
4430            },
4431        );
4432
4433        // Not found
4434        assert!(matches!(
4435            check_secret_version_idempotency(&versions, "v2", None, &Some("x".to_string()), &None),
4436            VersionIdempotency::NotFound
4437        ));
4438
4439        // Match
4440        assert!(matches!(
4441            check_secret_version_idempotency(
4442                &versions,
4443                "v1",
4444                Some("hello".to_string()),
4445                &Some("hello".to_string()),
4446                &None
4447            ),
4448            VersionIdempotency::Match
4449        ));
4450
4451        // Conflict
4452        assert!(matches!(
4453            check_secret_version_idempotency(
4454                &versions,
4455                "v1",
4456                Some("hello".to_string()),
4457                &Some("different".to_string()),
4458                &None
4459            ),
4460            VersionIdempotency::Conflict
4461        ));
4462    }
4463
4464    #[test]
4465    fn test_is_mutating_action() {
4466        assert!(is_mutating_action("CreateSecret"));
4467        assert!(is_mutating_action("DeleteSecret"));
4468        assert!(is_mutating_action("TagResource"));
4469        assert!(!is_mutating_action("GetSecretValue"));
4470        assert!(!is_mutating_action("ListSecrets"));
4471        assert!(!is_mutating_action("DescribeSecret"));
4472    }
4473
4474    #[test]
4475    fn test_parse_tags_empty() {
4476        let val = serde_json::json!(null);
4477        assert_eq!(parse_tags(&val), vec![]);
4478    }
4479
4480    #[test]
4481    fn test_tags_to_json_roundtrip() {
4482        let tags = vec![
4483            ("k1".to_string(), "v1".to_string()),
4484            ("k2".to_string(), "v2".to_string()),
4485        ];
4486        let json = tags_to_json(&tags);
4487        assert_eq!(json.len(), 2);
4488        assert_eq!(json[0]["Key"], "k1");
4489        assert_eq!(json[1]["Value"], "v2");
4490    }
4491
4492    #[test]
4493    fn test_filter_name_prefix() {
4494        let secret = Secret {
4495            name: "prod/database".to_string(),
4496            arn: "arn".to_string(),
4497            description: None,
4498            kms_key_id: None,
4499            versions: HashMap::new(),
4500            current_version_id: None,
4501            tags: vec![],
4502            tags_ever_set: false,
4503            deleted: false,
4504            deletion_date: None,
4505            created_at: Utc::now(),
4506            last_changed_at: Utc::now(),
4507            last_accessed_at: None,
4508            rotation_enabled: None,
4509            rotation_lambda_arn: None,
4510            rotation_rules: None,
4511            last_rotated_at: None,
4512            resource_policy: None,
4513        };
4514        assert!(filter_name(&secret, &["prod/"]));
4515        assert!(!filter_name(&secret, &["staging/"]));
4516    }
4517
4518    #[test]
4519    fn test_filter_tag_value() {
4520        let secret = Secret {
4521            name: "s".to_string(),
4522            arn: "arn".to_string(),
4523            description: None,
4524            kms_key_id: None,
4525            versions: HashMap::new(),
4526            current_version_id: None,
4527            tags: vec![("env".to_string(), "production".to_string())],
4528            tags_ever_set: true,
4529            deleted: false,
4530            deletion_date: None,
4531            created_at: Utc::now(),
4532            last_changed_at: Utc::now(),
4533            last_accessed_at: None,
4534            rotation_enabled: None,
4535            rotation_lambda_arn: None,
4536            rotation_rules: None,
4537            last_rotated_at: None,
4538            resource_policy: None,
4539        };
4540        assert!(filter_tag_value(&secret, &["prod"]));
4541        assert!(!filter_tag_value(&secret, &["staging"]));
4542    }
4543
4544    #[test]
4545    fn test_filter_all_searches_name_desc_tags() {
4546        let secret = Secret {
4547            name: "my-secret".to_string(),
4548            arn: "arn".to_string(),
4549            description: Some("important database".to_string()),
4550            kms_key_id: None,
4551            versions: HashMap::new(),
4552            current_version_id: None,
4553            tags: vec![("team".to_string(), "backend".to_string())],
4554            tags_ever_set: true,
4555            deleted: false,
4556            deletion_date: None,
4557            created_at: Utc::now(),
4558            last_changed_at: Utc::now(),
4559            last_accessed_at: None,
4560            rotation_enabled: None,
4561            rotation_lambda_arn: None,
4562            rotation_rules: None,
4563            last_rotated_at: None,
4564            resource_policy: None,
4565        };
4566        // Matches name
4567        assert!(filter_all(&secret, &["my"]));
4568        // Matches description
4569        assert!(filter_all(&secret, &["database"]));
4570        // Matches tag key
4571        assert!(filter_all(&secret, &["team"]));
4572        // Matches tag value
4573        assert!(filter_all(&secret, &["backend"]));
4574        // No match
4575        assert!(!filter_all(&secret, &["zzzz"]));
4576    }
4577}