Repository: getmoto/moto
Pull request #8223 (2024-10-11 14:50:44)

SecretsManager - Allow force deletion of already marked-for-delete secret
## Motivation
AWS supports force deletion of already deleted secrets, but this is blocked in moto.

This PR enables this, including a test.

## Changes
* Allow force-deletion of secrets already marked for deletion


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8223?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.53%. Comparing base [(`43410b9`)](https://app.codecov.io/gh/getmoto/moto/commit/43410b987e1a5a172a7d429d68f5c94105154476?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`7b04a77`)](https://app.codecov.io/gh/getmoto/moto/commit/7b04a77e184d881b2fa39dd380c3f3676471a4a0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 9 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           master    #8223    +/-   ##
========================================
  Coverage   94.53%   94.53%            
========================================
  Files        1155     1157     +2     
  Lines       99707    99973   +266     
========================================
+ Hits        94261    94513   +252     
- Misses       5446     5460    +14     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8223/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8223/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.80% <0.00%> (-0.05%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8223/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.51% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8223?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2b8c40350..89a404cf3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [ 3.8, 3.9, "3.10", "3.11", "3.12", "3.13.0-rc.1" ]
+        python-version: [ 3.8, 3.9, "3.10", "3.11", "3.12", "3.13" ]
 
     steps:
       - uses: actions/checkout@v4
@@ -47,7 +47,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13.0-rc.1"]
+        python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
     steps:
     - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
@@ -81,6 +81,10 @@ jobs:
     needs: lint
     uses: ./.github/workflows/tests_sdk_java.yml
 
+  jstest:
+    needs: lint
+    uses: ./.github/workflows/tests_sdk_js.yml
+
   dotnettest:
     needs: lint
     uses: ./.github/workflows/tests_sdk_dotnet.yml
diff --git a/.gitignore b/.gitignore
index 9587a7d81..467eb180b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,5 @@ other_langs/terraform/*/.terraform*
 other_langs/terraform/*/terraform*
 other_langs/terraform/awslambda/lambda_function_payload.zip
 other_langs/tests_dotnet/ebs/obj/Debug/*
+other_langs/tests_js/node_modules
+other_langs/tests_js/package-lock.json
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 27e35b2b4..472ffd904 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -468,7 +468,7 @@ INSTANCE_TEMPLATE = """<item>
           {% if instance.key_name is not none %}
              <keyName>{{ instance.key_name }}</keyName>
           {% endif %}
-          <ebsOptimized>{{ instance.ebs_optimized }}</ebsOptimized>
+          <ebsOptimized>{{ instance.ebs_optimized | lower }}</ebsOptimized>
           <amiLaunchIndex>{{ instance.ami_launch_index }}</amiLaunchIndex>
           <instanceType>{{ instance.instance_type }}</instanceType>
           {% if instance.iam_instance_profile %}
@@ -537,7 +537,7 @@ INSTANCE_TEMPLATE = """<item>
                      <volumeId>{{ deviceobject.volume_id }}</volumeId>
                      <status>{{ deviceobject.status }}</status>
                      <attachTime>{{ deviceobject.attach_time }}</attachTime>
-                     <deleteOnTermination>{{ deviceobject.delete_on_termination }}</deleteOnTermination>
+                     <deleteOnTermination>{{ 'true' if deviceobject.delete_on_termination else 'false' }}</deleteOnTermination>
                      <size>{{deviceobject.size}}</size>
                 </ebs>
               </item>
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 21c5f083a..2a768a91a 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -939,9 +939,9 @@ class DBInstance(CloudFormationModel, RDSBaseModel):
                 <VpcId>{{ database.db_subnet_group.vpc_id }}</VpcId>
               </DBSubnetGroup>
               {% endif %}
-              <PubliclyAccessible>{{ database.publicly_accessible }}</PubliclyAccessible>
-              <CopyTagsToSnapshot>{{ database.copy_tags_to_snapshot }}</CopyTagsToSnapshot>
-              <AutoMinorVersionUpgrade>{{ database.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
+              <PubliclyAccessible>{{ 'true' if database.publicly_accessible else 'false' }}</PubliclyAccessible>
+              <CopyTagsToSnapshot>{{ 'true' if database.copy_tags_to_snapshot else 'false' }}</CopyTagsToSnapshot>
+              <AutoMinorVersionUpgrade>{{ 'true' if database.auto_minor_version_upgrade else 'false' }}</AutoMinorVersionUpgrade>
               <AllocatedStorage>{{ database.allocated_storage }}</AllocatedStorage>
               <StorageEncrypted>{{ 'true' if database.storage_encrypted else 'false' }}</StorageEncrypted>
               {% if database.kms_key_id %}
diff --git a/moto/secretsmanager/models.py b/moto/secretsmanager/models.py
index 5a3cf7f05..bbebebb58 100644
--- a/moto/secretsmanager/models.py
+++ b/moto/secretsmanager/models.py
@@ -948,7 +948,7 @@ class SecretsManagerBackend(BaseBackend):
                 msg = f"You can't delete secret {secret_id} that still has replica regions [{replica_regions}]"
                 raise InvalidParameterException(msg)
 
-            if secret.is_deleted():
+            if secret.is_deleted() and not force_delete_without_recovery:
                 raise InvalidRequestException(
                     "An error occurred (InvalidRequestException) when calling the DeleteSecret operation: You tried to \
                     perform the operation on a secret that's currently marked deleted."
diff --git a/setup.py b/setup.py
index e402fb4b6..4ed919a8c 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@ class CompressJsonCommand(Command):
                 print(f"Compressing {file} into {target_filename}...")  # noqa
                 with open(file, "rb") as source:
                     with open(target_filename, "wb") as target:
-                        target.write(gzip.compress(source.read()))
+                        target.write(gzip.compress(source.read(), mtime=0))
                 print(f"Removing {file}...")  # noqa
                 Path(file).unlink()
 


Test patch:
diff --git a/.github/workflows/tests_decoratormode.yml b/.github/workflows/tests_decoratormode.yml
index 8cdc43aba..bfbe19203 100644
--- a/.github/workflows/tests_decoratormode.yml
+++ b/.github/workflows/tests_decoratormode.yml
@@ -7,7 +7,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13.0-rc.1"]
+        python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
 
     steps:
     - uses: actions/checkout@v4
diff --git a/.github/workflows/tests_sdk_js.yml b/.github/workflows/tests_sdk_js.yml
new file mode 100644
index 000000000..64dc93c8e
--- /dev/null
+++ b/.github/workflows/tests_sdk_js.yml
@@ -0,0 +1,32 @@
+name: JS SDK test
+on: [workflow_call]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+    - name: Set up Python 3.11
+      uses: actions/setup-python@v5
+      with:
+        python-version: "3.11"
+    - name: Start MotoServer
+      run: |
+        pip install build
+        python -m build
+        docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock python:3.11-slim /moto/scripts/ci_moto_server.sh &
+        python scripts/ci_wait_for_server.py
+    - uses: actions/setup-node@v4
+    - name: Install dependencies
+      run: |
+        pwd
+        ls -la
+        cd other_langs/tests_js && npm install
+    - name: Run tests
+      run: |
+        pwd
+        ls -la
+        cd other_langs/tests_js && npm test
diff --git a/other_langs/tests_js/ec2.test.js b/other_langs/tests_js/ec2.test.js
new file mode 100644
index 000000000..6fbb83792
--- /dev/null
+++ b/other_langs/tests_js/ec2.test.js
@@ -0,0 +1,15 @@
+import { EC2Client, RunInstancesCommand } from '@aws-sdk/client-ec2'
+
+process.env['AWS_ACCESS_KEY_ID'] = 'test'
+process.env['AWS_SECRET_ACCESS_KEY'] = 'test'
+
+const client = new EC2Client({endpoint:'http://localhost:5000', region: 'us-east-1'});
+
+const command = new RunInstancesCommand({
+    InstanceType: 't2.nano',
+    ImageId: 'ami-0001',
+    MinCount: 1,
+    MaxCount: 1,
+});
+
+const { Instances } = await client.send(command);
diff --git a/other_langs/tests_js/package.json b/other_langs/tests_js/package.json
new file mode 100644
index 000000000..242dc288f
--- /dev/null
+++ b/other_langs/tests_js/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "aws-sdk-v3-examples",
+  "version": "1.0.0",
+  "main": "index.js",
+  "dependencies": {
+    "@aws-sdk/client-ec2": "^3.32.0",
+    "@aws-sdk/client-rds": "^3.32.0",
+    "@aws-sdk/client-s3": "^3.32.0"
+  },
+  "type": "module",
+  "scripts": {
+    "test": "node --test *.test.js"
+  }
+}
diff --git a/other_langs/tests_js/rds.test.js b/other_langs/tests_js/rds.test.js
new file mode 100644
index 000000000..44d812f97
--- /dev/null
+++ b/other_langs/tests_js/rds.test.js
@@ -0,0 +1,19 @@
+import { CreateDBInstanceCommand, DescribeDBInstancesCommand, RDSClient } from '@aws-sdk/client-rds'
+
+process.env['AWS_ACCESS_KEY_ID'] = 'test'
+process.env['AWS_SECRET_ACCESS_KEY'] = 'test'
+
+const client = new RDSClient({region: 'us-east-1', endpoint:'http://localhost:5000'});
+const create_command = new CreateDBInstanceCommand(
+    {
+        DBInstanceClass: "dbic",
+        DBInstanceIdentifier: "dbii",
+        Engine: "mysql",
+        DBName: "dbn"
+    }
+);
+
+await client.send(create_command);
+
+const command = new DescribeDBInstancesCommand({});
+const res = await client.send(command);
diff --git a/other_langs/tests_js/s3.test.js b/other_langs/tests_js/s3.test.js
new file mode 100644
index 000000000..c62e9d65d
--- /dev/null
+++ b/other_langs/tests_js/s3.test.js
@@ -0,0 +1,13 @@
+import { CreateBucketCommand, S3Client } from '@aws-sdk/client-s3'
+
+process.env['AWS_ACCESS_KEY_ID'] = 'test'
+process.env['AWS_SECRET_ACCESS_KEY'] = 'test'
+
+
+const s3 = new S3Client({
+  region:'us-east-1',
+  endpoint: 'http://localhost:5000',
+  forcePathStyle: true,
+})
+
+s3.send(new CreateBucketCommand({ Bucket: 'asdfqwerasdfqwe3' }))
diff --git a/tests/test_secretsmanager/test_secretsmanager.py b/tests/test_secretsmanager/test_secretsmanager.py
index e386aff25..8879b64f0 100644
--- a/tests/test_secretsmanager/test_secretsmanager.py
+++ b/tests/test_secretsmanager/test_secretsmanager.py
@@ -628,6 +628,17 @@ def test_delete_secret_that_is_marked_deleted():
         conn.delete_secret(SecretId="test-secret")
 
 
+@mock_aws
+def test_force_delete_secret_that_is_marked_deleted():
+    conn = boto3.client("secretsmanager", region_name="us-west-2")
+
+    conn.create_secret(Name="test-secret", SecretString="foosecret")
+
+    conn.delete_secret(SecretId="test-secret")
+
+    conn.delete_secret(SecretId="test-secret", ForceDeleteWithoutRecovery=True)
+
+
 @mock_aws
 def test_get_random_password_default_length():
     conn = boto3.client("secretsmanager", region_name="us-west-2")