use super::*;
use bytes::Bytes;
use http::{HeaderMap, Method};
use parking_lot::RwLock;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
fn make_state() -> SharedSecretsManagerState {
Arc::new(RwLock::new(
fakecloud_core::multi_account::MultiAccountState::new(
"123456789012",
"us-east-1",
"http://localhost:4566",
),
))
}
fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
match result {
Err(e) => e,
Ok(_) => panic!("expected error, got Ok"),
}
}
fn make_request(action: &str, body: &str) -> AwsRequest {
AwsRequest {
service: "secretsmanager".to_string(),
action: action.to_string(),
region: "us-east-1".to_string(),
account_id: "123456789012".to_string(),
request_id: "test-request-id".to_string(),
headers: HeaderMap::new(),
query_params: HashMap::new(),
body: Bytes::from(body.to_string()),
body_stream: parking_lot::Mutex::new(None),
path_segments: vec![],
raw_path: "/".to_string(),
raw_query: String::new(),
method: Method::POST,
is_query_protocol: false,
access_key_id: None,
principal: None,
}
}
#[tokio::test]
async fn test_create_and_get_secret() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "test/secret", "SecretString": "mysecretvalue"}"#,
);
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Name"], "test/secret");
assert!(body["ARN"].as_str().unwrap().contains("test/secret"));
let req = make_request("GetSecretValue", r#"{"SecretId": "test/secret"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["SecretString"], "mysecretvalue");
}
#[tokio::test]
async fn test_create_secret_without_value() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("CreateSecret", r#"{"Name": "empty-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Name"], "empty-secret");
assert!(body.get("VersionId").is_none());
}
#[tokio::test]
async fn test_put_secret_value_creates_version() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "versioned", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "versioned", "SecretString": "v2"}"#,
);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Name"], "versioned");
let req = make_request("GetSecretValue", r#"{"SecretId": "versioned"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["SecretString"], "v2");
}
#[tokio::test]
async fn test_delete_and_restore_secret() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "deleteme", "SecretString": "value"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "deleteme"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert!(body["DeletionDate"].as_f64().is_some());
let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
assert!(svc.handle(req).await.is_err());
let req = make_request("RestoreSecret", r#"{"SecretId": "deleteme"}"#);
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let req = make_request("GetSecretValue", r#"{"SecretId": "deleteme"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["SecretString"], "value");
}
#[tokio::test]
async fn test_list_secrets() {
let state = make_state();
let svc = SecretsManagerService::new(state);
for name in &["alpha", "beta", "gamma"] {
let req = make_request(
"CreateSecret",
&format!(r#"{{"Name": "{name}", "SecretString": "val"}}"#),
);
svc.handle(req).await.unwrap();
}
let req = make_request("ListSecrets", "{}");
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["SecretList"].as_array().unwrap().len(), 3);
}
#[tokio::test]
async fn test_tags() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "tagged", "SecretString": "val"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request(
"TagResource",
r#"{"SecretId": "tagged", "Tags": [{"Key": "env", "Value": "prod"}]}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let tags = body["Tags"].as_array().unwrap();
assert!(tags
.iter()
.any(|t| t["Key"] == "env" && t["Value"] == "prod"));
let req = make_request(
"UntagResource",
r#"{"SecretId": "tagged", "TagKeys": ["env"]}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DescribeSecret", r#"{"SecretId": "tagged"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Tags"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_get_random_password() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("GetRandomPassword", "{}");
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 32);
}
#[tokio::test]
async fn test_replication_ops_return_arn() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "repl-secret", "SecretString": "val"}"#,
);
let resp = svc.handle(req).await.unwrap();
let create_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let expected_arn = create_body["ARN"].as_str().unwrap();
for action in &[
"ReplicateSecretToRegions",
"RemoveRegionsFromReplication",
"StopReplicationToReplica",
] {
let req = make_request(action, r#"{"SecretId": "repl-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(
body["ARN"].as_str().unwrap(),
expected_arn,
"{action} should return the secret's actual ARN"
);
}
}
#[tokio::test]
async fn test_secret_id_length_validation() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let long_id = "x".repeat(2049);
let req = make_request("GetSecretValue", &format!(r#"{{"SecretId": "{long_id}"}}"#));
match svc.handle(req).await {
Err(e) => assert!(e.to_string().contains("ValidationException")),
Ok(_) => panic!("expected ValidationException"),
}
}
#[tokio::test]
async fn test_name_length_validation() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let long_name = "x".repeat(513);
let req = make_request(
"CreateSecret",
&format!(r#"{{"Name": "{long_name}", "SecretString": "val"}}"#),
);
match svc.handle(req).await {
Err(e) => assert!(e.to_string().contains("ValidationException")),
Ok(_) => panic!("expected ValidationException"),
}
}
#[tokio::test]
async fn test_next_token_length_validation() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let long_token = "x".repeat(4097);
let req = make_request(
"ListSecrets",
&format!(r#"{{"NextToken": "{long_token}"}}"#),
);
match svc.handle(req).await {
Err(e) => assert!(e.to_string().contains("ValidationException")),
Ok(_) => panic!("expected ValidationException"),
}
}
#[tokio::test]
async fn test_client_request_token_length_validation() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "test", "SecretString": "val", "ClientRequestToken": "short"}"#,
);
match svc.handle(req).await {
Err(e) => assert!(e.to_string().contains("ValidationException")),
Ok(_) => panic!("expected ValidationException"),
}
}
#[tokio::test]
async fn test_rotate_secret_with_lambda_creates_pending_version() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "rotate-me", "SecretString": "old-password"}"#,
);
svc.handle(req).await.unwrap();
let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
let body = serde_json::json!({
"SecretId": "rotate-me",
"RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:rotator",
"ClientRequestToken": token,
});
let req = make_request("RotateSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(resp_body["VersionId"], token);
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("rotate-me").unwrap();
assert!(
!secret.versions.contains_key(token),
"AWSPENDING version must not be pre-created; the rotation Lambda creates it"
);
assert_eq!(
secret.rotation_lambda_arn.as_deref(),
Some("arn:aws:lambda:us-east-1:123456789012:function:rotator")
);
assert_eq!(secret.rotation_enabled, Some(true));
}
#[tokio::test]
async fn test_rotate_secret_without_lambda_promotes_directly() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "rotate-no-lambda", "SecretString": "value1"}"#,
);
svc.handle(req).await.unwrap();
let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
let body = serde_json::json!({
"SecretId": "rotate-no-lambda",
"ClientRequestToken": token,
});
let req = make_request("RotateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("rotate-no-lambda").unwrap();
let new_ver = secret.versions.get(token).unwrap();
assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
assert_eq!(secret.current_version_id.as_deref(), Some(token));
}
#[tokio::test]
async fn test_rotate_secret_stores_rotation_config() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "rot-cfg", "SecretString": "pw"}"#,
);
svc.handle(req).await.unwrap();
let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
let body = serde_json::json!({
"SecretId": "rot-cfg",
"RotationLambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:my-rotator",
"RotationRules": { "AutomaticallyAfterDays": 30 },
"ClientRequestToken": token,
});
let req = make_request("RotateSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("rot-cfg").unwrap();
assert_eq!(secret.rotation_enabled, Some(true));
assert_eq!(
secret.rotation_lambda_arn.as_deref(),
Some("arn:aws:lambda:us-east-1:123456789012:function:my-rotator")
);
assert!(secret.last_rotated_at.is_some());
let rules = secret.rotation_rules.as_ref().unwrap();
assert_eq!(rules.automatically_after_days, Some(30));
assert!(!secret.versions.contains_key(token));
}
#[tokio::test]
async fn test_rotate_secret_version_stages_change() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "rot-stages", "SecretString": "original"}"#,
);
svc.handle(req).await.unwrap();
let original_vid = {
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("rot-stages").unwrap();
secret.current_version_id.clone().unwrap()
};
let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
let body = serde_json::json!({
"SecretId": "rot-stages",
"ClientRequestToken": token,
});
let req = make_request("RotateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("rot-stages").unwrap();
let new_ver = secret.versions.get(token).unwrap();
assert!(new_ver.stages.contains(&"AWSCURRENT".to_string()));
let old_ver = secret.versions.get(&original_vid).unwrap();
assert!(old_ver.stages.contains(&"AWSPREVIOUS".to_string()));
assert!(!old_ver.stages.contains(&"AWSCURRENT".to_string()));
}
#[tokio::test]
async fn test_cancel_rotate_secret() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "cancel-rot", "SecretString": "pw"}"#,
);
svc.handle(req).await.unwrap();
let token = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
let body = serde_json::json!({
"SecretId": "cancel-rot",
"ClientRequestToken": token,
});
let req = make_request("RotateSecret", &body.to_string());
svc.handle(req).await.unwrap();
{
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("cancel-rot").unwrap();
assert_eq!(secret.rotation_enabled, Some(true));
}
let req = make_request("CancelRotateSecret", r#"{"SecretId": "cancel-rot"}"#);
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Name"], "cancel-rot");
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("cancel-rot").unwrap();
assert_eq!(secret.rotation_enabled, Some(false));
}
#[tokio::test]
async fn test_cancel_rotate_secret_fails_when_not_enabled() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "no-rot", "SecretString": "pw"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("CancelRotateSecret", r#"{"SecretId": "no-rot"}"#);
let result = svc.handle(req).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_batch_get_secret_value_multiple() {
let state = make_state();
let svc = SecretsManagerService::new(state);
for (name, val) in &[("batch-a", "va"), ("batch-b", "vb"), ("batch-c", "vc")] {
let req = make_request(
"CreateSecret",
&format!(r#"{{"Name": "{name}", "SecretString": "{val}"}}"#),
);
svc.handle(req).await.unwrap();
}
let body = serde_json::json!({
"SecretIdList": ["batch-a", "batch-b", "batch-c"]
});
let req = make_request("BatchGetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let values = resp_body["SecretValues"].as_array().unwrap();
assert_eq!(values.len(), 3);
let names: Vec<&str> = values.iter().map(|v| v["Name"].as_str().unwrap()).collect();
assert!(names.contains(&"batch-a"));
assert!(names.contains(&"batch-b"));
assert!(names.contains(&"batch-c"));
assert!(resp_body.get("Errors").is_none());
}
#[tokio::test]
async fn test_batch_get_secret_value_with_missing() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "exists", "SecretString": "val"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretIdList": ["exists", "nonexistent"]
});
let req = make_request("BatchGetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let values = resp_body["SecretValues"].as_array().unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0]["Name"], "exists");
let errors = resp_body["Errors"].as_array().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0]["SecretId"], "nonexistent");
assert_eq!(errors[0]["ErrorCode"], "ResourceNotFoundException");
}
#[tokio::test]
async fn test_update_secret_changes_description_and_kms() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "updatable", "SecretString": "val", "Description": "old desc"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "updatable",
"Description": "new desc",
"KmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/my-key"
});
let req = make_request("UpdateSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(resp_body["Name"], "updatable");
assert!(resp_body.get("VersionId").is_none());
let req = make_request("DescribeSecret", r#"{"SecretId": "updatable"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Description"], "new desc");
assert_eq!(
body["KmsKeyId"],
"arn:aws:kms:us-east-1:123456789012:key/my-key"
);
}
#[tokio::test]
async fn test_update_secret_with_new_value() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "upd-val", "SecretString": "old"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "upd-val",
"SecretString": "new-value"
});
let req = make_request("UpdateSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert!(resp_body["VersionId"].as_str().is_some());
let req = make_request("GetSecretValue", r#"{"SecretId": "upd-val"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["SecretString"], "new-value");
}
#[tokio::test]
async fn test_get_random_password_custom_length() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("GetRandomPassword", r#"{"PasswordLength": 64}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["RandomPassword"].as_str().unwrap().len(), 64);
}
#[tokio::test]
async fn test_get_random_password_exclude_chars() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"GetRandomPassword",
r#"{"PasswordLength": 100, "ExcludeCharacters": "abc123"}"#,
);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let password = body["RandomPassword"].as_str().unwrap();
assert_eq!(password.len(), 100);
assert!(!password.contains('a'));
assert!(!password.contains('b'));
assert!(!password.contains('c'));
assert!(!password.contains('1'));
assert!(!password.contains('2'));
assert!(!password.contains('3'));
}
#[tokio::test]
async fn test_get_random_password_exclude_types() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"PasswordLength": 50,
"ExcludeUppercase": true,
"ExcludeNumbers": true,
"ExcludePunctuation": true,
"RequireEachIncludedType": false,
});
let req = make_request("GetRandomPassword", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let password = resp_body["RandomPassword"].as_str().unwrap();
assert_eq!(password.len(), 50);
assert!(password.chars().all(|c| c.is_ascii_lowercase()));
}
#[tokio::test]
async fn test_get_random_password_too_short() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("GetRandomPassword", r#"{"PasswordLength": 3}"#);
assert!(svc.handle(req).await.is_err());
}
#[tokio::test]
async fn test_get_random_password_too_long() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("GetRandomPassword", r#"{"PasswordLength": 4097}"#);
assert!(svc.handle(req).await.is_err());
}
#[tokio::test]
async fn test_update_secret_version_stage_move_current() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "stage-test", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "stage-test", "SecretString": "v2"}"#,
);
svc.handle(req).await.unwrap();
let (v1_id, v2_id) = {
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("stage-test").unwrap();
let current = secret.current_version_id.clone().unwrap();
let previous = secret
.versions
.iter()
.find(|(id, _)| **id != current)
.map(|(id, _)| id.clone())
.unwrap();
(previous, current)
};
let body = serde_json::json!({
"SecretId": "stage-test",
"VersionStage": "AWSCURRENT",
"MoveToVersionId": v1_id,
"RemoveFromVersionId": v2_id,
});
let req = make_request("UpdateSecretVersionStage", &body.to_string());
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("stage-test").unwrap();
let v1 = secret.versions.get(&v1_id).unwrap();
assert!(v1.stages.contains(&"AWSCURRENT".to_string()));
let v2 = secret.versions.get(&v2_id).unwrap();
assert!(v2.stages.contains(&"AWSPREVIOUS".to_string()));
assert!(!v2.stages.contains(&"AWSCURRENT".to_string()));
assert_eq!(secret.current_version_id.as_deref(), Some(v1_id.as_str()));
}
#[tokio::test]
async fn test_update_secret_version_stage_custom_label() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "custom-stage", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let vid = {
let _accts = state.read();
let s = _accts.default_ref();
s.secrets
.get("custom-stage")
.unwrap()
.current_version_id
.clone()
.unwrap()
};
let body = serde_json::json!({
"SecretId": "custom-stage",
"VersionStage": "MYAPP_LIVE",
"MoveToVersionId": vid,
});
let req = make_request("UpdateSecretVersionStage", &body.to_string());
svc.handle(req).await.unwrap();
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("custom-stage").unwrap();
let ver = secret.versions.get(&vid).unwrap();
assert!(ver.stages.contains(&"MYAPP_LIVE".to_string()));
assert!(ver.stages.contains(&"AWSCURRENT".to_string()));
}
#[tokio::test]
async fn test_validate_resource_policy() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let policy = serde_json::json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:root"},
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}]
});
let body = serde_json::json!({
"ResourcePolicy": policy.to_string(),
});
let req = make_request("ValidateResourcePolicy", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(resp_body["PolicyValidationPassed"], true);
assert_eq!(resp_body["ValidationErrors"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_validate_resource_policy_requires_policy() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("ValidateResourcePolicy", r#"{}"#);
assert!(svc.handle(req).await.is_err());
}
#[tokio::test]
async fn test_put_get_delete_resource_policy() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "policy-secret", "SecretString": "val"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["Name"], "policy-secret");
assert!(body.get("ResourcePolicy").is_none());
let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
let put_body = serde_json::json!({
"SecretId": "policy-secret",
"ResourcePolicy": policy,
});
let req = make_request("PutResourcePolicy", &put_body.to_string());
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["ResourcePolicy"], policy);
let req = make_request("DeleteResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
let resp = svc.handle(req).await.unwrap();
assert_eq!(resp.status, StatusCode::OK);
let req = make_request("GetResourcePolicy", r#"{"SecretId": "policy-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert!(body.get("ResourcePolicy").is_none());
}
#[tokio::test]
async fn test_batch_get_secret_value_with_deleted() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "batch-del", "SecretString": "val"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "batch-del"}"#);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretIdList": ["batch-del"]
});
let req = make_request("BatchGetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let resp_body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(resp_body["SecretValues"].as_array().unwrap().len(), 0);
let errors = resp_body["Errors"].as_array().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0]["ErrorCode"], "InvalidRequestException");
}
#[tokio::test]
async fn create_secret_idempotent_same_value() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let token = "a".repeat(32);
let body = serde_json::json!({
"Name": "idem",
"SecretString": "val",
"ClientRequestToken": token,
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("CreateSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["Name"], "idem");
assert_eq!(b["VersionId"], token);
}
#[tokio::test]
async fn create_secret_idempotent_conflict() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let token = "a".repeat(32);
let body = serde_json::json!({
"Name": "conflict",
"SecretString": "val1",
"ClientRequestToken": token,
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let body2 = serde_json::json!({
"Name": "conflict",
"SecretString": "val2",
"ClientRequestToken": token,
});
let req = make_request("CreateSecret", &body2.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceExistsException"));
}
#[tokio::test]
async fn create_secret_duplicate_name_no_token() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("CreateSecret", r#"{"Name": "dup", "SecretString": "v1"}"#);
svc.handle(req).await.unwrap();
let req = make_request("CreateSecret", r#"{"Name": "dup", "SecretString": "v2"}"#);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceExistsException"));
}
#[tokio::test]
async fn create_secret_with_tags_and_description() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Name": "full-secret",
"SecretString": "v",
"Description": "my secret desc",
"KmsKeyId": "alias/my-key",
"Tags": [{"Key": "env", "Value": "staging"}],
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("DescribeSecret", r#"{"SecretId": "full-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["Description"], "my secret desc");
assert_eq!(b["KmsKeyId"], "alias/my-key");
assert_eq!(b["Tags"][0]["Key"], "env");
}
#[tokio::test]
async fn put_secret_value_requires_value() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "novalue", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("PutSecretValue", r#"{"SecretId": "novalue"}"#);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidRequestException"));
}
#[tokio::test]
async fn put_secret_value_not_found() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "ghost", "SecretString": "v"}"#,
);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceNotFoundException"));
}
#[tokio::test]
async fn put_secret_value_on_deleted_secret() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "del-put", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "del-put"}"#);
svc.handle(req).await.unwrap();
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "del-put", "SecretString": "v2"}"#,
);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidRequestException"));
}
#[tokio::test]
async fn put_secret_value_idempotent_match() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "put-idem", "SecretString": "original"}"#,
);
svc.handle(req).await.unwrap();
let token = "b".repeat(32);
let body = serde_json::json!({
"SecretId": "put-idem",
"SecretString": "new-val",
"ClientRequestToken": token,
});
let req = make_request("PutSecretValue", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("PutSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["VersionId"], token);
}
#[tokio::test]
async fn put_secret_value_idempotent_conflict() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "put-conflict", "SecretString": "original"}"#,
);
svc.handle(req).await.unwrap();
let token = "c".repeat(32);
let body = serde_json::json!({
"SecretId": "put-conflict",
"SecretString": "val-a",
"ClientRequestToken": token,
});
let req = make_request("PutSecretValue", &body.to_string());
svc.handle(req).await.unwrap();
let body2 = serde_json::json!({
"SecretId": "put-conflict",
"SecretString": "val-b",
"ClientRequestToken": token,
});
let req = make_request("PutSecretValue", &body2.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceExistsException"));
}
#[tokio::test]
async fn put_secret_value_with_custom_stages() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "staged", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "staged",
"SecretString": "v2",
"VersionStages": ["AWSCURRENT", "MYAPP_V2"],
});
let req = make_request("PutSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let stages = b["VersionStages"].as_array().unwrap();
assert!(stages.iter().any(|s| s == "MYAPP_V2"));
}
#[tokio::test]
async fn update_secret_not_found() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"SecretId": "ghost",
"Description": "new",
});
let req = make_request("UpdateSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceNotFoundException"));
}
#[tokio::test]
async fn update_secret_on_deleted() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "upd-del", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "upd-del"}"#);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "upd-del",
"Description": "new",
});
let req = make_request("UpdateSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidRequestException"));
}
#[tokio::test]
async fn update_secret_idempotent_match() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "upd-idem", "SecretString": "orig"}"#,
);
svc.handle(req).await.unwrap();
let token = "d".repeat(32);
let body = serde_json::json!({
"SecretId": "upd-idem",
"SecretString": "new-val",
"ClientRequestToken": token,
});
let req = make_request("UpdateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("UpdateSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["VersionId"], token);
}
#[tokio::test]
async fn delete_secret_force() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "force-del", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "force-del",
"ForceDeleteWithoutRecovery": true,
});
let req = make_request("DeleteSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["Name"], "force-del");
let _accts = state.read();
let s = _accts.default_ref();
assert!(!s.secrets.contains_key("force-del"));
}
#[tokio::test]
async fn delete_secret_force_nonexistent() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"SecretId": "not-here",
"ForceDeleteWithoutRecovery": true,
});
let req = make_request("DeleteSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["Name"], "not-here");
}
#[tokio::test]
async fn delete_secret_recovery_window() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "rec-win", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "rec-win",
"RecoveryWindowInDays": 7,
});
let req = make_request("DeleteSecret", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert!(b["DeletionDate"].as_f64().is_some());
}
#[tokio::test]
async fn delete_secret_invalid_recovery_window() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "bad-win", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "bad-win",
"RecoveryWindowInDays": 3,
});
let req = make_request("DeleteSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
let body = serde_json::json!({
"SecretId": "bad-win",
"RecoveryWindowInDays": 31,
});
let req = make_request("DeleteSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn delete_secret_force_and_recovery_conflict() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("CreateSecret", r#"{"Name": "both", "SecretString": "v"}"#);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "both",
"ForceDeleteWithoutRecovery": true,
"RecoveryWindowInDays": 7,
});
let req = make_request("DeleteSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn delete_already_deleted_secret() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "dbl-del", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "dbl-del"}"#);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "dbl-del"}"#);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidRequestException"));
}
#[tokio::test]
async fn get_secret_value_by_version_id() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "ver-get", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let v1_id = {
let _accts = state.read();
let s = _accts.default_ref();
s.secrets
.get("ver-get")
.unwrap()
.current_version_id
.clone()
.unwrap()
};
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "ver-get", "SecretString": "v2"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "ver-get",
"VersionId": v1_id,
"VersionStage": "AWSPREVIOUS",
});
let req = make_request("GetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretString"], "v1");
}
#[tokio::test]
async fn get_secret_value_version_stage_mismatch() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "mismatch", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let vid = {
let _accts = state.read();
let s = _accts.default_ref();
s.secrets
.get("mismatch")
.unwrap()
.current_version_id
.clone()
.unwrap()
};
let body = serde_json::json!({
"SecretId": "mismatch",
"VersionId": vid,
"VersionStage": "AWSPREVIOUS",
});
let req = make_request("GetSecretValue", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceNotFoundException"));
}
#[tokio::test]
async fn get_secret_value_not_found() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("GetSecretValue", r#"{"SecretId": "nope"}"#);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceNotFoundException"));
}
#[tokio::test]
async fn get_secret_value_no_versions() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("CreateSecret", r#"{"Name": "empty-ver"}"#);
svc.handle(req).await.unwrap();
let req = make_request("GetSecretValue", r#"{"SecretId": "empty-ver"}"#);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceNotFoundException"));
}
#[tokio::test]
async fn get_secret_value_with_binary() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Name": "bin-secret",
"SecretBinary": "SGVsbG8=", });
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("GetSecretValue", r#"{"SecretId": "bin-secret"}"#);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert!(b.get("SecretBinary").is_some());
assert!(b.get("SecretString").is_none());
}
#[tokio::test]
async fn list_secrets_filter_by_name() {
let state = make_state();
let svc = SecretsManagerService::new(state);
for name in &["prod/db", "prod/api", "staging/db"] {
let body = serde_json::json!({"Name": name, "SecretString": "v"});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
}
let body = serde_json::json!({
"Filters": [{"Key": "name", "Values": ["prod/"]}]
});
let req = make_request("ListSecrets", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
}
#[tokio::test]
async fn list_secrets_filter_by_tag_key() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Name": "tagged-s",
"SecretString": "v",
"Tags": [{"Key": "team", "Value": "backend"}],
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let body = serde_json::json!({"Name": "untagged-s", "SecretString": "v"});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"Filters": [{"Key": "tag-key", "Values": ["team"]}]
});
let req = make_request("ListSecrets", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
assert_eq!(b["SecretList"][0]["Name"], "tagged-s");
}
#[tokio::test]
async fn list_secrets_filter_by_description() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Name": "desc-match",
"SecretString": "v",
"Description": "Database credentials for production",
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let body = serde_json::json!({"Name": "no-desc", "SecretString": "v"});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"Filters": [{"Key": "description", "Values": ["Database"]}]
});
let req = make_request("ListSecrets", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn list_secrets_include_planned_deletion() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("CreateSecret", r#"{"Name": "alive", "SecretString": "v"}"#);
svc.handle(req).await.unwrap();
let req = make_request("CreateSecret", r#"{"Name": "doomed", "SecretString": "v"}"#);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "doomed"}"#);
svc.handle(req).await.unwrap();
let req = make_request("ListSecrets", "{}");
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretList"].as_array().unwrap().len(), 1);
let body = serde_json::json!({"IncludePlannedDeletion": true});
let req = make_request("ListSecrets", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
}
#[tokio::test]
async fn list_secrets_pagination() {
let state = make_state();
let svc = SecretsManagerService::new(state);
for i in 0..5 {
let body = serde_json::json!({
"Name": format!("page-{i}"),
"SecretString": "v",
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
}
let body = serde_json::json!({"MaxResults": 2});
let req = make_request("ListSecrets", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretList"].as_array().unwrap().len(), 2);
assert!(b["NextToken"].as_str().is_some());
}
#[tokio::test]
async fn list_secrets_invalid_filter_key() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Filters": [{"Key": "bogus", "Values": ["x"]}]
});
let req = make_request("ListSecrets", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ValidationException"));
}
#[tokio::test]
async fn list_secrets_empty_filter_values() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Filters": [{"Key": "name", "Values": []}]
});
let req = make_request("ListSecrets", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn list_secret_version_ids() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "multi-ver", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "multi-ver", "SecretString": "v2"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("ListSecretVersionIds", r#"{"SecretId": "multi-ver"}"#);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["Name"], "multi-ver");
assert_eq!(b["Versions"].as_array().unwrap().len(), 2);
}
#[tokio::test]
async fn describe_secret_with_rotation_and_next_date() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "rot-desc", "SecretString": "pw"}"#,
);
svc.handle(req).await.unwrap();
let token = "e".repeat(32);
let body = serde_json::json!({
"SecretId": "rot-desc",
"RotationRules": {"AutomaticallyAfterDays": 14},
"ClientRequestToken": token,
});
let req = make_request("RotateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("DescribeSecret", r#"{"SecretId": "rot-desc"}"#);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["RotationEnabled"], true);
assert!(b["LastRotatedDate"].as_f64().is_some());
assert!(b["NextRotationDate"].as_f64().is_some());
assert_eq!(b["RotationRules"]["AutomaticallyAfterDays"], 14);
}
#[tokio::test]
async fn describe_secret_deleted_shows_deletion_date() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "del-desc", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "del-desc"}"#);
svc.handle(req).await.unwrap();
let req = make_request("DescribeSecret", r#"{"SecretId": "del-desc"}"#);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert!(b["DeletedDate"].as_f64().is_some());
}
#[tokio::test]
async fn batch_get_secret_value_both_list_and_filters() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"SecretIdList": ["a"],
"Filters": [{"Key": "name", "Values": ["a"]}],
});
let req = make_request("BatchGetSecretValue", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn batch_get_secret_value_max_results_without_filters() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"SecretIdList": ["a"],
"MaxResults": 10,
});
let req = make_request("BatchGetSecretValue", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn batch_get_secret_value_with_filters() {
let state = make_state();
let svc = SecretsManagerService::new(state);
for name in &["batch-f-a", "batch-f-b", "other-c"] {
let body = serde_json::json!({"Name": name, "SecretString": "v"});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
}
let body = serde_json::json!({
"Filters": [{"Key": "name", "Values": ["batch-f"]}],
});
let req = make_request("BatchGetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretValues"].as_array().unwrap().len(), 2);
}
#[tokio::test]
async fn rotate_secret_invalid_token_length() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "rot-val", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "rot-val",
"ClientRequestToken": "short",
});
let req = make_request("RotateSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn rotate_secret_invalid_rules() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "rot-rules", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "rot-rules",
"RotationRules": {"AutomaticallyAfterDays": 0},
});
let req = make_request("RotateSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn rotate_secret_on_deleted() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "rot-del", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "rot-del"}"#);
svc.handle(req).await.unwrap();
let body = serde_json::json!({"SecretId": "rot-del"});
let req = make_request("RotateSecret", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidRequestException"));
}
#[tokio::test]
async fn cancel_rotate_on_deleted() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("CreateSecret", r#"{"Name": "cr-del", "SecretString": "v"}"#);
svc.handle(req).await.unwrap();
let req = make_request("DeleteSecret", r#"{"SecretId": "cr-del"}"#);
svc.handle(req).await.unwrap();
let req = make_request("CancelRotateSecret", r#"{"SecretId": "cr-del"}"#);
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidRequestException"));
}
#[tokio::test]
async fn update_version_stage_missing_remove_from() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request(
"CreateSecret",
r#"{"Name": "stage-err", "SecretString": "v1"}"#,
);
svc.handle(req).await.unwrap();
let req = make_request(
"PutSecretValue",
r#"{"SecretId": "stage-err", "SecretString": "v2"}"#,
);
svc.handle(req).await.unwrap();
let new_vid = {
let _accts = state.read();
let s = _accts.default_ref();
let secret = s.secrets.get("stage-err").unwrap();
secret
.versions
.iter()
.find(|(_, v)| v.stages.contains(&"AWSPREVIOUS".to_string()))
.map(|(id, _)| id.clone())
.unwrap()
};
let body = serde_json::json!({
"SecretId": "stage-err",
"VersionStage": "AWSCURRENT",
"MoveToVersionId": new_vid,
});
let req = make_request("UpdateSecretVersionStage", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("InvalidParameterException"));
}
#[tokio::test]
async fn find_secret_by_arn() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "arn-lookup", "SecretString": "v"}"#,
);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let arn = b["ARN"].as_str().unwrap();
let body = serde_json::json!({"SecretId": arn});
let req = make_request("GetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretString"], "v");
}
#[tokio::test]
async fn find_secret_by_partial_arn() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "partial-arn", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let partial = "arn:aws:secretsmanager:us-east-1:123456789012:secret:partial-arn";
let body = serde_json::json!({"SecretId": partial});
let req = make_request("GetSecretValue", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["SecretString"], "v");
}
#[tokio::test]
async fn validate_resource_policy_with_secret_id() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request(
"CreateSecret",
r#"{"Name": "pol-val", "SecretString": "v"}"#,
);
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "pol-val",
"ResourcePolicy": r#"{"Version":"2012-10-17","Statement":[]}"#,
});
let req = make_request("ValidateResourcePolicy", &body.to_string());
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(b["PolicyValidationPassed"], true);
}
#[tokio::test]
async fn validate_resource_policy_nonexistent_secret() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"SecretId": "ghost",
"ResourcePolicy": r#"{"Version":"2012-10-17","Statement":[]}"#,
});
let req = make_request("ValidateResourcePolicy", &body.to_string());
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("ResourceNotFoundException"));
}
#[tokio::test]
async fn tag_resource_updates_existing_tag() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let body = serde_json::json!({
"Name": "tag-upd",
"SecretString": "v",
"Tags": [{"Key": "env", "Value": "dev"}],
});
let req = make_request("CreateSecret", &body.to_string());
svc.handle(req).await.unwrap();
let body = serde_json::json!({
"SecretId": "tag-upd",
"Tags": [{"Key": "env", "Value": "prod"}],
});
let req = make_request("TagResource", &body.to_string());
svc.handle(req).await.unwrap();
let req = make_request("DescribeSecret", r#"{"SecretId": "tag-upd"}"#);
let resp = svc.handle(req).await.unwrap();
let b: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let tags = b["Tags"].as_array().unwrap();
assert_eq!(tags.len(), 1);
assert_eq!(tags[0]["Value"], "prod");
}
#[tokio::test]
async fn unsupported_action_returns_error() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request("BogusAction", "{}");
let err = expect_err(svc.handle(req).await);
assert!(err.to_string().contains("BogusAction"));
}
#[test]
fn test_split_words_basic() {
assert_eq!(split_words("hello"), vec!["hello"]);
assert_eq!(split_words("HelloWorld"), vec!["Hello", "World"]);
assert_eq!(split_words("my/secret/name"), vec!["my", "secret", "name"]);
assert_eq!(split_words("my-secret-name"), vec!["my", "secret", "name"]);
assert_eq!(split_words("my_secret_name"), vec!["my", "secret", "name"]);
}
#[test]
fn test_split_words_multiple_delimiters() {
assert_eq!(split_words("my/secret-name"), vec!["my/secret-name"]);
}
#[test]
fn test_split_words_with_spaces() {
let words = split_words("hello world");
assert_eq!(words, vec!["hello", "world"]);
}
#[test]
fn test_match_pattern_prefix() {
assert!(match_pattern("prod", "production", true, true));
assert!(!match_pattern("Prod", "production", true, true));
assert!(match_pattern("Prod", "production", true, false));
}
#[test]
fn test_match_pattern_word() {
assert!(match_pattern("hello", "HelloWorld", false, false));
assert!(match_pattern("world", "HelloWorld", false, false));
}
#[test]
fn test_matcher_negation() {
assert!(matcher(&["!prod"], &["staging"], true, true));
}
#[test]
fn test_base64_roundtrip() {
let data = b"Hello, World!";
let encoded = base64_encode(data);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(&decoded, data);
}
#[test]
fn test_base64_decode_invalid() {
assert!(base64_decode("!!!").is_none());
}
#[test]
fn test_check_version_idempotency() {
let mut versions = BTreeMap::new();
versions.insert(
"v1".to_string(),
SecretVersion {
version_id: "v1".to_string(),
secret_string: Some("hello".to_string()),
secret_binary: None,
stages: vec!["AWSCURRENT".to_string()],
created_at: Utc::now(),
},
);
assert!(matches!(
check_secret_version_idempotency(&versions, "v2", None, &Some("x".to_string()), &None),
VersionIdempotency::NotFound
));
assert!(matches!(
check_secret_version_idempotency(
&versions,
"v1",
Some("hello".to_string()),
&Some("hello".to_string()),
&None
),
VersionIdempotency::Match
));
assert!(matches!(
check_secret_version_idempotency(
&versions,
"v1",
Some("hello".to_string()),
&Some("different".to_string()),
&None
),
VersionIdempotency::Conflict
));
}
#[test]
fn test_is_mutating_action() {
assert!(is_mutating_action("CreateSecret"));
assert!(is_mutating_action("DeleteSecret"));
assert!(is_mutating_action("TagResource"));
assert!(!is_mutating_action("GetSecretValue"));
assert!(!is_mutating_action("ListSecrets"));
assert!(!is_mutating_action("DescribeSecret"));
}
#[test]
fn test_parse_tags_empty() {
let val = serde_json::json!(null);
assert_eq!(parse_tags(&val), vec![]);
}
#[test]
fn test_tags_to_json_roundtrip() {
let tags = vec![
("k1".to_string(), "v1".to_string()),
("k2".to_string(), "v2".to_string()),
];
let json = tags_to_json(&tags);
assert_eq!(json.len(), 2);
assert_eq!(json[0]["Key"], "k1");
assert_eq!(json[1]["Value"], "v2");
}
#[test]
fn test_filter_name_prefix() {
let secret = Secret {
name: "prod/database".to_string(),
arn: "arn".to_string(),
description: None,
kms_key_id: None,
versions: BTreeMap::new(),
current_version_id: None,
tags: vec![],
tags_ever_set: false,
deleted: false,
deletion_date: None,
created_at: Utc::now(),
last_changed_at: Utc::now(),
last_accessed_at: None,
rotation_enabled: None,
rotation_lambda_arn: None,
rotation_rules: None,
last_rotated_at: None,
resource_policy: None,
};
assert!(filter_name(&secret, &["prod/"]));
assert!(!filter_name(&secret, &["staging/"]));
}
#[test]
fn test_filter_tag_value() {
let secret = Secret {
name: "s".to_string(),
arn: "arn".to_string(),
description: None,
kms_key_id: None,
versions: BTreeMap::new(),
current_version_id: None,
tags: vec![("env".to_string(), "production".to_string())],
tags_ever_set: true,
deleted: false,
deletion_date: None,
created_at: Utc::now(),
last_changed_at: Utc::now(),
last_accessed_at: None,
rotation_enabled: None,
rotation_lambda_arn: None,
rotation_rules: None,
last_rotated_at: None,
resource_policy: None,
};
assert!(filter_tag_value(&secret, &["prod"]));
assert!(!filter_tag_value(&secret, &["staging"]));
}
#[test]
fn test_filter_all_searches_name_desc_tags() {
let secret = Secret {
name: "my-secret".to_string(),
arn: "arn".to_string(),
description: Some("important database".to_string()),
kms_key_id: None,
versions: BTreeMap::new(),
current_version_id: None,
tags: vec![("team".to_string(), "backend".to_string())],
tags_ever_set: true,
deleted: false,
deletion_date: None,
created_at: Utc::now(),
last_changed_at: Utc::now(),
last_accessed_at: None,
rotation_enabled: None,
rotation_lambda_arn: None,
rotation_rules: None,
last_rotated_at: None,
resource_policy: None,
};
assert!(filter_all(&secret, &["my"]));
assert!(filter_all(&secret, &["database"]));
assert!(filter_all(&secret, &["team"]));
assert!(filter_all(&secret, &["backend"]));
assert!(!filter_all(&secret, &["zzzz"]));
}
fn make_request_for(action: &str, account: &str, body: &str) -> AwsRequest {
let mut req = make_request(action, body);
req.account_id = account.to_string();
req
}
#[tokio::test]
async fn cross_account_get_secret_value_denied_without_policy() {
let state = make_state();
let svc = SecretsManagerService::new(state);
let req = make_request_for(
"CreateSecret",
"111111111111",
r#"{"Name": "shared/secret", "SecretString": "ssss"}"#,
);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let arn = body["ARN"].as_str().unwrap().to_string();
let req = make_request_for(
"GetSecretValue",
"222222222222",
&format!(r#"{{"SecretId": "{arn}"}}"#),
);
let err = expect_err(svc.handle(req).await);
assert_eq!(err.code(), "AccessDeniedException");
}
#[tokio::test]
async fn cross_account_get_secret_value_allowed_with_matching_policy() {
let state = make_state();
let svc = SecretsManagerService::new(state.clone());
let req = make_request_for(
"CreateSecret",
"111111111111",
r#"{"Name": "shared/secret", "SecretString": "shhh"}"#,
);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let arn = body["ARN"].as_str().unwrap().to_string();
let policy = serde_json::json!({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::222222222222:root"},
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}]
});
let put_policy = make_request_for(
"PutResourcePolicy",
"111111111111",
&format!(
r#"{{"SecretId": "{arn}", "ResourcePolicy": {}}}"#,
serde_json::to_string(&policy.to_string()).unwrap()
),
);
svc.handle(put_policy).await.unwrap();
let req = make_request_for(
"GetSecretValue",
"222222222222",
&format!(r#"{{"SecretId": "{arn}"}}"#),
);
let resp = svc.handle(req).await.unwrap();
let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
assert_eq!(body["SecretString"].as_str().unwrap(), "shhh");
}
#[test]
fn secret_owner_account_extracts_from_arn() {
assert_eq!(
secret_owner_account(
"arn:aws:secretsmanager:us-east-1:111111111111:secret:s-abc123",
"999999999999"
),
"111111111111"
);
assert_eq!(
secret_owner_account("plain-name", "999999999999"),
"999999999999"
);
}